From 9c7e96050b0dee39f12031e8f5d6c5fe28c61880 Mon Sep 17 00:00:00 2001 From: IntenZe1337 <57249709+IntenZe1337@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:53:53 +0200 Subject: [PATCH] fix: don't forward stale Content-Encoding/Content-Length to the client httpx transparently decompresses the upstream response body, so by the time we build the Response the body is plaintext. Forwarding the upstream headers verbatim via dict(response.headers) keeps Content-Encoding: gzip and the compressed Content-Length, which describe a body that no longer exists. Any client that sends Accept-Encoding: gzip then tries to gunzip plaintext and fails. With curl this is CURLE_BAD_CONTENT_ENCODING (exit 61): $ curl --compressed -sS http://localhost:8082/v1/messages \ -H 'content-type: application/json' \ -H 'x-api-key: ...' -H 'anthropic-version: 2023-06-01' \ -d '{"model":"...","max_tokens":16,"messages":[{"role":"user","content":"hi"}]}' curl: (61) Error while processing content unencoding: invalid stored block lengths Strip the hop-by-hop / transport-describing headers and let the ASGI server compute correct values for the body actually sent. Streaming responses were never affected; this covers the two non-streaming return paths (/v1/messages and /v1/messages/count_tokens). Co-Authored-By: Claude Opus 5 --- proxy.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/proxy.py b/proxy.py index e736b26..0e0cdcb 100755 --- a/proxy.py +++ b/proxy.py @@ -119,6 +119,24 @@ async def lifespan(app: FastAPI): sonnet_semaphore = asyncio.Semaphore(MAX_CONCURRENT_REQUESTS) +# httpx transparently decompresses the upstream response body, so the upstream +# Content-Encoding and Content-Length no longer describe what we forward. Passing +# them through makes the client try to gunzip plaintext (CURLE_BAD_CONTENT_ENCODING). +# Dropping them lets the ASGI server compute correct values for the body we send. +HOP_BY_HOP_HEADERS = frozenset({ + "content-encoding", + "content-length", + "transfer-encoding", + "connection", + "keep-alive", +}) + + +def passthrough_headers(headers) -> dict: + """Upstream headers minus the ones describing the original transport encoding.""" + return {k: v for k, v in headers.items() if k.lower() not in HOP_BY_HOP_HEADERS} + + def get_provider_config(model_name: str): """ Determine which provider to route to based on model name. @@ -285,7 +303,7 @@ async def proxy_messages(request: Request): return Response( content=response.content, status_code=response.status_code, - headers=dict(response.headers) + headers=passthrough_headers(response.headers) ) except httpx.ReadTimeout: @@ -414,7 +432,7 @@ async def proxy_count_tokens(request: Request): return Response( content=response.content, status_code=response.status_code, - headers=dict(response.headers) + headers=passthrough_headers(response.headers) ) except httpx.ReadTimeout: