From 367f00315f41eb4d3b6744ad96803a29e5447530 Mon Sep 17 00:00:00 2001 From: mateo Date: Fri, 14 Aug 2026 21:11:29 +0000 Subject: [PATCH] fix(passthrough): stop forwarding client Accept-Encoding upstream Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/passthrough/utils.py | 31 ++++++---- .../passthrough/test_passthrough_utils.py | 57 +++++++++++++++++++ 2 files changed, 78 insertions(+), 10 deletions(-) create mode 100644 tests/test_litellm/passthrough/test_passthrough_utils.py diff --git a/litellm/passthrough/utils.py b/litellm/passthrough/utils.py index e419322dca6..8ce58c423e9 100644 --- a/litellm/passthrough/utils.py +++ b/litellm/passthrough/utils.py @@ -18,12 +18,26 @@ _PASS_THROUGH_PROTECTED_HEADERS: Final[frozenset] = frozenset( "x-goog-api-key", "host", "content-length", + "accept-encoding", } ) # Header name prefix used to block AWS SigV4 signing headers from being overridden. _PASS_THROUGH_PROTECTED_HEADER_PREFIXES: Final[tuple] = ("x-amz-",) +# Client headers that must never reach the upstream provider. `accept-encoding` is +# dropped so httpx negotiates a content coding it actually has a decoder for: an +# encoding it cannot decode (e.g. brotli, absent the optional `brotli` package) +# leaves the body compressed while get_response_headers strips Content-Encoding, +# handing the client bytes it has no way to read. +_NON_FORWARDED_REQUEST_HEADERS: Final[frozenset[str]] = frozenset( + { + "content-length", + "host", + "accept-encoding", + } +) + class BasePassthroughUtils: @staticmethod @@ -66,17 +80,14 @@ class BasePassthroughUtils: e.g., 'x-pass-anthropic-beta: value' becomes 'anthropic-beta: value' """ if forward_headers is True: - # Header We Should NOT forward - request_headers.pop("content-length", None) - request_headers.pop("host", None) - custom_header_names: Final = {header_name.lower() for header_name in headers} - for header_name in list(request_headers.keys()): - if header_name.lower() in custom_header_names: - request_headers.pop(header_name, None) - - # Combine request headers with custom headers - headers = {**request_headers, **headers} + forwardable_headers: Final = { + header_name: header_value + for header_name, header_value in request_headers.items() + if header_name.lower() not in _NON_FORWARDED_REQUEST_HEADERS + and header_name.lower() not in custom_header_names + } + headers = {**forwardable_headers, **headers} # Process x-pass- prefixed headers (strip prefix and forward) # Credential and protocol-level headers are excluded from this mechanism. diff --git a/tests/test_litellm/passthrough/test_passthrough_utils.py b/tests/test_litellm/passthrough/test_passthrough_utils.py new file mode 100644 index 00000000000..ad01c9d42da --- /dev/null +++ b/tests/test_litellm/passthrough/test_passthrough_utils.py @@ -0,0 +1,57 @@ +import httpx +from httpx._decoders import SUPPORTED_DECODERS + +from litellm.passthrough.utils import BasePassthroughUtils + +ANTHROPIC_MESSAGES_URL = "https://api.anthropic.com/v1/messages" + + +def _build_upstream_request(forwarded_headers: dict) -> httpx.Request: + with httpx.Client(headers={"user-agent": "litellm/test"}) as client: + return client.build_request("POST", ANTHROPIC_MESSAGES_URL, headers=forwarded_headers) + + +def test_client_accept_encoding_is_not_forwarded_upstream(): + headers = BasePassthroughUtils.forward_headers_from_request( + request_headers={ + "accept-encoding": "br", + "anthropic-version": "2023-06-01", + "host": "localhost:4000", + "content-length": "123", + }, + headers={"x-api-key": "sk-anthropic"}, + forward_headers=True, + ) + + assert "accept-encoding" not in {name.lower() for name in headers} + assert headers["anthropic-version"] == "2023-06-01" + assert "host" not in headers + assert "content-length" not in headers + + +def test_client_accept_encoding_is_not_forwarded_via_x_pass_prefix(): + headers = BasePassthroughUtils.forward_headers_from_request( + request_headers={"x-pass-accept-encoding": "br"}, + headers={}, + forward_headers=False, + ) + + assert "accept-encoding" not in headers + + +def test_upstream_request_only_advertises_decodable_encodings(): + """A content coding httpx cannot decode would reach the client still compressed, + with Content-Encoding stripped by get_response_headers (LIT-5613).""" + forwarded_headers = BasePassthroughUtils.forward_headers_from_request( + request_headers={"accept-encoding": "br, zstd, exotic"}, + headers={"x-api-key": "sk-anthropic"}, + forward_headers=True, + ) + + advertised = { + value.strip().lower() + for value in _build_upstream_request(forwarded_headers).headers["accept-encoding"].split(",") + } + + assert advertised + assert advertised <= set(SUPPORTED_DECODERS)