Skip to content

Fix reverse-proxy SSE/streaming fidelity (event: lines, gzip, header propagation, Host)#657

Merged
pdettori merged 4 commits into
rossoctl:mainfrom
pdettori:fix/authbridge-reverse-proxy-sse-fidelity
Jul 13, 2026
Merged

Fix reverse-proxy SSE/streaming fidelity (event: lines, gzip, header propagation, Host)#657
pdettori merged 4 commits into
rossoctl:mainfrom
pdettori:fix/authbridge-reverse-proxy-sse-fidelity

Conversation

@pdettori

@pdettori pdettori commented Jul 11, 2026

Copy link
Copy Markdown
Member

Fixes #656.

Three reverse-proxy fidelity fixes so authbridge-proxy faithfully proxies streaming (SSE) responses — required for spec-compliant SSE clients (e.g. the Anthropic SDK) to work through the reverse proxy when a response-side StreamingResponder plugin (e.g. inference-parser) is active.

Commits

  1. rewrite outbound Host header in the reverse-proxy Director — wrap the default Director to set req.Host = target.Host, so the backend receives its own host. Cloudflare-fronted backends (api.anthropic.com) reject a mismatched Host.
  2. forward all inbound header mutations to the backend — propagate the full inbound header diff (set/replace/delete), not just Authorization, so a header a plugin sets on the inbound pipeline (e.g. x-api-key) actually reaches the backend. Content-Length/Content-Encoding stay managed by the body-rewrite path.
  3. preserve the SSE event: line and strip Accept-Encodingsseframe.Reader now captures the event: field (LastEvent()) and the streaming re-framer emits event: <type> before the data: lines; the Director strips the client's Accept-Encoding so Go's transport transparently decompresses upstream gzip (no Content-Encoding: gzip over re-emitted plaintext). Without these, SSE clients decode zero events.

Testing

