Skip to content

fix(a2a): wire maxRequestBodyBytes into zio-http; raise default to 64 MiB#59

Merged
arcaputo3 merged 1 commit into
mainfrom
fix/a2a-max-request-body-bytes
Jun 12, 2026
Merged

fix(a2a): wire maxRequestBodyBytes into zio-http; raise default to 64 MiB#59
arcaputo3 merged 1 commit into
mainfrom
fix/a2a-max-request-body-bytes

Conversation

@arcaputo3

Copy link
Copy Markdown
Contributor

Summary

Fixes #58. The JVM A2A server built its zio-http server from Server.Config.default and never applied the configured maxRequestBodyBytes. zio-http 3.0.1's default is RequestStreaming.Disabled(1024 * 100), so every A2A request was silently capped at 100 KiB and larger bodies were rejected with HTTP 413 before the handler ran.

This broke file uploads: A2A Parts carry files as base64 fileWithBytes inside the JSON-RPC body (~33% inflation), so even a handful of small Markdown files tripped the limit (reported by rtalk: "413 too large for uploads… for trivial ones too").

Changes

  • A2AServerLive.scala — chain .disableRequestStreaming(config.maxRequestBodyBytes) onto Server.Config.default.binding(...) so the configured cap actually applies. disableRequestStreaming keeps full-body aggregation (the JSON-RPC routes parse the whole body) at the larger cap instead of 100 KiB.
  • A2AServerDefaults.scala — raise MaxRequestBodyBytes from 1 MiB → 64 MiB to cover inline document uploads after base64 inflation. Clients needing larger payloads should use fileWithUri references.

JS/Bun parity (no production change needed)

The JS/Bun server already enforces maxRequestBodyBytes manually in readBodyLimited (chunked read + long-arithmetic guard) and shares the same A2AServerDefaults.MaxRequestBodyBytes, so the default bump raises its limit automatically. Verified, and covered with a parity test.

