From 071a7bbad13edb0fc12599be3f6a07b4ecf5f202 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Wed, 18 Feb 2026 09:44:35 -0800 Subject: [PATCH] fix: add .copy() to create_request_copy and tests for header caching Protect cached headers from mutation in create_request_copy sites and add unit tests for _safe_get_request_headers caching behavior. --- .../llm_passthrough_endpoints.py | 2 +- .../vertex_ai_endpoints/langfuse_endpoints.py | 2 +- .../common_utils/test_http_parsing_utils.py | 46 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index 339523603c8..028edea8c3f 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -61,7 +61,7 @@ def create_request_copy(request: Request): return { "method": request.method, "url": str(request.url), - "headers": _safe_get_request_headers(request), + "headers": _safe_get_request_headers(request).copy(), "cookies": request.cookies, "query_params": dict(request.query_params), } diff --git a/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py b/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py index bff770b7ea3..627618387d5 100644 --- a/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py +++ b/litellm/proxy/vertex_ai_endpoints/langfuse_endpoints.py @@ -33,7 +33,7 @@ def create_request_copy(request: Request): return { "method": request.method, "url": str(request.url), - "headers": _safe_get_request_headers(request), + "headers": _safe_get_request_headers(request).copy(), "cookies": request.cookies, "query_params": dict(request.query_params), } 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 af366b082a0..05d2ab5d796 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 @@ -761,3 +761,49 @@ async def test_request_body_with_html_script_tags(): f"Message content with HTML was modified during parsing: " f"expected={msg['content']!r}, got={result['messages'][2]['content']!r}" ) + + +def test_safe_get_request_headers_caches_on_request_state(): + """ + Test that _safe_get_request_headers caches the result on request.state + and returns the same object on subsequent calls. + """ + mock_request = MagicMock() + mock_request.headers = {"content-type": "application/json", "authorization": "Bearer sk-123"} + mock_request.state = MagicMock(spec=[]) # empty spec so getattr returns default + + # First call — should create and cache + result1 = _safe_get_request_headers(mock_request) + assert result1 == {"content-type": "application/json", "authorization": "Bearer sk-123"} + assert mock_request.state._cached_headers is result1 + + # Second call — should return the cached object (same identity) + result2 = _safe_get_request_headers(mock_request) + assert result2 is result1 + + +def test_safe_get_request_headers_none_request(): + """ + Test that _safe_get_request_headers returns empty dict for None request. + """ + result = _safe_get_request_headers(None) + assert result == {} + + +def test_safe_get_request_headers_copy_protects_cache(): + """ + Test that callers using .copy() before mutation do not corrupt the cache. + """ + mock_request = MagicMock() + mock_request.headers = {"authorization": "Bearer sk-123", "host": "localhost"} + mock_request.state = MagicMock(spec=[]) + + original = _safe_get_request_headers(mock_request) + + # Simulate what mutation call sites do: copy then pop + mutable = _safe_get_request_headers(mock_request).copy() + mutable.pop("authorization", None) + + # Cache must be unaffected + assert "authorization" in _safe_get_request_headers(mock_request) + assert _safe_get_request_headers(mock_request) is original