feat: flush MITM responses per-chunk for live streaming (git clone, SSE)#229
Conversation
…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.
|
💬 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. |
|
| 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
|
@greptile review |
Summary
Streaming responses (git clone pack data, SSE event streams) were buffered by Go's
ResponseWriteruntil 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'sWriteTimeout: 5 * time.Minutekilled any transfer that took longer than 5 minutes to stream.Concrete failure:
git cloneofmicrosoft/TypeScriptthrough the proxy died withfatal: early EOF/fetch-pack: unexpected disconnect while reading sideband packet. Same root cause as external draft PR #197.Per-chunk flushing
Wraps the
ResponseWriterin aflushingWriterthat callshttp.Flusher.Flush()after every successfulWrite.io.Copyuses 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.Copydoes oneWriteand oneFlush, same as before.CONNECT tunnel WriteTimeout raised to 30 minutes
The previous 5-minute
WriteTimeouton the CONNECT tunnel'shttp.Serverwas 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 byReadHeaderTimeout(10s) andReadTimeout(60s) on the request side,IdleTimeout(2 min) between requests, and the upstream transport'sResponseHeaderTimeout(5 min) for stalled upstreams.WebSocket oversized-frame substitution warning
Added
slog.Warnwhen 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
Test plan
go test ./...— all 20 packages green)TestMITMForwardStreamsChunksPromptlygit cloneofmicrosoft/TypeScriptthrough proxyhttpbin.org/drip?duration=5&numbytes=5) through proxySecurity checklist