diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 58e8866ce99..ae08e768006 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -637,13 +637,16 @@ def proxy_exception_from_http_exception(exc: HTTPException, headers: dict[str, s existing_fields: Final = getattr(exc, "provider_specific_fields", None) or {} merged_fields: Final = {**existing_fields, **structured_fields} if structured_fields else (existing_fields or None) error_status: Final = error_status_code(exc, status.HTTP_400_BAD_REQUEST) + # Preserve headers carried on the HTTPException itself (e.g. `retry-after` + # set by the rate limiter on 429); caller-supplied headers win on conflict. + merged_headers: Final = {**(exc.headers or {}), **headers} return ProxyException( message=message, type=openai_error_type(exc, error_status), param=openai_error_param(exc), code=error_status, provider_specific_fields=merged_fields, - headers=headers, + headers=merged_headers, ) diff --git a/tests/unit/proxy/test_common_request_processing.py b/tests/unit/proxy/test_common_request_processing.py index 09ed6399d68..137a6d144bb 100644 --- a/tests/unit/proxy/test_common_request_processing.py +++ b/tests/unit/proxy/test_common_request_processing.py @@ -2355,6 +2355,22 @@ class TestCommonRequestProcessingHelpers: assert plain.code == "429" assert plain.provider_specific_fields is None + async def test_proxy_exception_from_http_exception_preserves_exc_headers(self): + """Headers carried on the HTTPException itself (e.g. `retry-after` set by + the rate limiter on 429) must survive the conversion to ProxyException; + caller-supplied headers win on conflict.""" + from litellm.proxy.common_request_processing import ( + proxy_exception_from_http_exception, + ) + + exc = HTTPException(status_code=429, detail="slow down", headers={"retry-after": "60"}) + result = proxy_exception_from_http_exception(exc, {"x-litellm-call-id": "abc"}) + assert result.headers == {"retry-after": "60", "x-litellm-call-id": "abc"} + + conflict = HTTPException(status_code=429, detail="slow down", headers={"retry-after": "60"}) + overridden = proxy_exception_from_http_exception(conflict, {"retry-after": "30"}) + assert overridden.headers == {"retry-after": "30"} + async def test_proxy_exception_from_route_error_helper(self) -> None: """The shared route error -> ProxyException conversion keeps the status an exception carries, never the OpenAI SDK's ``code`` field (``None`` on most litellm exceptions)."""