diff --git a/litellm/proxy/common_utils/http_parsing_utils.py b/litellm/proxy/common_utils/http_parsing_utils.py index 07bd037fe6..ce8f1661c1 100644 --- a/litellm/proxy/common_utils/http_parsing_utils.py +++ b/litellm/proxy/common_utils/http_parsing_utils.py @@ -62,8 +62,8 @@ async def _read_request_body(request: Optional[Request]) -> Dict: def _safe_get_request_parsed_body(request: Optional[Request]) -> Optional[dict]: if request is None: return None - if hasattr(request, "state") and hasattr(request.state, "parsed_body"): - return request.state.parsed_body + if hasattr(request, "scope") and "parsed_body" in request.scope: + return request.scope["parsed_body"] return None @@ -74,7 +74,7 @@ def _safe_set_request_parsed_body( try: if request is None: return - request.state.parsed_body = parsed_body + request.scope["parsed_body"] = parsed_body except Exception as e: verbose_proxy_logger.debug( "Unexpected error setting request parsed body - {}".format(e) diff --git a/tests/litellm/proxy/common_utils/test_http_parsing_utils.py b/tests/litellm/proxy/common_utils/test_http_parsing_utils.py new file mode 100644 index 0000000000..4d9199ff48 --- /dev/null +++ b/tests/litellm/proxy/common_utils/test_http_parsing_utils.py @@ -0,0 +1,107 @@ +import json +import os +import sys +from unittest.mock import AsyncMock, MagicMock, patch + +import orjson +import pytest +from fastapi import Request +from fastapi.testclient import TestClient + +sys.path.insert( + 0, os.path.abspath("../../../..") +) # Adds the parent directory to the system path + + +import litellm +from litellm.proxy.common_utils.http_parsing_utils import ( + _read_request_body, + _safe_get_request_parsed_body, + _safe_set_request_parsed_body, +) + + +@pytest.mark.asyncio +async def test_request_body_caching(): + """ + Test that the request body is cached after the first read and subsequent + calls use the cached version instead of parsing again. + """ + # Create a mock request with a JSON body + mock_request = MagicMock() + test_data = {"key": "value"} + # Use AsyncMock for the body method + mock_request.body = AsyncMock(return_value=orjson.dumps(test_data)) + mock_request.headers = {"content-type": "application/json"} + mock_request.scope = {} + + # First call should parse the body + result1 = await _read_request_body(mock_request) + assert result1 == test_data + assert "parsed_body" in mock_request.scope + assert mock_request.scope["parsed_body"] == test_data + + # Verify the body was read once + mock_request.body.assert_called_once() + + # Reset the mock to track the second call + mock_request.body.reset_mock() + + # Second call should use the cached body + result2 = await _read_request_body(mock_request) + assert result2 == test_data + + # Verify the body was not read again + mock_request.body.assert_not_called() + + +@pytest.mark.asyncio +async def test_form_data_parsing(): + """ + Test that form data is correctly parsed from the request. + """ + # Create a mock request with form data + mock_request = MagicMock() + test_data = {"name": "test_user", "message": "hello world"} + + # Mock the form method to return the test data as an awaitable + mock_request.form = AsyncMock(return_value=test_data) + mock_request.headers = {"content-type": "application/x-www-form-urlencoded"} + mock_request.scope = {} + + # Parse the form data + result = await _read_request_body(mock_request) + + # Verify the form data was correctly parsed + assert result == test_data + assert "parsed_body" in mock_request.scope + assert mock_request.scope["parsed_body"] == test_data + + # Verify form() was called + mock_request.form.assert_called_once() + + # The body method should not be called for form data + assert not hasattr(mock_request, "body") or not mock_request.body.called + + +@pytest.mark.asyncio +async def test_empty_request_body(): + """ + Test handling of empty request bodies. + """ + # Create a mock request with an empty body + mock_request = MagicMock() + mock_request.body = AsyncMock(return_value=b"") # Empty bytes as an awaitable + mock_request.headers = {"content-type": "application/json"} + mock_request.scope = {} + + # Parse the empty body + result = await _read_request_body(mock_request) + + # Verify an empty dict is returned + assert result == {} + assert "parsed_body" in mock_request.scope + assert mock_request.scope["parsed_body"] == {} + + # Verify the body was read + mock_request.body.assert_called_once() diff --git a/tests/local_testing/test_http_parsing_utils.py b/tests/local_testing/test_http_parsing_utils.py index 2c6956c793..4d509fc16d 100644 --- a/tests/local_testing/test_http_parsing_utils.py +++ b/tests/local_testing/test_http_parsing_utils.py @@ -8,7 +8,7 @@ import sys sys.path.insert( 0, os.path.abspath("../..") -) # Adds the parent directory to the system path +) # Adds the parent directory to the system-path from litellm.proxy.common_utils.http_parsing_utils import _read_request_body