Pass bytes/file-like values directly in multipart, add charset preservation test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
William Easton 2026-04-14 13:31:25 -05:00
commit aad942cc86
No known key found for this signature in database
2 changed files with 69 additions and 4 deletions

View file

@ -1,5 +1,6 @@
"""Request director using openapi-core for stateless HTTP request building."""
import io
import json as _json
from typing import Any, ClassVar
from urllib.parse import quote, urljoin
@ -105,10 +106,17 @@ class RequestDirector:
# treats them as form fields. Scalars must be stringified
# because httpx rejects non-string/bytes values in files=.
# Use _query_scalar_to_str for booleans (true/false, not True/False).
files = {
k: v if isinstance(v, tuple) else (None, _query_scalar_to_str(v))
for k, v in body.items()
}
files = {}
for k, v in body.items():
if isinstance(v, tuple):
files[k] = v
elif isinstance(v, bytes | io.IOBase):
# bytes and file-like objects are passed directly so
# httpx can transmit them as binary parts without
# stringifying to their Python repr.
files[k] = (None, v)
else:
files[k] = (None, _query_scalar_to_str(v))
elif (
declared_content_type == "application/x-www-form-urlencoded"
and isinstance(body, dict)

View file

@ -476,6 +476,33 @@ class TestContentTypeHandling:
assert request.headers["content-type"] == "application/merge-patch+json"
assert json.loads(request.content) == {"name": "test"}
def test_content_type_preserves_media_type_parameters(self, director):
"""Media-type parameters like charset are preserved on the wire."""
route = HTTPRoute(
path="/items",
method="POST",
operation_id="create_item",
request_body=RequestBodyInfo(
required=True,
content_schema={
"application/json-patch+json; charset=utf-8": {
"type": "object",
"properties": {"name": {"type": "string"}},
}
},
),
parameter_map={
"name": {"location": "body", "openapi_name": "name"},
},
)
request = director.build(route, {"name": "test"}, "https://example.com")
assert (
request.headers["content-type"]
== "application/json-patch+json; charset=utf-8"
)
assert json.loads(request.content) == {"name": "test"}
def test_custom_content_type_preserves_other_headers(self, director):
"""Custom content type doesn't clobber other headers from parameters."""
route = HTTPRoute(
@ -1322,6 +1349,36 @@ class TestContentTypeDispatch:
assert "report.csv" in body
assert "a,b,c" in body
def test_multipart_bare_bytes_not_stringified(self, director):
"""Bare bytes values are passed directly to httpx, not repr-stringified."""
route = HTTPRoute(
path="/upload",
method="POST",
operation_id="upload_binary",
request_body=RequestBodyInfo(
required=True,
content_schema={
"multipart/form-data": {
"type": "object",
"properties": {
"payload": {"type": "string", "format": "binary"},
},
}
},
),
parameter_map={
"payload": {"location": "body", "openapi_name": "payload"},
},
)
raw = b"\x89PNG\r\n\x1a\nfake-image-data"
request = director.build(route, {"payload": raw})
content_type = request.headers.get("content-type", "")
assert "multipart/form-data" in content_type
request.read()
# The raw bytes should appear in the body verbatim, not as "b'\\x89PNG...'"
assert raw in request.content
assert b"b'" not in request.content
def test_json_body_still_works(self, director, json_route):
"""application/json bodies should still be sent as JSON (regression)."""
request = director.build(