Problem
Server-streaming RPCs are currently exposed only as NDJSON (application/x-ndjson, chunked). Browser consumers using the native EventSource API cannot read this framing, so streaming endpoints are effectively unusable from the browser. Additionally, a gRPC error mid-stream is surfaced as a broken HTTP body (io::Error truncates the response) with no explicit signal, so a client cannot tell "the stream ended" from "the stream failed".
Solution
Add Server-Sent Events output for server-streaming RPCs, selected via content negotiation, keeping NDJSON as the backward-compatible default.
Accept negotiation: Accept: text/event-stream -> SSE response; anything else -> current NDJSON.
- SSE framing via
axum::response::sse (Sse / Event / KeepAlive), not hand-rolled data: strings.
- Keep-alive for SSE, interval configurable (
streaming.sse_keep_alive_secs, default 15s) so idle streams survive LB / nginx read timeouts.
- Terminal error event: a gRPC
Status error mid-stream is emitted as an explicit final frame in both formats (event: error for SSE, a final {"error","message"} JSON line for NDJSON) and the stream is closed cleanly instead of truncated.
- Shared
SerializeOptions: unary and streaming JSON serialization currently duplicate the SerializeOptions block; extract one helper so both paths are byte-for-byte consistent.
Acceptance criteria
- A server-streaming RPC requested with
Accept: text/event-stream returns content-type: text/event-stream with one data: event per gRPC message.
- The same RPC without that
Accept header keeps returning application/x-ndjson (no behavior change for existing clients).
- SSE responses carry keep-alive comments at the configured interval.
- A gRPC error mid-stream produces an explicit terminal
event: error (SSE) / final error JSON line (NDJSON), then a clean close.
- Unary and streaming share a single
SerializeOptions constructor.
- Unit tests cover:
Accept decision, SSE event framing of one message, NDJSON framing of one message, error-frame formatting.
- README streaming bullet documents the SSE/NDJSON negotiation and the keep-alive knob.
Estimate
4h (1h Accept + SSE handler split, 1h keep-alive config wiring, 1h error-frame + shared options, 1h tests + docs)
Problem
Server-streaming RPCs are currently exposed only as NDJSON (
application/x-ndjson, chunked). Browser consumers using the nativeEventSourceAPI cannot read this framing, so streaming endpoints are effectively unusable from the browser. Additionally, a gRPC error mid-stream is surfaced as a broken HTTP body (io::Errortruncates the response) with no explicit signal, so a client cannot tell "the stream ended" from "the stream failed".Solution
Add Server-Sent Events output for server-streaming RPCs, selected via content negotiation, keeping NDJSON as the backward-compatible default.
Acceptnegotiation:Accept: text/event-stream-> SSE response; anything else -> current NDJSON.axum::response::sse(Sse/Event/KeepAlive), not hand-rolleddata:strings.streaming.sse_keep_alive_secs, default 15s) so idle streams survive LB / nginx read timeouts.Statuserror mid-stream is emitted as an explicit final frame in both formats (event: errorfor SSE, a final{"error","message"}JSON line for NDJSON) and the stream is closed cleanly instead of truncated.SerializeOptions: unary and streaming JSON serialization currently duplicate theSerializeOptionsblock; extract one helper so both paths are byte-for-byte consistent.Acceptance criteria
Accept: text/event-streamreturnscontent-type: text/event-streamwith onedata:event per gRPC message.Acceptheader keeps returningapplication/x-ndjson(no behavior change for existing clients).event: error(SSE) / final error JSON line (NDJSON), then a clean close.SerializeOptionsconstructor.Acceptdecision, SSE event framing of one message, NDJSON framing of one message, error-frame formatting.Estimate
4h (1h Accept + SSE handler split, 1h keep-alive config wiring, 1h error-frame + shared options, 1h tests + docs)