From 5f829c144d4608c809bc2cf71026c0d006ed95e0 Mon Sep 17 00:00:00 2001 From: agharsallah <17379925+agharsallah@users.noreply.github.com> Date: Sun, 5 Jul 2026 20:07:20 +0200 Subject: [PATCH] fix(tools): add timeouts to HTTP requests in API client helpers The requests.get/post calls in the API Hub and Application Integration tool clients omitted timeout=, so python-requests would wait forever on a stalled endpoint, hanging the tool invocation (and the agent). This also brought the clients in line with load_web_page.py, which already sets a default request timeout. Add a module-level _REQUEST_TIMEOUT_SECONDS constant (30s, matching load_web_page.py) to each client module and pass timeout= to every request call site. Extend the existing client tests to assert the mocked request receives timeout. Signed-off-by: agharsallah <17379925+agharsallah@users.noreply.github.com> --- .../apihub_tool/clients/apihub_client.py | 19 +++++++++++++++---- .../clients/connections_client.py | 7 ++++++- .../clients/integration_client.py | 7 ++++++- .../apihub_tool/clients/test_apihub_client.py | 5 +++++ .../clients/test_connections_client.py | 1 + .../clients/test_integration_client.py | 2 ++ 6 files changed, 35 insertions(+), 6 deletions(-) diff --git a/src/google/adk/tools/apihub_tool/clients/apihub_client.py b/src/google/adk/tools/apihub_tool/clients/apihub_client.py index ac566c846b6..6074f2f03e0 100644 --- a/src/google/adk/tools/apihub_tool/clients/apihub_client.py +++ b/src/google/adk/tools/apihub_tool/clients/apihub_client.py @@ -32,6 +32,9 @@ from google.oauth2 import service_account import requests +# Default timeout in seconds for HTTP requests. +_REQUEST_TIMEOUT_SECONDS = 30 + class BaseAPIHubClient(ABC): """Base class for API Hub clients.""" @@ -133,7 +136,9 @@ def list_apis(self, project: str, location: str) -> List[Dict[str, Any]]: "accept": "application/json, text/plain, */*", "Authorization": f"Bearer {self._get_access_token()}", } - response = requests.get(url, headers=headers) + response = requests.get( + url, headers=headers, timeout=_REQUEST_TIMEOUT_SECONDS + ) response.raise_for_status() apis = response.json().get("apis", []) return apis @@ -153,7 +158,9 @@ def get_api(self, api_resource_name: str) -> Dict[str, Any]: "accept": "application/json, text/plain, */*", "Authorization": f"Bearer {self._get_access_token()}", } - response = requests.get(url, headers=headers) + response = requests.get( + url, headers=headers, timeout=_REQUEST_TIMEOUT_SECONDS + ) response.raise_for_status() apis = response.json() return apis @@ -173,7 +180,9 @@ def get_api_version(self, api_version_name: str) -> Dict[str, Any]: "accept": "application/json, text/plain, */*", "Authorization": f"Bearer {self._get_access_token()}", } - response = requests.get(url, headers=headers) + response = requests.get( + url, headers=headers, timeout=_REQUEST_TIMEOUT_SECONDS + ) response.raise_for_status() return response.json() @@ -192,7 +201,9 @@ def _fetch_spec(self, api_spec_resource_name: str) -> str: "accept": "application/json, text/plain, */*", "Authorization": f"Bearer {self._get_access_token()}", } - response = requests.get(url, headers=headers) + response = requests.get( + url, headers=headers, timeout=_REQUEST_TIMEOUT_SECONDS + ) response.raise_for_status() content_base64 = response.json().get("contents", "") if content_base64: diff --git a/src/google/adk/tools/application_integration_tool/clients/connections_client.py b/src/google/adk/tools/application_integration_tool/clients/connections_client.py index c33c9c44249..a9b1e57d1f6 100644 --- a/src/google/adk/tools/application_integration_tool/clients/connections_client.py +++ b/src/google/adk/tools/application_integration_tool/clients/connections_client.py @@ -37,6 +37,9 @@ "integrations.mtls.googleapis.com" ) +# Default timeout in seconds for HTTP requests. +_REQUEST_TIMEOUT_SECONDS = 30 + class ConnectionsClient: """Utility class for interacting with Google Cloud Connectors API.""" @@ -886,7 +889,9 @@ def _execute_api_call(self, url): "Authorization": f"Bearer {self._get_access_token()}", } - response = requests.get(url, headers=headers) + response = requests.get( + url, headers=headers, timeout=_REQUEST_TIMEOUT_SECONDS + ) response.raise_for_status() return response diff --git a/src/google/adk/tools/application_integration_tool/clients/integration_client.py b/src/google/adk/tools/application_integration_tool/clients/integration_client.py index d342d2bbd5a..e58b4ecc3f8 100644 --- a/src/google/adk/tools/application_integration_tool/clients/integration_client.py +++ b/src/google/adk/tools/application_integration_tool/clients/integration_client.py @@ -35,6 +35,9 @@ "{location}-integrations.mtls.googleapis.com" ) +# Default timeout in seconds for HTTP requests. +_REQUEST_TIMEOUT_SECONDS = 30 + class IntegrationClient: """A client for interacting with Google Cloud Application Integration. @@ -118,7 +121,9 @@ def get_openapi_spec_for_integration(self): ], "fileFormat": "JSON", } - response = requests.post(url, headers=headers, json=data) + response = requests.post( + url, headers=headers, json=data, timeout=_REQUEST_TIMEOUT_SECONDS + ) response.raise_for_status() spec = response.json().get("openApiSpec", {}) return json.loads(spec) diff --git a/tests/unittests/tools/apihub_tool/clients/test_apihub_client.py b/tests/unittests/tools/apihub_tool/clients/test_apihub_client.py index 36554e939c2..e008ad14b05 100644 --- a/tests/unittests/tools/apihub_tool/clients/test_apihub_client.py +++ b/tests/unittests/tools/apihub_tool/clients/test_apihub_client.py @@ -14,6 +14,7 @@ import base64 import json +from unittest.mock import ANY from unittest.mock import MagicMock from unittest.mock import patch @@ -74,6 +75,7 @@ def test_list_apis(self, mock_get, client): "accept": "application/json, text/plain, */*", "Authorization": "Bearer mocked_token", }, + timeout=ANY, ) @patch("requests.get") @@ -105,6 +107,7 @@ def test_get_api(self, mock_get, client): "accept": "application/json, text/plain, */*", "Authorization": "Bearer mocked_token", }, + timeout=ANY, ) @patch("requests.get") @@ -127,6 +130,7 @@ def test_get_api_version(self, mock_get, client): "accept": "application/json, text/plain, */*", "Authorization": "Bearer mocked_token", }, + timeout=ANY, ) @patch("requests.get") @@ -151,6 +155,7 @@ def test_get_spec_content(self, mock_get, client): "accept": "application/json, text/plain, */*", "Authorization": "Bearer mocked_token", }, + timeout=ANY, ) @patch("requests.get") diff --git a/tests/unittests/tools/application_integration_tool/clients/test_connections_client.py b/tests/unittests/tools/application_integration_tool/clients/test_connections_client.py index ad7ebdf5c94..cff5be58781 100644 --- a/tests/unittests/tools/application_integration_tool/clients/test_connections_client.py +++ b/tests/unittests/tools/application_integration_tool/clients/test_connections_client.py @@ -110,6 +110,7 @@ def test_execute_api_call_success( "Content-Type": "application/json", "Authorization": f"Bearer {mock_credentials.token}", }, + timeout=mock.ANY, ) def test_execute_api_call_credential_error( diff --git a/tests/unittests/tools/application_integration_tool/clients/test_integration_client.py b/tests/unittests/tools/application_integration_tool/clients/test_integration_client.py index 70de449037f..971102cd289 100644 --- a/tests/unittests/tools/application_integration_tool/clients/test_integration_client.py +++ b/tests/unittests/tools/application_integration_tool/clients/test_integration_client.py @@ -162,6 +162,7 @@ def test_get_openapi_spec_for_integration_success( }], "fileFormat": "JSON", }, + timeout=mock.ANY, ) def test_get_openapi_spec_for_integration_success_mtls( @@ -218,6 +219,7 @@ def test_get_openapi_spec_for_integration_success_mtls( }], "fileFormat": "JSON", }, + timeout=mock.ANY, ) def test_get_openapi_spec_for_integration_credential_error(