From 2aaddc94ad96ca9fad1869960c484a715126eaab Mon Sep 17 00:00:00 2001 From: Anas Khan <83116240+anxkhn@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:32:23 +0530 Subject: [PATCH] fix(openapi): let httpx set the multipart boundary Content-Type For multipart/form-data request bodies, RestApiTool passed files= to httpx (which builds a boundary-delimited body and generates a matching Content-Type: multipart/form-data; boundary=... header) but also forced header_params["Content-Type"] = "multipart/form-data". The forced, boundary-less header overrode httpx's generated one, so the sent request advertised no boundary while the body used one, leaving RFC 7578 servers unable to parse the parts. Skip setting Content-Type for the multipart/form-data branch so httpx emits the boundary-bearing header. Other media types (json, x-www-form-urlencoded, octet-stream, text/plain) are unchanged. Add a regression test that builds the httpx request and asserts the wire Content-Type carries the boundary matching the body delimiter, and update the existing multipart test that previously asserted the boundary-less header. Signed-off-by: Anas Khan <83116240+anxkhn@users.noreply.github.com> --- .../openapi_spec_parser/rest_api_tool.py | 6 ++- .../openapi_spec_parser/test_rest_api_tool.py | 54 ++++++++++++++++++- 2 files changed, 58 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 61cb6a37e43..a96f8c5befa 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 @@ -454,7 +454,11 @@ def _prepare_request_params( elif mime_type == "text/plain": body_kwargs["data"] = body_data - if mime_type: + # For multipart/form-data the Content-Type is left unset so httpx can + # generate it from the `files` payload together with the required + # boundary parameter. Forcing a boundary-less header here would make the + # request body unparsable by the server. + if mime_type and mime_type != "multipart/form-data": header_params["Content-Type"] = mime_type break # Process only the first mime_type 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 ad72915e12b..4799758a241 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 @@ -673,7 +673,59 @@ def test_prepare_request_params_multipart( request_params = tool._prepare_request_params(params, kwargs) assert request_params["files"] == {"file1": b"file_content"} - assert request_params["headers"]["Content-Type"] == "multipart/form-data" + # For multipart/form-data the boundary-bearing Content-Type must be set by + # httpx (from the `files` payload), not forced here. Forcing a boundary-less + # "multipart/form-data" header would override the boundary httpx generates + # and make the request body unparsable. + assert "Content-Type" not in request_params["headers"] + + def test_prepare_request_params_multipart_content_type_has_boundary( + self, sample_endpoint, sample_auth_credential, sample_auth_scheme + ): + mock_operation = Operation( + operationId="test_op", + requestBody=RequestBody( + content={ + "multipart/form-data": MediaType( + schema=OpenAPISchema( + type="object", + properties={ + "file1": OpenAPISchema( + type="string", format="binary" + ) + }, + ) + ) + } + ), + ) + tool = RestApiTool( + name="test_tool", + description="test", + endpoint=sample_endpoint, + operation=mock_operation, + auth_credential=sample_auth_credential, + auth_scheme=sample_auth_scheme, + ) + params = [ + ApiParameter( + original_name="file1", + py_name="file1", + param_location="body", + param_schema=OpenAPISchema(type="string", format="binary"), + ) + ] + kwargs = {"file1": b"file_content"} + + request_params = tool._prepare_request_params(params, kwargs) + + # Build the request httpx would actually send and assert the wire + # Content-Type carries the multipart boundary that matches the body. + request = httpx.Client().build_request(**request_params) + content_type = request.headers["Content-Type"] + assert content_type.startswith("multipart/form-data; boundary=") + boundary = content_type.split("boundary=", 1)[1] + assert request.read().startswith(f"--{boundary}".encode()) def test_prepare_request_params_octet_stream( self, sample_endpoint, sample_auth_scheme, sample_auth_credential