Skip to content

fix(gateway): bounded response bodies, single parse, SSE pass-through - #43

Merged
shreyanshjain7174 merged 2 commits into
mainfrom
fix/upstream-body-streaming
Oct 17, 2026
Merged

shreyanshjain7174 merged 2 commits into
mainfrom
fix/upstream-body-streaming

Conversation

@shreyanshjain7174

Copy link
Copy Markdown
Contributor

Plan

Phase 0, item 5 of the AgentGate scale-out prompt: response body limits, a single params parse, and SSE pass-through.

The gateway read upstream bodies with io.ReadAll(io.LimitReader(body, 1 MiB)) and silently truncated anything larger. It parsed params 3 times and response JSON twice. It had no streaming.

What changed

  • Oversized responses. The gateway reads at most limit + 1 bytes. Over the limit, the agent gets 502 upstream_response_too_large and the receipt records it. Nothing is truncated silently. The default is 1 MiB, overridable per service with upstream.max_response_bytes (1 KiB to 64 MiB).
  • Params parsed once. One decode feeds the receipt digest, URL building and the request body.
    • A dedicated canonicalizer replaces the second JCS pass.
    • Receipts hash identically: golden vectors, a corpus test, and a differential fuzz test (FuzzDigestParamsLegacy) against the old jcs.Transform path all pass.
  • Response JSON scanned once. json.Valid, then the ActResponse envelope is written by hand around the validated bytes. Field order and text escaping are unchanged. Upstream JSON is no longer HTML-escaped, which is safe because the response is application/json with nosniff.
  • SSE pass-through. Streaming happens only when the action sets stream: true, the agent sends Accept: text/event-stream, and the upstream answers 2xx with text/event-stream.
    • The receipt commits when upstream headers arrive, before any byte reaches the agent.
    • Bytes flush as they arrive.
    • A client disconnect closes the upstream.
    • max_stream_bytes (default 64 MiB) and an idle timeout (default 60s) end the stream.
    • No upstream headers are copied to the agent. The gateway forces text/event-stream, nosniff and no-cache.
    • A SHA-256 of the streamed bytes and the byte count are logged server-side.
    • Everything else falls back to the buffered path.
  • Receipt format unchanged. docs/spec/proposals/receipt-stream-digest.md proposes a v2 field for the stream digest.

Numbers (BenchmarkHandleAct, median of 5)

ns/op allocs/op
Before 49,099 330
After 46,745 266

A 1 MiB JSON response envelope now costs 4 allocs/op.

Validation

  • go build ./...: passed
  • go vet ./...: passed
  • go test -race ./...: passed
  • semgrep --config auto --error --quiet internal/gateway internal/upstream internal/registry: passed. Two scoped nosemgrep lines on raw writes. Each sits on a response with a fixed Content-Type and nosniff, and is commented.

Review

Reviewed by a second model (Claude Opus 5.5). Approved with 2 minors, both fixed here: a bounded final write deadline on streams, and removing the second JSON scan.

Known limits

  • An SSE receipt attests that the upstream accepted the request and started the stream. A later stream failure is logged, not receipted. Documented.
  • buildURL still does not escape path params. This is unchanged from main and is a separate fix.

shreyanshjain7174 and others added 2 commits October 18, 2026 03:49
Reject oversized responses instead of returning truncated bodies.
Reuse decoded params without changing JCS receipt hashes.
Commit SSE acceptance receipts before forwarding any bytes.
Keep final stream hashes in logs pending a v2 spec.

Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep a 5s deadline for HTTP stream terminators.
Forward validated JSON without a second scan or body-sized copy.
Preserve envelope field order and existing text escaping.

Signed-off-by: Shreyansh Sancheti <43677304+shreyanshjain7174@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@shreyanshjain7174
shreyanshjain7174 merged commit 9b37897 into main Oct 17, 2026
1 check passed
@shreyanshjain7174
shreyanshjain7174 deleted the fix/upstream-body-streaming branch October 24, 2026 17:47
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