diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 1c2bd7ea217..dbc853f8ac3 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -208,6 +208,14 @@ async def _read_request_body(request: Request | None) -> dict: code=status.HTTP_400_BAD_REQUEST, ) + if not isinstance(parsed_body, dict): + raise ProxyException( + message="JSON request body must be an 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 diff --git a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py index 7929a0b21af..d8c7dfd78fd 100644 --- a/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py +++ b/tests/test_litellm/proxy/common_utils/test_http_parsing_utils.py @@ -46,6 +46,19 @@ def _starlette_request(body: bytes, content_type: str) -> Request: return Request(scope, receive) +@pytest.mark.parametrize("body", [b"[]", b"123", b'"str"', b"true", b"null"]) +@pytest.mark.asyncio +async def test_read_request_body_rejects_non_object_json(body: bytes): + request = _starlette_request(body, "application/json") + + with pytest.raises(ProxyException) as error: + await _read_request_body(request) + + assert error.value.code == "400" + assert "JSON request body must be an object" in error.value.message + assert _safe_get_request_parsed_body(request) is None + + @pytest.mark.asyncio async def test_read_raw_json_body_returns_the_bytes_the_parsed_body_came_from(): body = b'{"model": "claude-sonnet-4-5", "messages": [{"role": "user", "content": "hi"}]}'