Skip to content

fix: harden SSE transport and enforce request body size limit - #1184

Open
ez-lbz wants to merge 4 commits into
a2aproject:mainfrom
ez-lbz:fix/transport-hardening
Open

fix: harden SSE transport and enforce request body size limit#1184
ez-lbz wants to merge 4 commits into
a2aproject:mainfrom
ez-lbz:fix/transport-hardening

Conversation

@ez-lbz

@ez-lbz ez-lbz commented Aug 10, 2026

Copy link
Copy Markdown

What changed

1. Configure SSE heartbeat and stream timeout explicitly

Problem: EventSourceResponse was created without explicit ping/send_timeout in src/a2a/server/routes/jsonrpc_dispatcher.py (_create_response) and src/a2a/server/routes/rest_dispatcher.py (_handle_streaming). With the installed sse-starlette, the default ping is None — no heartbeat is emitted — so idle streams send nothing and dead clients are never detected; there is also no cap on how long a stream may sit without producing output.

Fix (src/a2a/server/routes/jsonrpc_dispatcher.py, src/a2a/server/routes/rest_dispatcher.py):

  • Added SSE_PING_INTERVAL_SECONDS = 15 and SSE_SEND_TIMEOUT_SECONDS = 300 to src/a2a/utils/constants.py.
  • Every EventSourceResponse created by both dispatchers now passes ping=15 (heartbeat frame every 15s) and send_timeout=300 (tear down a stream that makes no progress for 5 minutes).

2. Enforce a request body size limit

Problem: No limit was enforced on the HTTP request body. A client could POST an arbitrarily large payload, which the JSON-RPC (request.json()) and REST (request.body()) paths buffer fully in memory — a memory-exhaustion / DoS vector.

Fix (src/a2a/server/routes/common.py, src/a2a/server/routes/jsonrpc_dispatcher.py, src/a2a/server/routes/rest_dispatcher.py):

  • Added MAX_REQUEST_BODY_SIZE = 10 MiB to src/a2a/utils/constants.py.
  • Added read_request_body_with_limit() in routes/common.py: it fast-rejects via the Content-Length header when present, and otherwise streams the body in chunks with an incremental cap, so oversized chunked bodies are rejected while being read rather than buffered unboundedly. The body is cached on the request so existing request.body()/stream() consumers keep working.
  • JSON-RPC: handle_requests now reads through the limited reader; the existing HTTPException(413) handling returns InvalidRequestError("Payload too large").
  • REST: all four body-reading sites (_handle_streaming pre-consume, on_message_send, on_message_send_stream, set_push_notification) now use a wrapper that converts the 413 into InvalidRequestError("Payload too large") (400).

Testing

  • ./.venv/Scripts/python -m pytest tests/server/routes/test_jsonrpc_dispatcher.py tests/server/routes/test_rest_dispatcher.py -q39 passed (includes new tests: oversized body rejected via body content and via Content-Length, and ping_interval/send_timeout assertions on the returned EventSourceResponse for both dispatchers).
  • ./.venv/Scripts/python -m pytest tests/server/ tests/compat/ -q924 passed, 90 skipped, 3 xfailed (pre-existing xfails reference upstream issue [Feat]: Improve server concurrency architecture #869).
  • ./.venv/Scripts/python -m ruff check on all modified files: clean.
  • Behavior change: SSE streams now emit a heartbeat comment every 15s (previously none with the installed sse-starlette) and are torn down after 300s without progress; request bodies over 10 MiB are rejected with 413/"Payload too large" instead of being buffered.

@ez-lbz
ez-lbz requested a review from a team as a code owner August 10, 2026 16:10
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

🧪 Code Coverage (vs main)

⬇️ Download Full Report

Base PR Delta
src/a2a/server/routes/common.py 89.66% 88.33% 🔴 -1.32%
src/a2a/server/routes/jsonrpc_dispatcher.py 85.44% 86.59% 🟢 +1.15%
src/a2a/server/routes/rest_dispatcher.py 94.25% 92.43% 🔴 -1.82%
Total 92.97% 92.96% 🔴 -0.02%

Generated by coverage-comment.yml

ez-lbz added 3 commits August 11, 2026 00:39
Use the literal 413 as the ImportError fallback instead of the Any special
form, and move the ty ignore comment to the line that actually errors so
the type checker suppression takes effect.
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