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..7bddd00528 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 @@ -75,6 +75,24 @@ def snake_to_lower_camel(snake_case_string: str): ]) +def _to_param_string(value: Any) -> Any: + """Serializes a header or cookie parameter value for httpx. + + httpx serializes query values itself but rejects anything other than a string + for a header or cookie, so an integer or boolean parameter declared by the + spec would fail the whole tool call. Numbers are stringified, booleans use + their JSON spelling and arrays are comma-joined, matching OpenAPI's default + serialization. Other values, such as strings, are returned unchanged. + """ + if isinstance(value, bool): + return "true" if value else "false" + if isinstance(value, (int, float)): + return str(value) + if isinstance(value, list): + return ",".join(str(_to_param_string(item)) for item in value) + return value + + HttpxClientFactory = Callable[[], httpx.AsyncClient] """Type alias for a zero-argument factory returning an ``httpx.AsyncClient``. @@ -417,9 +435,9 @@ def _prepare_request_params( if v is not None: query_params[original_k] = v elif param_location == "header": - header_params[original_k] = v + header_params[original_k] = _to_param_string(v) elif param_location == "cookie": - cookie_params[original_k] = v + cookie_params[original_k] = _to_param_string(v) # Construct URL base_url = self.endpoint.base_url or "" 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..8d62b48ea5 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 @@ -1919,6 +1919,59 @@ def test_prepare_request_params_cookie_param( assert request_params["cookies"]["session_id"] == "cookie_value" + @pytest.mark.parametrize( + "value, expected", + [ + (50, "50"), + (1.5, "1.5"), + (True, "true"), + (False, "false"), + (["a", "b", 3], "a,b,3"), + ("already-a-string", "already-a-string"), + ], + ) + def test_prepare_request_params_serializes_header_and_cookie_values( + self, + sample_endpoint, + sample_operation, + value, + expected, + ): + """httpx rejects non-string header and cookie values.""" + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=sample_operation, + ) + params = [ + ApiParameter( + original_name="X-Page-Size", + py_name="x_page_size", + param_location="header", + param_schema=OpenAPISchema(type="integer"), + ), + ApiParameter( + original_name="page_size", + py_name="page_size", + param_location="cookie", + param_schema=OpenAPISchema(type="integer"), + ), + ] + kwargs = {"x_page_size": value, "page_size": value} + + request_params = tool._prepare_request_params(params, kwargs) + + assert request_params["headers"]["X-Page-Size"] == expected + assert request_params["cookies"]["page_size"] == expected + # The values must be accepted by httpx when the request is built. + httpx.Request( + request_params["method"], + request_params["url"], + headers=request_params["headers"], + cookies=request_params["cookies"], + ) + def test_prepare_request_params_quota_project_id( self, sample_endpoint,