mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
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:
parent
7c30184f3b
commit
aad942cc86
2 changed files with 69 additions and 4 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue