From 9ed92ae487778e8a6976814fc219770688950aeb Mon Sep 17 00:00:00 2001 From: Naveen Chatlapalli Date: Sat, 19 Sep 2026 00:44:31 -0500 Subject: [PATCH] fix(openapi): decode error bodies with the declared charset RestApiTool built the error message for a failed call with response.content.decode("utf-8"). A non-UTF-8 error body (a Latin-1 page from a legacy server, a binary body) raised UnicodeDecodeError inside the HTTPStatusError handler, where the sibling ValueError handler cannot catch it, and ended the whole agent run instead of reporting the HTTP error to the model. Use response.text, as the non-JSON success path already does: it honors the declared charset and replaces undecodable bytes. Fixes #7206 Claude-Session: https://claude.ai/code/session_013vXxD1ga1hnCq2uFRwNks7 --- .../openapi_spec_parser/rest_api_tool.py | 2 +- .../openapi_spec_parser/test_rest_api_tool.py | 33 ++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) 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 b9f91f8e212..fca6c834e02 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 5cd2d340e87..87e0e0c87b0 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 @@ -355,7 +355,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 +390,37 @@ 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_decodes_body_with_declared_charset( + self, + mock_request, + mock_tool_context, + sample_endpoint, + sample_operation, + ): + """A non-UTF-8 error body must reach the model, not abort the run.""" + mock_request.return_value = httpx.Response( + status_code=404, + request=httpx.Request("GET", "https://example.com/test"), + content="Commande introuvable : échec".encode("latin-1"), + headers={"content-type": "text/plain; charset=iso-8859-1"}, + ) + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=sample_operation, + ) + + result = await tool.call(args={}, tool_context=mock_tool_context) + + assert result["error"].endswith( + "Status Code: 404, Commande introuvable : échec" + ) + @patch( "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" )