feat: configurable MITM response/request body size caps#227
Conversation
…mited responses The MITM proxy hardcoded a 100 MB response body cap and 64 MB request body cap that silently truncated large transfers — breaking git clone of any non-trivial repo, git push of large changesets, and bulk file downloads. The truncation was invisible: server logged 200 OK with no warning, and clients saw corrupted data with no hint the proxy was at fault.
|
💬 Discussion in Slack: #pr-review-agent-vault-227-feat-configurable-mitm-response-request-body-size-caps Posted by Review Police — reviews, comments, new commits, and CI failures will stream into this channel. |
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
|
| Filename | Overview |
|---|---|
| internal/mitm/forward.go | Core change: replaces unconditional body materialization with a conditional streaming path for known-length, non-body-substitution requests; adds response size rejection (502 for known oversize, panic-abort for mid-stream oversize). Logic is sound; chunked bodies are still materialized and hard-capped at 64 MB regardless of MaxRequestBytes. |
| internal/mitm/websocket.go | Adds response-size limiting to the non-upgrade fallback path; mid-stream abort now includes slog.Warn (addressing prior review comment). Known-oversize 502 path is missing the slog.Warn present in forward.go. |
| internal/mitm/proxy.go | Adds maxResponseBytes/maxRequestBytes fields and Options wiring; zero-value MaxRequestBytes correctly falls back to DefaultMaxRequestBytes (1 GiB). |
| internal/brokercore/substitution.go | Adds HasBodySubstitutions helper; ApplyBodySubstitutions already has an early-exit when no body subs are present, confirming the streaming path is safe. |
| internal/mitm/proxy_test.go | Adds 7 new tests covering 502 rejection, mid-stream abort, exact-fit pass-through, unlimited default, configurable request cap, and zero-value default. Coverage is thorough for the new code paths. |
| cmd/defaults.go | Adds defaultMaxResponseBytes/defaultMaxRequestBytes env-var readers; asymmetric sentinel handling (0=unlimited for response, 0=default 1 GiB for request) matches documented behavior. |
| cmd/server.go | Threads maxRespBytes/maxReqBytes through all three startup paths (foreground, detached child, spawnDetached) and registers the CLI flags; changes are consistent and complete. |
| internal/brokercore/session.go | Replaces MaxProxyBodyBytes with DefaultMaxRequestBytes (1 GiB) and introduces MaxMaterializeBytes (64 MB) for the RAM-materialization path. |
Reviews (2): Last reviewed commit: "fix: add missing slog.Warn on mid-stream..." | Re-trigger Greptile
|
@greptile review |
…SE) (#229) ## 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 - [x] Bug fix - [ ] New feature - [ ] Refactor / cleanup - [ ] Documentation - [ ] CI / build ## Test plan - [x] Existing tests pass (`go test ./...` — all 20 packages green) - [x] 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) | - [x] 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 - [x] No secrets or credentials in code - [x] No new unauthenticated endpoints - [x] Input validation on new API surfaces - [x] Checked for OWASP top 10 (injection, XSS, etc.) --------- Co-authored-by: saif <11242541+saifsmailbox98@users.noreply.github.com>
Summary
The MITM proxy hardcoded a 100 MB response body cap (
MaxResponseBytes) and a 64 MB request body cap (MaxProxyBodyBytes) that silently truncated large transfers.git cloneof any repo with >100 MB of history (TypeScript, React, Kubernetes, most company monorepos) failed with a confusingearly EOF/unexpected disconnect while reading sideband packeterror. The server loggedstatus=200with no warning — users had no way to tell the proxy was at fault.Confirmed by reproduction: cloning
microsoft/TypeScriptthrough the proxy truncated at exactly 104,857,600 bytes (the hardcoded limit). Same root cause reported in #210 and external draft PR #198.Response body cap: now unlimited by default
Response bodies are streamed with a 32 KB
io.Copybuffer — no OOM risk regardless of size. The old 100 MB cap only broke legitimate transfers. Default is now0(unlimited). Operators who want a bandwidth guard can set--max-response-bytes/AGENT_VAULT_MAX_RESPONSE_BYTES.When a limit IS configured:
Content-Length > limit): returns 502 Bad Gateway withX-Agent-Vault-Proxy-Errorand a clear JSON error before streaming any bodyhttp.ErrAbortHandler— client sees a connection reset, not silently corrupted dataThis follows the industry standard (nginx, Squid, Envoy): reject, never silently truncate.
Request body cap: configurable, default 1 GiB
Raised from 64 MB to 1 GiB via
--max-request-bytes/AGENT_VAULT_MAX_REQUEST_BYTES. Fixesgit pushof large repos and bulk file uploads that previously hit 413.OOM fix: conditional body materialization
Previously, every request body was fully buffered in RAM by
MaterializeRequestBody— up to the body cap per concurrent request. Now:Content-Lengthis forwarded from the original request.MaxMaterializeBytes) regardless of the request cap. Body substitutions target small API payloads (JSON, form-encoded), never large file uploads.The reorder is safe:
Inject()takes(ctx, vaultID, host, path)— no body dependency — so it moves before materialization.HasBodySubstitutions()(new helper) checks the substitution surfaces before deciding the path.Observability
Both the 502 rejection and mid-stream abort paths log
slog.Warnwith host, path, bytes streamed, and the configured limit. Previously: zero logging on truncation.Type of change
Test plan
make test— all 20 packages green)TestResponseLimitRejectsKnownOversizeWith502X-Agent-Vault-Proxy-Error+ JSON errorTestResponseLimitAbortsChunkedMidStreamTestResponseExactFitNoAbortTestResponseUnlimitedByDefaultMaxResponseBytes=0→ large response passes throughTestRequestBodyCapConfigurableTestDefaultMaxRequestBytesZeroMeansDefaultOptions{}zero-value → 1 GiB defaultTestHasBodySubstitutionsmicrosoft/TypeScriptthrough the proxy — completed successfully (was failing before at 100 MB)Supersedes #198.
Security checklist