Skip to content

feat: flush MITM responses per-chunk for live streaming (git clone, SSE)#229

Merged
saifsmailbox98 merged 2 commits into
mainfrom
saif/age2-5-flush-mitm-responses-per-chunk-for-live-streaming-git-clone
Jun 2, 2026
Merged

feat: flush MITM responses per-chunk for live streaming (git clone, SSE)#229
saifsmailbox98 merged 2 commits into
mainfrom
saif/age2-5-flush-mitm-responses-per-chunk-for-live-streaming-git-clone

Conversation

@saifsmailbox98

Copy link
Copy Markdown
Collaborator

Summary

Streaming responses (git clone pack data, SSE event streams) were buffered by Go's ResponseWriter until the internal 4 KB buffer filled or the handler returned. Upstream chunk boundaries collapsed into a single blob — git's sideband parser stalled, SSE events arrived late, and any chunk-boundary-sensitive protocol broke. On top of that, the CONNECT tunnel's WriteTimeout: 5 * time.Minute killed any transfer that took longer than 5 minutes to stream.

Concrete failure: git clone of microsoft/TypeScript through the proxy died with fatal: early EOF / fetch-pack: unexpected disconnect while reading sideband packet. Same root cause as external draft PR #197.

Per-chunk flushing

Wraps the ResponseWriter in a flushingWriter that calls http.Flusher.Flush() after every successful Write. io.Copy uses a 32 KB buffer, so this flushes every ≤32 KB — upstream chunk boundaries reach the client immediately instead of coalescing. Applied to both the HTTP forward path (forward.go) and the WebSocket non-101 fallback path (websocket.go).

For non-streaming responses (single JSON body with Content-Length), this is effectively a no-op — io.Copy does one Write and one Flush, same as before.

CONNECT tunnel WriteTimeout raised to 30 minutes

The previous 5-minute WriteTimeout on the CONNECT tunnel's http.Server was an absolute deadline from request-header-read to response-complete. Any streaming response >5 minutes was killed. Raised to 30 minutes to cover realistic long-running transfers. Slow-loris defense is preserved by ReadHeaderTimeout (10s) and ReadTimeout (60s) on the request side, IdleTimeout (2 min) between requests, and the upstream transport's ResponseHeaderTimeout (5 min) for stalled upstreams.

WebSocket oversized-frame substitution warning

Added slog.Warn when a WebSocket text frame exceeds the 1 MB substitution buffer (maxWSSubstitutionPayload) and substitutions are configured. The existing fragmentation case already warned; the size case silently passed through.

Supersedes #197.

Type of change

  • Bug fix
  • New feature
  • Refactor / cleanup
  • Documentation
  • CI / build

Test plan

  • Existing tests pass (go test ./... — all 20 packages green)
  • Added/updated tests for new behavior:
Test Validates
TestMITMForwardStreamsChunksPromptly Upstream sends 5 chunks with 80ms delays; asserts inter-chunk arrival gaps ≥40ms at the client (proves per-chunk flushing, not batch delivery)
  • Manual testing:
Scenario Result
Full git clone of microsoft/TypeScript through proxy Completed: 3.4 GB, 81K files, ~6 min. Previously failed at 100 MB (body cap, fixed in #227) and at 5 min (WriteTimeout, fixed here).
SSE drip test (httpbin.org/drip?duration=5&numbytes=5) through proxy 5 bytes arrived 1/sec — same cadence as direct, no buffering.

Security checklist

  • No secrets or credentials in code
  • No new unauthenticated endpoints
  • Input validation on new API surfaces
  • Checked for OWASP top 10 (injection, XSS, etc.)

…0 min

Streaming responses (git clone pack data, SSE event streams) were buffered
by Go's ResponseWriter until the internal 4 KB buffer filled or the handler
returned. Combined with a 5-minute WriteTimeout on the CONNECT tunnel, large
git clones failed: data arrived late, and transfers >5 min were killed.
@linear

linear Bot commented Jun 2, 2026

Copy link
Copy Markdown

AGE2-5

@infisical-review-police

Copy link
Copy Markdown

💬 Discussion in Slack: #pr-review-agent-vault-229-feat-flush-mitm-responses-per-chunk-for-live-streaming

Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel.

@greptile-apps

greptile-apps Bot commented Jun 2, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes silent buffering of streaming responses (git clone pack data, SSE) through the MITM proxy by wrapping ResponseWriter in a flushingWriter that calls http.Flusher.Flush() after every Write, and raises the CONNECT tunnel's WriteTimeout from 5 to 30 minutes so long-running transfers are not killed mid-stream.

  • flushingWriter is added in forward.go and applied to both the HTTP forward path and the WebSocket non-101 fallback in websocket.go; it gracefully degrades to plain io.Copy when the writer doesn't implement http.Flusher.
  • connect.go raises WriteTimeout to 30 minutes with a comment detailing the compensating slow-loris guards (ReadHeaderTimeout, ReadTimeout, IdleTimeout, upstream ResponseHeaderTimeout).
  • A missing slog.Warn is added in websocket.go for WebSocket text frames that exceed the 1 MB substitution payload limit, matching the existing warning for the fragmentation case.

Confidence Score: 5/5

Safe to merge; the streaming fix is narrowly scoped, correctly implemented, and does not affect non-streaming responses.

All changes are well-contained: flushingWriter correctly delegates both Write and Flush to the same underlying ResponseWriter, the io.Copy integration is sound, the timeout increase is paired with an explanatory comment and relies on pre-existing compensating guards, and the new WebSocket warning fills a genuine logging gap. No logic or correctness issues were found.

No files require special attention.

Important Files Changed

Filename Overview
internal/mitm/forward.go Adds flushingWriter that calls http.Flusher.Flush() after every successful Write, wired into the response copy path; implementation is correct and the non-streaming no-op property holds.
internal/mitm/connect.go WriteTimeout raised from 5 to 30 minutes for the inner CONNECT-tunnel server, with a clear comment explaining the compensating guards (ReadHeaderTimeout, ReadTimeout, IdleTimeout, upstream ResponseHeaderTimeout).
internal/mitm/websocket.go Non-101 fallback path now uses flushingWriter for consistent streaming; adds a missing slog.Warn for WebSocket frames that exceed the substitution size limit.
internal/mitm/proxy_test.go Adds TestMITMForwardStreamsChunksPromptly that sets up a chunked upstream with inter-chunk delays and asserts at least one arrival gap ≥40ms; accurately distinguishes buffered vs. flushed delivery.

Reviews (2): Last reviewed commit: "docs: add comment explaining WriteTimeou..." | Re-trigger Greptile

Comment thread internal/mitm/proxy_test.go
Comment thread internal/mitm/connect.go
@saifsmailbox98

Copy link
Copy Markdown
Collaborator Author

@greptile review

@saifsmailbox98 saifsmailbox98 merged commit e1f6f18 into main Jun 2, 2026
11 checks passed
@saifsmailbox98 saifsmailbox98 deleted the saif/age2-5-flush-mitm-responses-per-chunk-for-live-streaming-git-clone branch June 2, 2026 06:20
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.

1 participant