Skip to content

fix(openapi): let httpx set the multipart boundary Content-Type#6393

Open
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/openapi-multipart-content-type
Open

fix(openapi): let httpx set the multipart boundary Content-Type#6393
anxkhn wants to merge 1 commit into
google:mainfrom
anxkhn:fix/openapi-multipart-content-type

Conversation

@anxkhn

@anxkhn anxkhn commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

  • N/A (no existing issue; described below per the issue-template structure)

2. Or, if no issue exists, describe the change:

Problem:

RestApiTool sends every multipart/form-data request body with a boundary-less
Content-Type: multipart/form-data header, so the request cannot be parsed by the
receiving server.

In _prepare_request_params
(src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py) a
multipart body is passed to httpx via body_kwargs["files"] = body_data. When
files= is used, httpx builds a boundary-delimited body and generates a matching
Content-Type: multipart/form-data; boundary=<...> header. The method then
unconditionally runs header_params["Content-Type"] = mime_type, which overrides
httpx's generated header with a plain multipart/form-data that has no boundary
parameter. The sent request therefore advertises no boundary while the body uses
one, so an RFC 7578 server cannot find the part delimiters (typically a 400 or
empty/dropped fields). Every multipart upload tool generated from an OpenAPI spec
is affected.

Reproduced on the pinned httpx 0.28.1: the outgoing request body starts with
--<hex>\r\n... but the Content-Type header is exactly multipart/form-data
with no boundary=.

Solution:

Skip forcing Content-Type only for the multipart/form-data branch, so httpx
emits the correct boundary-bearing header it derives from the files payload. All
other media types (application/json, application/x-www-form-urlencoded,
application/octet-stream, text/plain) keep setting Content-Type exactly as
before.

# 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

This is the minimal fix: one guard condition plus an explanatory comment. No other
behavior changes.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Changes in
tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py:

  • Added test_prepare_request_params_multipart_content_type_has_boundary: builds
    the request httpx would actually send
    (httpx.Client().build_request(**request_params)) and asserts the wire
    Content-Type starts with multipart/form-data; boundary= and that the request
    body begins with the matching --<boundary> delimiter. This is a wire-level
    regression test; the previous test only inspected the intermediate dict and so
    masked the bug.
  • Updated the existing test_prepare_request_params_multipart: it previously
    asserted the boundary-less header (== "multipart/form-data"); it now asserts
    Content-Type is not forced by the tool ("Content-Type" not in headers).

Effectiveness (red/green): reverting the source guard back to if mime_type: makes
both multipart tests fail with the exact bug signature
("multipart/form-data".startswith("multipart/form-data; boundary=") is False);
restoring the guard makes them pass.

pytest summary (httpx 0.28.1):

tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py -k multipart
  -> 2 passed
tests/unittests/tools/openapi_tool
  -> 235 passed

Manual End-to-End (E2E) Tests:

Minimal reproduction of the wire behavior on the pinned httpx:

import httpx

# What the tool builds for a multipart body:
req = httpx.Client().build_request(
    method="POST",
    url="https://example.com/upload",
    files={"file1": b"file_content"},
)
print(req.headers["Content-Type"])
# Before this change the tool also set headers["Content-Type"]="multipart/form-data",
# which overrode the value above with a boundary-less header.
# After this change httpx's header is used, e.g.:
#   multipart/form-data; boundary=1b2c...   (body starts b"--1b2c...")

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules. (N/A)

Additional context

Scope is intentionally tiny: one guard condition and a comment in
rest_api_tool.py, plus the two test updates above.


Files changed

  • src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py
    (+5 / -1: the != "multipart/form-data" guard + 4-line explanatory comment)
  • tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py
    (+53 / -1: new wire-level regression test; corrected the masking assertion)

httpx was already imported in the test file (no new import).

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>
@adk-bot adk-bot added the tools [Component] This issue is related to tools label Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

tools [Component] This issue is related to tools

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants