Summary
When the authbridge-proxy reverse proxy re-frames a streaming (text/event-stream) response — which it does whenever a response-side StreamingResponder plugin (e.g. inference-parser) is active — it corrupts the stream in ways that break spec-compliant SSE clients such as the Anthropic SDK. A client reading the proxied stream decodes zero typed events, so an agent loop driving tools/structured-output over the proxied LLM never sees any content and stalls.
Root causes
-
SSE event: lines are dropped. authlib/listener/internal/sseframe surfaces only the data: payload (it skips event:/id:/retry: by design), and the streaming re-framer in authlib/listener/reverseproxy/server.go re-emits only data: <payload>. Anthropic's SSE (and the Anthropic SDK's parser) type each event from the event: line; without it the parser yields nothing.
- Observed (raw bytes): upstream
event: message_start\ndata: {…} → proxied data: {…} (no event:).
-
Content-Encoding: gzip returned over re-framed plaintext. The reverse proxy forwards the client's Accept-Encoding: gzip to the backend, receives a gzipped body, but the re-framer emits plaintext while the Content-Encoding: gzip response header is preserved. A client that requested gzip (the Anthropic SDK does; wget does not — which masks the bug) tries to gunzip plaintext and gets an empty/garbage stream.
-
Non-Authorization inbound header mutations are dropped, and the backend Host is not rewritten (found in the same investigation). The reverse proxy only propagated an Authorization change from the inbound pipeline to the forwarded request, silently dropping any other header a plugin set (e.g. x-api-key), and used httputil.NewSingleHostReverseProxy with no Director, so the backend received the inbound Host rather than the target host — which Cloudflare-fronted backends (api.anthropic.com) reject.
Impact
Any SSE/streaming client behind the reverse proxy with a StreamingResponder plugin active — notably LLM agent loops proxying Anthropic — receives an empty event stream and cannot function. This blocked the RC1 AuthBridge egress PoC (harness→AB1→api.anthropic.com); the fixes are validated end-to-end there.
Fix
See the linked PR:
sseframe.Reader captures the event: field (LastEvent()); the re-framer emits event: <type> before data:.
- The Director strips the client's
Accept-Encoding so Go's transport transparently decompresses (no Content-Encoding over plaintext).
- The reverse proxy forwards the full inbound header diff to the backend, and the Director rewrites
Host to the backend target.
Includes Go unit tests for each (sseframe event capture; reverse-proxy event: preservation, Accept-Encoding strip, full-header propagation, Host rewrite).
Summary
When the
authbridge-proxyreverse proxy re-frames a streaming (text/event-stream) response — which it does whenever a response-side StreamingResponder plugin (e.g.inference-parser) is active — it corrupts the stream in ways that break spec-compliant SSE clients such as the Anthropic SDK. A client reading the proxied stream decodes zero typed events, so an agent loop driving tools/structured-output over the proxied LLM never sees any content and stalls.Root causes
SSE
event:lines are dropped.authlib/listener/internal/sseframesurfaces only thedata:payload (it skipsevent:/id:/retry:by design), and the streaming re-framer inauthlib/listener/reverseproxy/server.gore-emits onlydata: <payload>. Anthropic's SSE (and the Anthropic SDK's parser) type each event from theevent:line; without it the parser yields nothing.event: message_start\ndata: {…}→ proxieddata: {…}(noevent:).Content-Encoding: gzipreturned over re-framed plaintext. The reverse proxy forwards the client'sAccept-Encoding: gzipto the backend, receives a gzipped body, but the re-framer emits plaintext while theContent-Encoding: gzipresponse header is preserved. A client that requested gzip (the Anthropic SDK does;wgetdoes not — which masks the bug) tries to gunzip plaintext and gets an empty/garbage stream.Non-
Authorizationinbound header mutations are dropped, and the backendHostis not rewritten (found in the same investigation). The reverse proxy only propagated anAuthorizationchange from the inbound pipeline to the forwarded request, silently dropping any other header a plugin set (e.g.x-api-key), and usedhttputil.NewSingleHostReverseProxywith noDirector, so the backend received the inboundHostrather than the target host — which Cloudflare-fronted backends (api.anthropic.com) reject.Impact
Any SSE/streaming client behind the reverse proxy with a StreamingResponder plugin active — notably LLM agent loops proxying Anthropic — receives an empty event stream and cannot function. This blocked the RC1 AuthBridge egress PoC (harness→AB1→api.anthropic.com); the fixes are validated end-to-end there.
Fix
See the linked PR:
sseframe.Readercaptures theevent:field (LastEvent()); the re-framer emitsevent: <type>beforedata:.Accept-Encodingso Go's transport transparently decompresses (noContent-Encodingover plaintext).Hostto the backend target.Includes Go unit tests for each (sseframe event capture; reverse-proxy
event:preservation,Accept-Encodingstrip, full-header propagation, Host rewrite).