a2aproject / a2aproject/a2a-rs

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

Aperta
#201 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
bug
Lingua principale
Rust
Stelle
75
Fork
19
Merge medio
11h 27m
PR unite (30g)
21

Descrizione

## 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.

Guida per i contributori

Apri la guida per i contributori

Valutazione

Questa issue non è ancora stata valutata.

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.