From 53d9d6a399eda61b66d7337cd3e76aacd6aba5d2 Mon Sep 17 00:00:00 2001 From: chelsealong Date: Sat, 19 Sep 2026 05:50:47 +0000 Subject: [PATCH] fix(tools): decode RestApiTool HTTP error bodies with declared charset response.content.decode("utf-8") raises UnicodeDecodeError for a non-UTF-8 error body (e.g. a Latin-1 page from a legacy server), inside the except httpx.HTTPStatusError handler where the sibling except ValueError can't catch it, crashing the whole agent run. Use response.text instead, matching the existing non-JSON success path a few lines below, which decodes with the response's declared charset and replaces undecodable bytes rather than raising. Fixes #7206 --- .../openapi_spec_parser/rest_api_tool.py | 2 +- .../openapi_spec_parser/test_rest_api_tool.py | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py index b9f91f8e21..fca6c834e0 100644 --- a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +++ b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py @@ -640,7 +640,7 @@ async def call( response.raise_for_status() # Raise HTTPStatusError for bad responses return response.json() # Try to decode JSON except httpx.HTTPStatusError: - error_details = response.content.decode("utf-8") + error_details = response.text self._logger.warning( "API call failed for tool %s: Status %d - %s", self.name, diff --git a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py index 5cd2d340e8..578db9421f 100644 --- a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py +++ b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py @@ -356,6 +356,7 @@ async def test_call_http_failure( mock_response = MagicMock() mock_response.status_code = 500 mock_response.content = b"Internal Server Error" + mock_response.text = "Internal Server Error" # Create a proper HTTPStatusError with request and response mock_http_request = MagicMock(spec=httpx.Request) @@ -390,6 +391,53 @@ async def test_call_http_failure( ) } + @patch( + "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" + ) + @pytest.mark.asyncio + async def test_call_http_failure_non_utf8_body( + self, + mock_request, + mock_tool_context, + sample_endpoint, + sample_operation, + sample_auth_scheme, + sample_auth_credential, + ): + # A server that declares (and uses) a non-UTF-8 charset for its error + # body, e.g. a legacy ERP or IIS server responding in Latin-1. + mock_http_request = httpx.Request("GET", "https://example.com") + mock_response = httpx.Response( + 404, + content="Commande introuvable : échec".encode("latin-1"), + headers={"content-type": "text/plain; charset=iso-8859-1"}, + request=mock_http_request, + ) + mock_request.return_value = mock_response + + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=sample_operation, + auth_scheme=sample_auth_scheme, + auth_credential=sample_auth_credential, + ) + + # Call the method + result = await tool.call(args={}, tool_context=mock_tool_context) + + # Check the result: the response is decoded with its declared charset + # instead of raising UnicodeDecodeError under a hardcoded utf-8 decode. + assert result == { + "error": ( + "Tool test_tool execution failed. Analyze this execution error" + " and your inputs. Retry with adjustments if applicable. But" + " make sure don't retry more than 3 times. Execution Error:" + " Status Code: 404, Commande introuvable : échec" + ) + } + @patch( "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" )