From 03af0007d2f86eac8493fecd151a4b6b21acd630 Mon Sep 17 00:00:00 2001 From: Declan Jackson Date: Thu, 29 Jan 2026 17:09:04 +1100 Subject: [PATCH 1/3] catch ValueError from httpx --- src/stirrup/tools/web.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stirrup/tools/web.py b/src/stirrup/tools/web.py index 1da23e6..2a4e4dd 100644 --- a/src/stirrup/tools/web.py +++ b/src/stirrup/tools/web.py @@ -121,7 +121,7 @@ async def fetch_web_page_executor(params: FetchWebPageParams) -> ToolResult[WebF f"{truncate_msg(body_md, MAX_LENGTH_WEB_FETCH_HTML)}", metadata=WebFetchMetadata(pages_fetched=[params.url]), ) - except httpx.HTTPError as exc: + except (httpx.HTTPError, ValueError) as exc: return ToolResult( content=f"{params.url}" f"{truncate_msg(str(exc), MAX_LENGTH_WEB_FETCH_HTML)}", From fe2f04db3866c7b499302e5291c8557b336d032f Mon Sep 17 00:00:00 2001 From: Declan Jackson Date: Thu, 29 Jan 2026 17:32:59 +1100 Subject: [PATCH 2/3] handle E2B error properly --- src/stirrup/tools/code_backends/e2b.py | 7 +++++++ tests/test_e2b_execution.py | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/src/stirrup/tools/code_backends/e2b.py b/src/stirrup/tools/code_backends/e2b.py index eeaf82c..d489b92 100644 --- a/src/stirrup/tools/code_backends/e2b.py +++ b/src/stirrup/tools/code_backends/e2b.py @@ -258,6 +258,13 @@ async def run_command(self, cmd: str, *, timeout: int = SHELL_TIMEOUT) -> Comman stderr=str(exc), error_kind="timeout", ) + except Exception as exc: + return CommandResult( + exit_code=1, + stdout="", + stderr=str(exc), + error_kind="execution_error", + ) async def save_output_files( self, diff --git a/tests/test_e2b_execution.py b/tests/test_e2b_execution.py index b930175..eacefba 100644 --- a/tests/test_e2b_execution.py +++ b/tests/test_e2b_execution.py @@ -107,6 +107,13 @@ async def test_run_command_exceptions(self, mock_sandbox: MagicMock) -> None: result = await provider.run_command("sleep 1000") assert result.error_kind == "timeout" + # Test unexpected Exception (catch-all handler) + mock_sandbox.commands.run = AsyncMock(side_effect=RuntimeError("E2B API failure")) + result = await provider.run_command("some command") + assert result.error_kind == "execution_error" + assert result.exit_code == 1 + assert "E2B API failure" in result.stderr + async def test_run_command_allowlist(self, mock_sandbox: MagicMock) -> None: """Test command allowlist enforcement.""" provider = E2BCodeExecToolProvider(allowed_commands=[r"^echo", r"^python"]) From eeb87a08e5be14b956cb9c5b68a9e7ccf6b3d021 Mon Sep 17 00:00:00 2001 From: Declan Jackson Date: Thu, 29 Jan 2026 17:33:47 +1100 Subject: [PATCH 3/3] add handling for HTTP errors in web search --- src/stirrup/tools/web.py | 55 ++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/stirrup/tools/web.py b/src/stirrup/tools/web.py index 2a4e4dd..d0759b5 100644 --- a/src/stirrup/tools/web.py +++ b/src/stirrup/tools/web.py @@ -208,33 +208,40 @@ async def _search(query: str, http_client: httpx.AsyncClient) -> dict: async def websearch_executor(params: WebSearchParams) -> ToolResult[WebSearchMetadata]: """Execute web search and format results as XML with title, URL, and description.""" - # Use provided client or create temporary one for backward compatibility - if client is not None: - data = await _search(params.query, client) - else: - async with httpx.AsyncClient(timeout=WEB_SEARCH_TIMEOUT) as temp_client: - data = await _search(params.query, temp_client) - - results = data.get("web", {}).get("results", []) - results_xml = ( - "\n" - + "\n".join( - ( - "" - f"\n{escape(result.get('title', '') or '')}" - f"\n{escape(result.get('url', '') or '')}" - f"\n{escape(result.get('description', '') or '')}" - "\n" + try: + # Use provided client or create temporary one for backward compatibility + if client is not None: + data = await _search(params.query, client) + else: + async with httpx.AsyncClient(timeout=WEB_SEARCH_TIMEOUT) as temp_client: + data = await _search(params.query, temp_client) + + results = data.get("web", {}).get("results", []) + results_xml = ( + "\n" + + "\n".join( + ( + "" + f"\n{escape(result.get('title', '') or '')}" + f"\n{escape(result.get('url', '') or '')}" + f"\n{escape(result.get('description', '') or '')}" + "\n" + ) + for result in results ) - for result in results + + "\n" ) - + "\n" - ) - return ToolResult( - content=truncate_msg(results_xml, MAX_LENGTH_WEB_SEARCH_RESULTS), - metadata=WebSearchMetadata(pages_returned=len(results)), - ) + return ToolResult( + content=truncate_msg(results_xml, MAX_LENGTH_WEB_SEARCH_RESULTS), + metadata=WebSearchMetadata(pages_returned=len(results)), + ) + except httpx.HTTPError as exc: + return ToolResult( + content=f"{truncate_msg(str(exc), 500)}", + success=False, + metadata=WebSearchMetadata(pages_returned=0), + ) return Tool[WebSearchParams, WebSearchMetadata]( name="web_search",