a2aproject / a2aproject/a2a-rs

a2a-server: SSE streams have no event ids or keep-alive, and request bodies are unbounded

Offen
#201 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
bug
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

Beitragsleitfaden öffnen

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

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.