This commit is contained in:
Prince 2026-11-07 03:29:20 +05:30 • committed by GitHub
commit 518e733bf2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 59 additions and 7 deletions

View file

@ -1,6 +1,6 @@
import json
import re
from collections.abc import Collection, Mapping
from collections.abc import Collection, Mapping, Sequence
from types import MappingProxyType, UnionType
from typing import Annotated, Any, Final, Literal, Protocol, Union, get_args, get_origin
@ -32,10 +32,14 @@ _FORM_CONTENT_TYPES: Final[frozenset[str]] = frozenset({"application/x-www-form-
# Binary bodies (e.g. OTLP trace exports on POST /v1/traces) are not JSON: arbitrary bytes used to
# hit the JSON surrogate-repair path and fail auth with a 400. JSON under these types still parses.
_BINARY_CONTENT_TYPES: Final[frozenset[str]] = frozenset({"application/x-protobuf", "application/protobuf"})
_FORM_FALLBACK_FIELDS: Final[frozenset[str]] = frozenset(
{"fallbacks", "context_window_fallbacks", "content_policy_fallbacks"}
)
_ANNOTATION_QUALIFIERS: Final[frozenset[object]] = frozenset({Annotated, NotRequired, ReadOnly, Required})
_DECODED_JSON: Final = TypeAdapter(object)
_JSON_REQUEST_BODY: Final = TypeAdapter(Mapping[str, object])
_FORM_FALLBACK_LIST: Final = TypeAdapter(list[object])
class _ReadableUpload(Protocol):
@ -233,6 +237,28 @@ def _parse_form_metadata(raw_metadata: str) -> object:
return parsed
def _parse_form_fallbacks(field: str, raw_fallbacks: str) -> Sequence[object]:
try:
return _FORM_FALLBACK_LIST.validate_json(raw_fallbacks)
except ValidationError as e:
raise ProxyException(
message=f"Invalid form payload: {field} must be a JSON array",
type="invalid_request_error",
param=field,
code=status.HTTP_400_BAD_REQUEST,
) from e
def _parse_form_field(field: str, value: object) -> object:
if not isinstance(value, str):
return value
if field == "metadata":
return _parse_form_metadata(value)
if field in _FORM_FALLBACK_FIELDS:
return _parse_form_fallbacks(field, value)
return value
def _reraise_request_size_error(error: Exception) -> None:
if isinstance(error, HTTPException) and error.status_code == status.HTTP_413_CONTENT_TOO_LARGE:
raise error
@ -292,12 +318,7 @@ async def read_request_body(request: Request | None) -> dict:
parsed_body = _parse_binary_body(binary_body)
elif _is_form_content_type(content_type):
form_body: Final = dict(await _read_form_data(request))
raw_metadata: Final = form_body.get("metadata")
parsed_body = (
{**form_body, "metadata": _parse_form_metadata(raw_metadata)}
if isinstance(raw_metadata, str)
else form_body
)
parsed_body = {field: _parse_form_field(field, value) for field, value in form_body.items()}
else:
# Read the request body
body: Final = await request.body()

View file

@ -423,6 +423,37 @@ async def test_form_data_with_invalid_json_metadata() -> None:
assert exc_info.value.param == "metadata"
def _form_request(fields: Mapping[str, str]) -> MagicMock:
request: Final = MagicMock()
request.form = AsyncMock(return_value=FormData(fields))
request.headers = {"content-type": "multipart/form-data"}
request.scope = {}
request.state._cached_headers = None
return request
@pytest.mark.asyncio
@pytest.mark.parametrize(
("field", "raw", "expected"),
[
("fallbacks", '["backup"]', ["backup"]),
("context_window_fallbacks", '[{"primary": ["backup"]}]', [{"primary": ["backup"]}]),
("content_policy_fallbacks", '[{"primary": ["backup"]}]', [{"primary": ["backup"]}]),
],
)
async def test_form_data_fallback_fields_are_decoded_to_lists(field: str, raw: str, expected: list[object]) -> None:
result: Final = await read_request_body(_form_request({"model": "primary", field: raw}))
assert result == {"model": "primary", field: expected}, result
@pytest.mark.asyncio
@pytest.mark.parametrize("raw", ['"backup"', "backup", '{"primary": ["backup"]}'])
async def test_form_data_fallbacks_that_are_not_a_json_array_are_rejected(raw: str) -> None:
with pytest.raises(ProxyException) as exc_info:
await read_request_body(_form_request({"model": "primary", "fallbacks": raw}))
assert (exc_info.value.code, exc_info.value.param) == ("400", "fallbacks"), exc_info.value.message
@pytest.mark.asyncio
async def test_form_data_without_metadata():
"""