From 019f946e2932dd28ab4a033ac232f27998430940 Mon Sep 17 00:00:00 2001 From: SUJALMU2004 Date: Sat, 5 Sep 2026 13:50:32 +0530 Subject: [PATCH] fix(tools): handle non-UTF-8 HTTP error responses --- .../openapi_spec_parser/rest_api_tool.py | 2 +- .../openapi_spec_parser/test_rest_api_tool.py | 64 +++++++++++++++---- 2 files changed, 53 insertions(+), 13 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 7b26402044..afb4d24201 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 @@ -610,7 +610,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 10079bfafd..99e4b9a856 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 @@ -322,18 +322,10 @@ async def test_call_http_failure( sample_auth_scheme, sample_auth_credential, ): - mock_response = MagicMock() - mock_response.status_code = 500 - mock_response.content = b"Internal Server Error" - - # Create a proper HTTPStatusError with request and response - mock_http_request = MagicMock(spec=httpx.Request) - mock_response.raise_for_status = MagicMock( - side_effect=httpx.HTTPStatusError( - "500 Server Error", - request=mock_http_request, - response=mock_response, - ) + mock_response = httpx.Response( + 500, + content=b"Internal Server Error", + request=httpx.Request("GET", "https://example.com/test"), ) mock_request.return_value = mock_response @@ -359,6 +351,54 @@ async def test_call_http_failure( ) } + @pytest.mark.parametrize( + "content,content_type,expected_text", + [ + pytest.param( + b"Acc\xe8s refus\xe9", + "text/plain; charset=iso-8859-1", + "Acc\u00e8s refus\u00e9", + id="declared-latin-1", + ), + pytest.param( + b"Invalid byte: \xff", + "text/plain; charset=utf-8", + "Invalid byte: \ufffd", + id="invalid-utf-8", + ), + pytest.param( + b"Invalid byte: \xff", + "text/plain", + "Invalid byte: \ufffd", + id="missing-charset", + ), + ], + ) + async def test_run_async_returns_decoded_http_error( + self, sample_endpoint, content, content_type, expected_text + ): + """HTTP error bodies remain tool errors regardless of their encoding.""" + + def handle_request(request): + return httpx.Response( + 400, content=content, headers={"Content-Type": content_type} + ) + + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=Operation(operationId="testOperation"), + httpx_client_factory=lambda: httpx.AsyncClient( + transport=httpx.MockTransport(handle_request) + ), + ) + + result = await tool.run_async(args={}, tool_context=None) + + assert "Status Code: 400" in result["error"] + assert expected_text in result["error"] + @patch( "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" )