Tests

  • JVM regression (A2AServerLiveSpec): starts a real bound zio-http server and POSTs a ~200 KiB message/send, asserting 200 + a completed task. The pre-existing body-limit test calls handleHttp in-process and bypasses the Server.Config cap, so it never caught this. Confirmed the new test fails without the fix (413 → response doesn't parse as JSON-RPC) and passes with it.
  • JS parity (A2ARestTransportSpec): accepts a ~200 KiB message/send at the default limit.

Both suites green: ./mill agent.jvm.test (143/143) and ./mill agent.js.test.bunTest (190/190).

Follow-ups (out of scope)

  • Modal/edge transports may impose their own request-size ceiling above this; this fix removes scalagent as the bottleneck for normal uploads. Prefer fileWithUri for very large files.
  • Optionally surface the inline-upload ceiling in the agent card/docs (no standard AgentCard field for it today).

🤖 Generated with Claude Code

… MiB

The JVM A2A server built its zio-http server from Server.Config.default and
never applied the configured maxRequestBodyBytes. zio-http 3.0.1's default is
RequestStreaming.Disabled(1024*100), so every A2A request was silently capped
at 100 KiB and larger bodies were rejected with HTTP 413 before the handler
ran. This broke file uploads: A2A Parts carry files as base64 fileWithBytes
inside the JSON-RPC body (~33% inflation), so even a few small files tripped
the limit.

- A2AServerLive: chain .disableRequestStreaming(config.maxRequestBodyBytes)
  onto Server.Config.default so the configured cap actually applies (keeps
  full-body aggregation, which the JSON-RPC routes need).
- A2AServerDefaults: raise MaxRequestBodyBytes from 1 MiB to 64 MiB to cover
  inline document uploads after base64 inflation. Clients needing larger
  payloads should use fileWithUri references.

The JS/Bun server already enforces this limit manually in readBodyLimited and
shares the same default, so the bump raises its limit too with no code change.

Tests:
- JVM regression (A2AServerLiveSpec): start a real bound zio-http server and
  POST a ~200 KiB message/send, asserting 200 + a completed task. The existing
  body-limit test calls handleHttp in-process and bypasses the Server.Config
  cap, so it never caught this; verified the new test fails without the fix.
- JS parity (A2ARestTransportSpec): accept a ~200 KiB message/send at the
  default limit.

Closes #58

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@claude

claude Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review: wire maxRequestBodyBytes into zio-http; raise default to 64 MiB

Solid, well-targeted fix. The root cause is correctly diagnosed: Server.Config.default carries RequestStreaming.Disabled(1024*100), so the framework 413'd before the handler ran, and the existing handleHttp-based limit test couldn't catch it because it never goes through Server.Config. The new JVM test that binds a real server and confirms it fails without the fix is exactly the right regression. The comments explaining why disableRequestStreaming (keeps full-body aggregation, not streaming) are appreciated. Nice catch on JS parity too.

A few things worth considering before merge:

🐛 The <= 0 "unlimited" sentinel is silently broken on the JVM bound server

Throughout the codebase, maxRequestBodyBytes <= 0 is treated as unlimited:

  • A2AHttpBinding.validateContentLength / validateBodyLengthif maxBytes > 0 && ...
  • JVM readBodyAfterContentLength (A2AServerLive.scala:262) — if maxBytes > 0 then [limited] else request.body.asChunk
  • JS readBodyLimited (A2AServer.scala:352) — if maxBytes <= 0 then [full read]

But the new wiring passes config.maxRequestBodyBytes straight into disableRequestStreaming(...). With maxRequestBodyBytes = 0, zio-http's RequestStreaming.Disabled(0) aggregates up to 0 bytes and will 413 every non-empty request — the opposite of the documented "unlimited" semantics, and effectively the very bug this PR fixes, reintroduced for that config. maxRequestBodyBytes is a public constructor param (A2AServerLive.Config), so a caller can reach this.

Suggest guarding the wiring to honor the sentinel, e.g.:
```scala
val base = Server.Config.default.binding(config.host, config.port)
val configured =
if config.maxRequestBodyBytes > 0 then base.disableRequestStreaming(config.maxRequestBodyBytes)
else base.enableRequestStreaming // or leave default / a sufficiently large cap
```
(Worth confirming zio-http 3.0.1's enableRequestStreaming semantics — under request streaming the handler's own chunked read still enforces the limit, so <= 0 stays genuinely unlimited.)

⚠️ Memory / DoS surface of the 64 MiB default

The default now lets a single request buffer up to 64 MiB fully in memory before the handler runs, and it applies to every A2A server — including ones that never expect inline uploads. Under concurrent connections this is a meaningful amplification vs. the old 1 MiB (64× per in-flight request). It's a reasonable trade-off for the upload use case and fileWithUri is the documented escape hatch, but consider (a) a one-line note in the PR/docs that operators fronting untrusted traffic should lower this, and (b) whether 64 MiB should be opt-in for upload-heavy deployments rather than the global default. Not blocking — just flagging the implicit policy change.

Minor

  • With the framework cap now equal to the in-handler cap, the nicer in-handler JSON-RPC bodySizeExceeded error ("Request body exceeds N byte limit") will never fire on the bound server for oversize bodies — zio-http returns a raw 413 first. That's strictly better than before (and the in-handler error still covers the handleHttp path), but it means oversize clients get a framework 413 rather than a JSON-RPC error body. Worth a sentence in docs if clients are expected to parse that error.
  • The new JVM test's coverage stops at the lower bound (~200 KiB accepted). Optional: a test asserting rejection above 64 MiB would pin the upper bound and guard against future regressions of the cap itself. Out of scope for this fix.

Overall: correct fix, good regression test, ship-worthy once the <= 0 sentinel case is handled (or explicitly confirmed unreachable/unsupported).

🤖 Generated with Claude Code

@arcaputo3 arcaputo3 merged commit c9a516b into main Jun 12, 2026
3 checks passed
@arcaputo3 arcaputo3 deleted the fix/a2a-max-request-body-bytes branch June 12, 2026 00:01
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.

A2A server caps request bodies at 100 KiB — file uploads 413 (configured maxRequestBodyBytes is never applied)

1 participant