a2aproject / a2aproject/a2a-rs
a2a-server: SSE streams have no event ids or keep-alive, and request bodies are unbounded
- Vorherrschende Sprache
- Rust
- Sterne
- 75
- Forks
- 19
- Ø Merge
- 11 Std. 27 Min.
- Gemergte PRs (30 T.)
- 21
Beschreibung
## Summary
Three gaps in `a2a-server`'s HTTP surface, all present in `a2a-go` and absent here.
### 1. SSE events carry no `id`
`sse_from_stream` and `sse_jsonrpc_stream` both build events as `Event::default().data(...)`. There is no event identifier on the wire.
`a2a-go` writes one on every event (`internal/sse/sse.go:78`, `id:` prefix at `:34`). Without it a client cannot tell a gap from a clean sequence, and `Last-Event-ID` — the mechanism SSE defines for resuming an interrupted stream — has nothing to carry.
### 2. No keep-alive on SSE responses
Neither stream attaches a keep-alive, so an idle subscription sends nothing between events. Proxies and load balancers terminate idle connections, typically after 30–60s, which is well inside the lifetime of the long-running tasks `subscribe` exists for. `a2a-go` has `SSEWriter::WriteKeepAlive` (`internal/sse/sse.go:68`).
### 3. No request body size limit
Neither `rest_router` nor `jsonrpc_router` bounds the request body, so a single large POST is read into memory. `a2a-go` caps SSE payloads at `MaxSSETokenSize = 10 * 1024 * 1024` (`internal/sse/sse.go:39`); the other SDKs bound request bodies similarly.
## Why this matters to us specifically
`a2acli`'s `task subscribe` is the direct casualty of (1) and (2):
- **(2) is why a subscription dies.** A task that runs for minutes with quiet stretches gets its connection dropped by an intermediary, and the CLI sees a stream that simply ends.
- **(1) is why it cannot recover.** Tier 2 #186 requires resumption after disconnect, and `SPEC.md` §9.4 says to resume "from the last received event where the server supports it". Without event IDs our server does not support it, so the best `a2acli` can ever do against a2a-rs is re-subscribe from scratch.
So #186 is blocked on this, not the other way round.
## Scope
- [ ] Assign an `id` to every SSE event on both stream functions.
- [ ] Attach a keep-alive so idle subscriptions are not dropped by intermediaries.
- [ ] Bound request bodies on both routers, rejecting oversized ones with `413`.
- [ ] Tests: IDs present and increasing across multiple events on both functions; an over-limit body is rejected on both bindings.
## Note on the ID scheme
`a2a-go` uses a fresh UUID per event. A monotonic counter is the more useful choice: it lets a client detect that it missed event 4 between 3 and 5, which an opaque UUID cannot. Worth taking the counter and recording the divergence, rather than matching Go exactly.
## Credit
Found and first fixed by @ez-lbz in #125.
Beitragsleitfaden
Rechercherichtung
Look at the `sse_from_stream` and `sse_jsonrpc_stream` functions to understand how SSE events are built. Examine the router handlers in `rest_router` and `jsonrpc_router` to see where request bodies are read. Add event IDs (likely a monotonic counter), implement keep-alive logic, and set a size limit for request bodies. Write tests to verify IDs are present and increasing, and that oversized requests are rejected with a 413 status.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- rust
- Bereich
- api, backend
- Issue-Typ
- Bug
- Schwierigkeit
- 3/5
- Geschätzter Aufwand
- 1-2 Tage
- Aktivitätsstatus
- Aktiv
- Klarheit
- Klar beschrieben
- Anfängerfreundlichkeit
- 70/100