go test ./authlib/listener/reverseproxy/... ./authlib/listener/internal/sseframe/... passes, including new tests: sseframe event capture; reverse-proxy event:-preservation, Accept-Encoding strip (verified via the Director's output), full inbound-header propagation, and Host rewrite. Validated end-to-end in the RC1 AuthBridge egress PoC (harness → AuthBridge → api.anthropic.com streaming agent leaf, 13/13 live on Kind).

Assisted-By: Claude Code

Summary by CodeRabbit

  • New Features
    • Streaming SSE responses now preserve and relay upstream event: types alongside data: lines.
  • Bug Fixes
    • Reverse proxy now rewrites Host to match the selected backend.
    • Request headers mutated during processing (including additions and deletions) are now consistently reflected downstream.
    • SSE streaming avoids compression-framing inconsistencies by stripping Accept-Encoding when response-body inspection is needed.
  • Tests
    • Added regression coverage for event: preservation, Host rewriting, header mutation behavior, and Accept-Encoding handling in streaming paths.

pdettori added 3 commits July 11, 2026 14:14
The default httputil.SingleHostReverseProxy Director rewrites the
outbound URL scheme/host/path but deliberately leaves req.Host as the
inbound caller's Host (e.g. authbridge-ab1:8080). Cloudflare-fronted
backends like api.anthropic.com validate Host against the request
line and reject the mismatch, breaking the real-LLM allow-path.

Wrap the existing Director to set req.Host to the backend target's
host after the default rewrite runs.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
The reverse proxy only propagated an Authorization mutation from the
inbound pipeline's pctx.Headers back onto the forwarded request. A
plugin like static-inject that sets x-api-key and deletes Authorization
had the x-api-key addition silently dropped and the deleted Authorization
still forwarded, so api.anthropic.com saw no valid credential (401).

Replace the Authorization-only propagation with a full header-diff sync:
delete headers present on r.Header but absent from pctx.Headers, then
copy every remaining pctx.Headers entry onto r.Header. Content-Length /
Content-Encoding are left untouched since they're managed by the
body-rewrite block and transport.

Adds tests for: x-api-key set + Authorization deleted reaching the
backend, and an untouched header still passing through unchanged.
Confirmed the existing Authorization-mint regression test
(TestInboundPropagation_RewrittenAuthReachesBackend) still passes.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
…oxy streaming

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 130627fb-aa8a-45cd-9da4-5b3202052249

📥 Commits

Reviewing files that changed from the base of the PR and between 3c86aac and fe2ad3b.

📒 Files selected for processing (2)
  • authbridge/authlib/listener/reverseproxy/server.go
  • authbridge/authlib/listener/reverseproxy/streaming_test.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • authbridge/authlib/listener/reverseproxy/streaming_test.go
  • authbridge/authlib/listener/reverseproxy/server.go

📝 Walkthrough

Walkthrough

The reverse proxy now captures SSE event: fields, preserves them during response re-framing, conditionally strips compression requests, rewrites the backend host, and propagates complete inbound pipeline header mutations. Tests cover each behavior.

Changes

SSE reverse proxy behavior

Layer / File(s) Summary
SSE event capture contract
authbridge/authlib/listener/internal/sseframe/reader.go, authbridge/authlib/listener/internal/sseframe/reader_test.go
Reader captures the current frame’s event: value, clears it per frame, exposes LastEvent(), and tests capture, reset, and empty behavior.
Outbound request normalization and mutation propagation
authbridge/authlib/listener/reverseproxy/server.go, authbridge/authlib/listener/reverseproxy/server_test.go, authbridge/authlib/listener/reverseproxy/streaming_test.go
The proxy rewrites Host, conditionally removes Accept-Encoding, and synchronizes pipeline-added, changed, and deleted headers with forwarded requests.
Streaming SSE re-framing
authbridge/authlib/listener/reverseproxy/server.go, authbridge/authlib/listener/reverseproxy/streaming_test.go
Re-framed responses emit upstream event: lines before corresponding data: lines, with regression coverage for event preservation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ReverseProxy
  participant InboundPipeline
  participant Backend
  participant streamingResponseBody
  Client->>ReverseProxy: Send streaming request
  ReverseProxy->>InboundPipeline: Run inbound pipeline
  InboundPipeline-->>ReverseProxy: Return mutated headers
  ReverseProxy->>Backend: Forward normalized request
  Backend-->>streamingResponseBody: Return SSE event and data lines
  streamingResponseBody-->>Client: Re-frame event and data lines
Loading

Possibly related PRs

Suggested reviewers: cwiklik

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main reverse-proxy SSE/streaming fixes, including event lines, gzip handling, header propagation, and Host rewriting.
Linked Issues check ✅ Passed The changes implement the linked issue’s required fixes and tests for SSE event preservation, Accept-Encoding handling, full header propagation, and Host rewriting.
Out of Scope Changes check ✅ Passed The added tests and scoped pass-through Accept-Encoding behavior stay within the reported reverse-proxy streaming fidelity work.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@huang195 huang195 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Perfetto — three tight, correct fixes, each with a test that locks in the exact bug.

  1. Host rewrite — the Director wrap sets req.Host = target.Host after calling the original director. Correct for Cloudflare/SNI-fronted backends (api.anthropic.com) that validate Host against the request line.
  2. Full inbound-header diff propagation — the delete-then-set loop reconciles pctx.Headers (the canonical clone) against r.Header, so a plugin-set x-api-key and a plugin-deleted Authorization both reach the backend correctly. This is exactly what unblocks the #655 static-inject plugin — nice tie-in.
  3. SSE event: preservationsseframe.Reader now captures the event: field (reset per frame, last-event-wins per the SSE spec, copied out of the reused buffer) and the re-framer emits event: <type> before the data: lines. Together with the Accept-Encoding strip (so Go's transport transparently decompresses and the re-framer sees plaintext), this fixes SSE clients (Anthropic SDK) decoding zero typed events.

Tests cover the tricky parts: header set + delete, Host rewrite, event-line preservation, event-reset-per-frame, and the Accept-Encoding strip. One low, optional efficiency note inline (the strip is unconditional); nothing blocking. Bravo. 🇮🇹

Maintainer author, green CI, DCO signed.

Assisted-By: Claude Code

Comment thread authbridge/authlib/listener/reverseproxy/server.go Outdated
…elines

The Director stripped the client's Accept-Encoding on every request, but
plaintext is only required when a plugin actually inspects the response
body — a StreamingResponder (SSE re-framing) or any ReadsBody/WritesBody
plugin (buffered read into pctx.ResponseBody). For a pure pass-through
proxy the strip forced needless upstream->client decompression, a small
efficiency regression on a remote backend or large non-streamed bodies.

Gate the strip on inbound.NeedsBody() || inbound.HasStreamingResponders()
so pass-through responses keep the caller's Accept-Encoding intact.
Addresses review feedback on rossoctl#657.

Update TestReverseProxy_StripsAcceptEncoding to carry a body-inspecting
StreamingResponder so the strip stays in scope, and add
TestReverseProxy_PreservesAcceptEncoding_PassThrough asserting an empty
pipeline preserves the header.

Assisted-By: Claude (Anthropic AI) <noreply@anthropic.com>
Signed-off-by: Paolo Dettori <dettori@us.ibm.com>
@pdettori
pdettori merged commit d17993f into rossoctl:main Jul 13, 2026
20 checks passed
@github-project-automation github-project-automation Bot moved this from New/ToDo to Done in Rossoctl Issue Prioritization Jul 13, 2026
@pdettori
pdettori deleted the fix/authbridge-reverse-proxy-sse-fidelity branch July 13, 2026 15:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

AuthBridge reverse proxy corrupts streamed SSE responses (drops event: lines, mishandles gzip) — breaks SSE clients

3 participants