Skip to content

Commit 3367b5b

Browse files
anxkhncopybara-github
authored andcommitted
fix: let httpx set the multipart boundary Content-Type
Merge #6393 PiperOrigin-RevId: 951002160
1 parent 08d5e50 commit 3367b5b

2 files changed

Lines changed: 58 additions & 2 deletions

File tree

src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,7 +454,11 @@ def _prepare_request_params(
454454
elif mime_type == "text/plain":
455455
body_kwargs["data"] = body_data
456456

457-
if mime_type:
457+
# For multipart/form-data the Content-Type is left unset so httpx can
458+
# generate it from the `files` payload together with the required
459+
# boundary parameter. Forcing a boundary-less header here would make the
460+
# request body unparsable by the server.
461+
if mime_type and mime_type != "multipart/form-data":
458462
header_params["Content-Type"] = mime_type
459463
break # Process only the first mime_type
460464

tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,59 @@ def test_prepare_request_params_multipart(
673673
request_params = tool._prepare_request_params(params, kwargs)
674674

675675
assert request_params["files"] == {"file1": b"file_content"}
676-
assert request_params["headers"]["Content-Type"] == "multipart/form-data"
676+
# For multipart/form-data the boundary-bearing Content-Type must be set by
677+
# httpx (from the `files` payload), not forced here. Forcing a boundary-less
678+
# "multipart/form-data" header would override the boundary httpx generates
679+
# and make the request body unparsable.
680+
assert "Content-Type" not in request_params["headers"]
681+
682+
def test_prepare_request_params_multipart_content_type_has_boundary(
683+
self, sample_endpoint, sample_auth_credential, sample_auth_scheme
684+
):
685+
mock_operation = Operation(
686+
operationId="test_op",
687+
requestBody=RequestBody(
688+
content={
689+
"multipart/form-data": MediaType(
690+
schema=OpenAPISchema(
691+
type="object",
692+
properties={
693+
"file1": OpenAPISchema(
694+
type="string", format="binary"
695+
)
696+
},
697+
)
698+
)
699+
}
700+
),
701+
)
702+
tool = RestApiTool(
703+
name="test_tool",
704+
description="test",
705+
endpoint=sample_endpoint,
706+
operation=mock_operation,
707+
auth_credential=sample_auth_credential,
708+
auth_scheme=sample_auth_scheme,
709+
)
710+
params = [
711+
ApiParameter(
712+
original_name="file1",
713+
py_name="file1",
714+
param_location="body",
715+
param_schema=OpenAPISchema(type="string", format="binary"),
716+
)
717+
]
718+
kwargs = {"file1": b"file_content"}
719+
720+
request_params = tool._prepare_request_params(params, kwargs)
721+
722+
# Build the request httpx would actually send and assert the wire
723+
# Content-Type carries the multipart boundary that matches the body.
724+
request = httpx.Client().build_request(**request_params)
725+
content_type = request.headers["Content-Type"]
726+
assert content_type.startswith("multipart/form-data; boundary=")
727+
boundary = content_type.split("boundary=", 1)[1]
728+
assert request.read().startswith(f"--{boundary}".encode())
677729

678730
def test_prepare_request_params_octet_stream(
679731
self, sample_endpoint, sample_auth_scheme, sample_auth_credential

0 commit comments

Comments
 (0)