Skip to content

Commit bf4143a

Browse files
GWealecopybara-github
authored andcommitted
fix: bound integration HTTP waits
Several synchronous API Hub and Application Integration requests had no timeout, so an unavailable endpoint could block agent execution indefinitely. One API Hub fallback error also printed the literal {path} instead of the rejected path. This applies a 30-second timeout to the affected GET and POST calls and preserves the rejected path in the error. Co-authored-by: George Weale <gweale@google.com> PiperOrigin-RevId: 952369892
1 parent c328cec commit bf4143a

6 files changed

Lines changed: 44 additions & 7 deletions

File tree

src/google/adk/tools/apihub_tool/clients/apihub_client.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
from google.oauth2 import service_account
3333
import requests
3434

35+
_DEFAULT_REQUEST_TIMEOUT_SECONDS = 30
36+
3537

3638
class BaseAPIHubClient(ABC):
3739
"""Base class for API Hub clients."""
@@ -116,7 +118,7 @@ def get_spec_content(self, path: str) -> str:
116118
spec_content = self._fetch_spec(api_spec_resource_name)
117119
return spec_content
118120

119-
raise ValueError("No API Hub resource found in path: {path}")
121+
raise ValueError(f"No API Hub resource found in path: {path}")
120122

121123
def list_apis(self, project: str, location: str) -> List[Dict[str, Any]]:
122124
"""Lists all APIs in the specified project and location.
@@ -133,7 +135,9 @@ def list_apis(self, project: str, location: str) -> List[Dict[str, Any]]:
133135
"accept": "application/json, text/plain, */*",
134136
"Authorization": f"Bearer {self._get_access_token()}",
135137
}
136-
response = requests.get(url, headers=headers)
138+
response = requests.get(
139+
url, headers=headers, timeout=_DEFAULT_REQUEST_TIMEOUT_SECONDS
140+
)
137141
response.raise_for_status()
138142
apis = response.json().get("apis", [])
139143
return apis
@@ -153,7 +157,9 @@ def get_api(self, api_resource_name: str) -> Dict[str, Any]:
153157
"accept": "application/json, text/plain, */*",
154158
"Authorization": f"Bearer {self._get_access_token()}",
155159
}
156-
response = requests.get(url, headers=headers)
160+
response = requests.get(
161+
url, headers=headers, timeout=_DEFAULT_REQUEST_TIMEOUT_SECONDS
162+
)
157163
response.raise_for_status()
158164
apis = response.json()
159165
return apis
@@ -173,7 +179,9 @@ def get_api_version(self, api_version_name: str) -> Dict[str, Any]:
173179
"accept": "application/json, text/plain, */*",
174180
"Authorization": f"Bearer {self._get_access_token()}",
175181
}
176-
response = requests.get(url, headers=headers)
182+
response = requests.get(
183+
url, headers=headers, timeout=_DEFAULT_REQUEST_TIMEOUT_SECONDS
184+
)
177185
response.raise_for_status()
178186
return response.json()
179187

@@ -192,7 +200,9 @@ def _fetch_spec(self, api_spec_resource_name: str) -> str:
192200
"accept": "application/json, text/plain, */*",
193201
"Authorization": f"Bearer {self._get_access_token()}",
194202
}
195-
response = requests.get(url, headers=headers)
203+
response = requests.get(
204+
url, headers=headers, timeout=_DEFAULT_REQUEST_TIMEOUT_SECONDS
205+
)
196206
response.raise_for_status()
197207
content_base64 = response.json().get("contents", "")
198208
if content_base64:

src/google/adk/tools/application_integration_tool/clients/connections_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
_DEFAULT_MTLS_INTEGRATIONS_ENDPOINT_TEMPLATE = (
3737
"integrations.mtls.googleapis.com"
3838
)
39+
_DEFAULT_REQUEST_TIMEOUT_SECONDS = 30
3940

4041

4142
class ConnectionsClient:
@@ -886,7 +887,9 @@ def _execute_api_call(self, url):
886887
"Authorization": f"Bearer {self._get_access_token()}",
887888
}
888889

889-
response = requests.get(url, headers=headers)
890+
response = requests.get(
891+
url, headers=headers, timeout=_DEFAULT_REQUEST_TIMEOUT_SECONDS
892+
)
890893
response.raise_for_status()
891894
return response
892895

src/google/adk/tools/application_integration_tool/clients/integration_client.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
_DEFAULT_MTLS_INTEGRATIONS_REGIONAL_ENDPOINT_TEMPLATE = (
3535
"{location}-integrations.mtls.googleapis.com"
3636
)
37+
_DEFAULT_REQUEST_TIMEOUT_SECONDS = 30
3738

3839

3940
class IntegrationClient:
@@ -118,7 +119,12 @@ def get_openapi_spec_for_integration(self):
118119
],
119120
"fileFormat": "JSON",
120121
}
121-
response = requests.post(url, headers=headers, json=data)
122+
response = requests.post(
123+
url,
124+
headers=headers,
125+
json=data,
126+
timeout=_DEFAULT_REQUEST_TIMEOUT_SECONDS,
127+
)
122128
response.raise_for_status()
123129
spec = response.json().get("openApiSpec", {})
124130
return json.loads(spec)

tests/unittests/tools/apihub_tool/clients/test_apihub_client.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ def test_list_apis(self, mock_get, client):
7474
"accept": "application/json, text/plain, */*",
7575
"Authorization": "Bearer mocked_token",
7676
},
77+
timeout=30,
7778
)
7879

7980
@patch("requests.get")
@@ -105,6 +106,7 @@ def test_get_api(self, mock_get, client):
105106
"accept": "application/json, text/plain, */*",
106107
"Authorization": "Bearer mocked_token",
107108
},
109+
timeout=30,
108110
)
109111

110112
@patch("requests.get")
@@ -127,6 +129,7 @@ def test_get_api_version(self, mock_get, client):
127129
"accept": "application/json, text/plain, */*",
128130
"Authorization": "Bearer mocked_token",
129131
},
132+
timeout=30,
130133
)
131134

132135
@patch("requests.get")
@@ -151,6 +154,7 @@ def test_get_spec_content(self, mock_get, client):
151154
"accept": "application/json, text/plain, */*",
152155
"Authorization": "Bearer mocked_token",
153156
},
157+
timeout=30,
154158
)
155159

156160
@patch("requests.get")
@@ -519,6 +523,17 @@ def test_get_spec_content_invalid_path(self, mock_get, client):
519523
):
520524
client.get_spec_content("invalid-path")
521525

526+
def test_get_spec_content_includes_path_in_fallback_error(self, client):
527+
with (
528+
patch.object(
529+
client, "_extract_resource_name", return_value=(None, None, None)
530+
),
531+
pytest.raises(
532+
ValueError, match="No API Hub resource found in path: actual-path"
533+
),
534+
):
535+
client.get_spec_content("actual-path")
536+
522537

523538
if __name__ == "__main__":
524539
pytest.main([__file__])

tests/unittests/tools/application_integration_tool/clients/test_connections_client.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ def test_execute_api_call_success(
110110
"Content-Type": "application/json",
111111
"Authorization": f"Bearer {mock_credentials.token}",
112112
},
113+
timeout=30,
113114
)
114115

115116
def test_execute_api_call_credential_error(

tests/unittests/tools/application_integration_tool/clients/test_integration_client.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ def test_get_openapi_spec_for_integration_success(
162162
}],
163163
"fileFormat": "JSON",
164164
},
165+
timeout=30,
165166
)
166167

167168
def test_get_openapi_spec_for_integration_success_mtls(
@@ -218,6 +219,7 @@ def test_get_openapi_spec_for_integration_success_mtls(
218219
}],
219220
"fileFormat": "JSON",
220221
},
222+
timeout=30,
221223
)
222224

223225
def test_get_openapi_spec_for_integration_credential_error(

0 commit comments

Comments
 (0)