Skip to content

feat(streaming): expose server-streaming RPCs as SSE via Accept negotiation #51

@polaz

Description

@polaz

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions