mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-11 03:38:38 +00:00
Merge 15225f691d into 6532dcb73b
This commit is contained in:
commit
5a1670425a
2 changed files with 61 additions and 0 deletions
|
|
@ -26,6 +26,18 @@ _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"})
|
||||
_OBJECT_ONLY_JSON_ROUTES: Final[frozenset[str]] = frozenset(
|
||||
{
|
||||
"/v1/chat/completions",
|
||||
"/chat/completions",
|
||||
"/engines/{model:path}/chat/completions",
|
||||
"/openai/deployments/{model:path}/chat/completions",
|
||||
"/v1/responses",
|
||||
"/responses",
|
||||
"/openai/v1/responses",
|
||||
"/v1/messages",
|
||||
}
|
||||
)
|
||||
|
||||
_ANNOTATION_QUALIFIERS: Final[frozenset[object]] = frozenset({Annotated, NotRequired, ReadOnly, Required})
|
||||
|
||||
|
|
@ -284,6 +296,16 @@ async def _read_request_body(request: Request | None) -> dict:
|
|||
code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
matched_route: Final = request.scope.get("route")
|
||||
route_path: Final = getattr(matched_route, "path", None) or request.scope.get("path", "")
|
||||
if route_path in _OBJECT_ONLY_JSON_ROUTES and not isinstance(parsed_body, dict):
|
||||
raise ProxyException(
|
||||
message="Invalid JSON payload: request body must be a JSON object",
|
||||
type="invalid_request_error",
|
||||
param="request_body",
|
||||
code=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
# Cache the parsed result
|
||||
_safe_set_request_parsed_body(request=request, parsed_body=parsed_body)
|
||||
return parsed_body
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import gzip
|
|||
import io
|
||||
import json
|
||||
from collections.abc import Mapping
|
||||
from types import SimpleNamespace
|
||||
from typing import Final, Literal, get_type_hints
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
|
|
@ -108,6 +109,44 @@ async def test_read_raw_json_body_returns_the_bytes_the_parsed_body_came_from():
|
|||
assert await read_raw_json_body(request) == body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("path", "route_template"),
|
||||
[
|
||||
("/v1/chat/completions", None),
|
||||
("/chat/completions", None),
|
||||
("/engines/model/chat/completions", "/engines/{model:path}/chat/completions"),
|
||||
("/openai/deployments/model/chat/completions", "/openai/deployments/{model:path}/chat/completions"),
|
||||
("/v1/responses", None),
|
||||
("/responses", None),
|
||||
("/openai/v1/responses", None),
|
||||
("/v1/messages", None),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize("body", [b"[]", b"null", b'"text"', b"1", b"true"])
|
||||
async def test_non_object_json_body_is_rejected_before_caching(body: bytes, path: str, route_template: str | None):
|
||||
request: Final = _starlette_request(body, "application/json", path=path)
|
||||
if route_template is not None:
|
||||
request.scope["route"] = SimpleNamespace(path=route_template)
|
||||
|
||||
with pytest.raises(ProxyException) as exc_info:
|
||||
await _read_request_body(request)
|
||||
|
||||
assert exc_info.value.code == "400"
|
||||
assert exc_info.value.type == "invalid_request_error"
|
||||
assert "must be a JSON object" in exc_info.value.message
|
||||
assert _safe_get_request_parsed_body(request) is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_passthrough_json_array_body_remains_available():
|
||||
body: Final = b'[{"role":"user","content":"hello"}]'
|
||||
request: Final = _starlette_request(body, "application/json", path="/vertex-ai/v1/rawPredict")
|
||||
|
||||
assert await _read_request_body(request) == json.loads(body)
|
||||
assert await request.body() == body
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_raw_json_body_is_none_until_the_body_has_been_parsed():
|
||||
request = _starlette_request(b'{"model": "claude-sonnet-4-5"}', "application/json")
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue