fix(proxy): preserve HTTPException headers (retry-after) when converting to ProxyException

proxy_exception_from_http_exception() replaced the exception's own
headers with only the caller-supplied dict, so a 429 raised by the rate
limiter with retry-after lost that header on routes that convert
HTTPException -> ProxyException (e.g. /v1/messages, unlike
/v1/chat/completions where FastAPI serves HTTPException.headers
directly). Merge exc.headers under the caller-supplied headers.

Fixes #46630
This commit is contained in:
dltsum 2026-10-15 16:47:43 +08:00
parent bf56719f09
commit 5376a5c684
2 changed files with 20 additions and 1 deletions

View file

@ -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,
)

View file

@ -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)."""