Skip to content

feat: configurable MITM response/request body size caps#227

Merged
saifsmailbox98 merged 2 commits into
mainfrom
saif/age2-6-configurable-mitm-response-body-size-cap-default-1-gib
Jun 1, 2026
Merged

feat: configurable MITM response/request body size caps#227
saifsmailbox98 merged 2 commits into
mainfrom
saif/age2-6-configurable-mitm-response-body-size-cap-default-1-gib

Conversation

@saifsmailbox98

@saifsmailbox98 saifsmailbox98 commented Jun 1, 2026

Copy link
Copy Markdown
Collaborator

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 clone of any repo with >100 MB of history (TypeScript, React, Kubernetes, most company monorepos) failed with a confusing early EOF / unexpected disconnect while reading sideband packet error. The server logged status=200 with no warning — users had no way to tell the proxy was at fault.

Confirmed by reproduction: cloning microsoft/TypeScript through 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.Copy buffer — no OOM risk regardless of size. The old 100 MB cap only broke legitimate transfers. Default is now 0 (unlimited). Operators who want a bandwidth guard can set --max-response-bytes / AGENT_VAULT_MAX_RESPONSE_BYTES.

When a limit IS configured:

  • Known oversize (Content-Length > limit): returns 502 Bad Gateway with X-Agent-Vault-Proxy-Error and a clear JSON error before streaming any body
  • Mid-stream discovery (chunked, hits limit): aborts the connection via http.ErrAbortHandler — client sees a connection reset, not silently corrupted data

This 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. Fixes git push of 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:

  • Known-length bodies without body substitutions (the common case: git, file uploads, API calls): streamed directly to the upstream without materialization. Content-Length is forwarded from the original request.
  • Unknown-length (chunked) or body-substitution bodies: materialized as before, but capped at 64 MB (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.Warn with host, path, bytes streamed, and the configured limit. Previously: zero logging on truncation.

Type of change

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

Test plan

  • Existing tests pass (make test — all 20 packages green)
  • Added/updated tests for new behavior (9 new tests):
Test Validates
TestResponseLimitRejectsKnownOversizeWith502 Content-Length > limit → 502 + X-Agent-Vault-Proxy-Error + JSON error
TestResponseLimitAbortsChunkedMidStream Chunked body > limit → connection abort, not clean 200
TestResponseExactFitNoAbort Body exactly at limit → completes normally (one-byte probe fix)
TestResponseUnlimitedByDefault Default MaxResponseBytes=0 → large response passes through
TestRequestBodyCapConfigurable Body > configured limit → 413
TestDefaultMaxRequestBytesZeroMeansDefault Options{} zero-value → 1 GiB default
TestHasBodySubstitutions Unit test for surface detection (8 subcases)
  • Manual testing: built binary, started server, cloned microsoft/TypeScript through the proxy — completed successfully (was failing before at 100 MB)

Supersedes #198.

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.)

…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.
@linear

linear Bot commented Jun 1, 2026

Copy link
Copy Markdown

AGE2-6

@infisical-review-police

Copy link
Copy Markdown

💬 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.

@mintlify

mintlify Bot commented Jun 1, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
agent-vault 🟢 Ready View Preview Jun 1, 2026, 10:51 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Comment thread internal/brokercore/brokercore.go
@veria-ai

veria-ai Bot commented Jun 1, 2026

Copy link
Copy Markdown

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 1 · PR risk: 0/10

@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown

Greptile Summary

This PR makes MITM proxy body-size caps configurable: response bodies are now unlimited by default (streamed with a 32 KB buffer, no OOM risk), and request bodies default to 1 GiB (up from 64 MB), fixing git clone and git push failures for large repositories. Request bodies with a known Content-Length and no body substitutions are now streamed directly to the upstream instead of being fully buffered in RAM.

  • Response limit: operators set --max-response-bytes / AGENT_VAULT_MAX_RESPONSE_BYTES; known-oversize returns 502, chunked oversize aborts the connection via http.ErrAbortHandler with a structured slog.Warn on both paths.
  • Request limit: operators set --max-request-bytes / AGENT_VAULT_MAX_REQUEST_BYTES; known-length bodies exceeding the limit get an immediate 413; bodies requiring RAM materialization (chunked or body-substitution) are still capped at a hard 64 MB MaxMaterializeBytes.
  • Streaming optimization: HasBodySubstitutions decides at runtime whether to stream (common path: git, file uploads) or materialize (body substitutions); ApplyBodySubstitutions has a matching early-exit when no body subs are present, confirming the streaming path is safe.

Confidence Score: 5/5

Safe to merge; the core streaming logic is correct, ApplyBodySubstitutions returns immediately when no body subs exist, and all three startup paths (foreground, detached child, spawned child) consistently thread the new limits through.

The change is well-tested (7 new integration tests covering 502 rejection, mid-stream abort, exact-fit pass-through, unlimited default, and configurable caps), the ApplyBodySubstitutions early-exit was verified to make the streaming path safe, and no new request-routing or host-validation code was introduced. The two findings are observability and documentation gaps that do not affect correctness for the primary use cases.

No files require special attention beyond the noted documentation gap in the CLI/env-var docs for chunked request behavior.

Important Files Changed

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

Comment thread internal/mitm/websocket.go
Comment thread internal/mitm/proxy.go
@saifsmailbox98

Copy link
Copy Markdown
Collaborator Author

@greptile review

@saifsmailbox98 saifsmailbox98 merged commit 5012f5c into main Jun 1, 2026
11 checks passed
@saifsmailbox98 saifsmailbox98 deleted the saif/age2-6-configurable-mitm-response-body-size-cap-default-1-gib branch June 1, 2026 23:27
saifsmailbox98 added a commit that referenced this pull request Jun 2, 2026
…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>
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