Skip to content

Flush forwarded event streams so SSE reaches the client as it arrives - #44

Merged
andrew merged 1 commit into
alpha-omega-security:mainfrom
abhinavgautam01:fix/egress-flush-forwarded-responses
Oct 2, 2026
Merged

andrew merged 1 commit into
alpha-omega-security:mainfrom
abhinavgautam01:fix/egress-flush-forwarded-responses

Conversation

@abhinavgautam01

Copy link
Copy Markdown
Contributor

Closes #43

Problem

Proxy.serveForward relays the upstream body with a bare io.Copy(w, resp.Body) and never flushes. On the inspected forward path, a server-sent event stream therefore collects in the server's output buffer (about 4KB) instead of reaching the client as each event arrives. CONNECT tunnels are unaffected because pipe copies raw bytes.

This came up in alpha-omega-security/scrutineer#1103, where model traffic for -model-proxy moves from a CONNECT tunnel to an inspected forward through this proxy. Scrutineer works around it today by wrapping the ResponseWriter itself, but every consumer of the forward path has the same problem.

Fix

serveForward now calls copyResponseBody:

  • Streamed responses (text/event-stream, or a body of unknown length with ContentLength == -1): the headers are flushed first, then the body is flushed after every read.
  • Everything else: bodies with a declared length are still copied in bulk with io.Copy, so ordinary downloads behave exactly as before.

This follows the rule httputil.ReverseProxy uses to decide when to flush immediately. It reuses the existing egressCopyBuf buffer size and adds only the standard library mime import, so there are no new dependencies.

Testing

  • TestEgressProxy_ForwardStreamsEventStream puts a real proxy listener between a client and an SSE upstream. The upstream withholds its second event until the client has read the first, so a buffering proxy times out instead of passing. It fails against the previous io.Copy (5s timeout) and passes with this change.
  • TestStreamingResponse covers which responses are flushed: event streams with and without parameters, unknown-length bodies and known-length bodies.
  • go mod verify, go mod tidy -diff, gofmt, go vet (default and integration tags), golangci-lint run (0 issues), go test -race ./... (egress coverage 90.8%) and govulncheck all pass locally.

Follow-up

Once a harness release includes this, scrutineer can upgrade and drop its flushingResponseWriter workaround from #1103.

@andrew
andrew merged commit b54b388 into alpha-omega-security:main Oct 2, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

egress: flush forwarded responses so SSE streams

2 participants