You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
SSE transport for incremental delivery (graphql-sse) #33
Incremental responses ship only as multipart/mixed (greem/src/http.rs:40-100; the axum example negotiates it from Accept). GraphQL over Server-Sent Events (graphql-sse, "distinct connections" mode) is the other transport clients implement for @defer/@stream, and the one that survives proxies and browsers that buffer multipart bodies. accepts_multipart already sees text/event-stream in its test inputs and treats it as not multipart (http.rs:105).
The encoder closure of execute_stream (ADR-0008) is where a wire format plugs in. An SSE frame is event: next + data: <json> per payload plus a final event: complete; nothing in the executor changes.
The multipart label is incrementalSpec=v0.2 negotiated per Accept (http.rs:45, fc5474d); SSE needs its own negotiation branch.
Scope note from #40's grilling: subscriptions ship no transport in the core. This issue's distinct-connections mode also carries the subscription response stream (one event: next per event, event: complete when the event source ends), so SSE negotiation in greem::http::handle replaces the 422 a subscription gets today. graphql-ws is #54; Apollo multipart subscriptions is #55 and waits on this negotiation.
Terms Transport and Framing are in CONTEXT.md; the status-code override is ADR-0014.
Scope
graphql-sse distinct connections mode only, as a third framing in greem::http::handle beside a single JSON body and multipart/mixed. It carries @defer/@stream payloads and the subscription response stream (Subscriptions #40).
Single connection mode (PUT reservation, X-GraphQL-Event-Stream-Token, 202 + one multiplexed stream, DELETE ?operationId=) is not planned. It needs a reservation table and expiry timers the core does not have (ADR-0008); if ever wanted it is a sibling of graphql-ws transport for subscriptions #54's protocol state machine, not a change to handle.
text/event-stream joins accept.rs as a single media type (SSE-only clients, mirroring Media::Multipart) and as a streaming framing. A bare entry matches (Exact); charset other than utf-8 excludes it; other parameters are ignored; text/* and */* do not count, as they do not for multipart.
SSE-only clients (no JSON type accepted) get every response as an event stream at 200, request errors included: a parse or validation failure is a next event carrying errors, then complete. graphql-sse requires this; a browser EventSource learns nothing from a 4xx. This overrides ADR-0012 for that client class only (ADR-0014).
Mixed-accept clients keep the draft's status codes for single results and request errors; only a response that streams (a subscription, or incremental delivery when SSE is the chosen framing) goes out as events.
A subscription streams over SSE whenever text/event-stream is accepted at q > 0, whatever its rank, since nothing else can carry it. Otherwise 406 with a message naming text/event-stream, replacing today's 422.
Incremental delivery over SSE follows the multipart rule (preferred at least as much as the single type): one next per payload in the pending/completed format, complete after the hasNext: false payload. When multipart and SSE both qualify: higher q, then higher specificity (incrementalSpec=v0.2 is Format), then multipart on a full tie, as the transport the incremental RFC defines.
Subscriptions are allowed over GET (EventSource can only GET) and QUERY; mutations keep 405/422. HTTP-level rejections (405, 406, 415) stay plain status codes for every client: the protocol's "validation steps" are GraphQL's, not method or body checks.
Surface
greem::http gains sse_event(&Payload) -> Vec<u8> (event: next + data: + compact JSON), sse_complete() with the empty data: line EventSource needs, and SSE_CONTENT_TYPE (text/event-stream; charset=utf-8), beside multipart_part and MULTIPART_CONTENT_TYPE.
Body::Parts is renamed Body::Stream, framing-neutral. handle chains the complete chunk after the payload stream ends, since the payload encoder never sees the end.
Response keeps {status, content_type, allow, body}. Cache-Control: no-cache and SSE comment-line keepalives (:\n\n merged into the body stream on a timer) are adapter concerns, documented on handle and Body::Stream. The axum example sets Cache-Control and adds no keepalive.
Proof
greem/tests/http.rs: SSE-only query as next + complete, request error as events, subscription over GET, incremental delivery over SSE, the multipart-versus-SSE tie, the 406 for a subscription without SSE.
The axum example gains a Subscription root with a finite event source over the seeded data, and a test reading a subscription through to complete.
http-audit runs the graphql-sse client once against the axum example (a query and a subscription), so a real client's parser accepts the frames.
#55 (Apollo multipart subscriptions) is unblocked once this lands: it is a second multipart label in the same negotiation, and the multipart-versus-SSE tie-break will then need a subscription case.
Activity
XiNiHa commentedon Oct 17, 2026
Scope note from #40's grilling: subscriptions ship no transport in the core. This issue's distinct-connections mode also carries the subscription response stream (one
event: nextper event,event: completewhen the event source ends), so SSE negotiation ingreem::http::handlereplaces the 422 a subscription gets today. graphql-ws is #54; Apollo multipart subscriptions is #55 and waits on this negotiation.test the subscription stream's pull discipline and reject subscriptio…
test the subscription stream's pull discipline and reject subscriptio…
test the subscription stream's pull discipline and reject subscriptio…
XiNiHa commentedon Oct 17, 2026
Settled design (grilling, 2026-10-17)
Terms Transport and Framing are in
CONTEXT.md; the status-code override is ADR-0014.Scope
greem::http::handlebeside a single JSON body andmultipart/mixed. It carries@defer/@streampayloads and the subscription response stream (Subscriptions #40).X-GraphQL-Event-Stream-Token, 202 + one multiplexed stream,DELETE ?operationId=) is not planned. It needs a reservation table and expiry timers the core does not have (ADR-0008); if ever wanted it is a sibling of graphql-ws transport for subscriptions #54's protocol state machine, not a change tohandle.accept.rsparses q since Serve GraphQL over HTTP from greem::http and audit it with graphql-http #47.Negotiation
text/event-streamjoinsaccept.rsas a single media type (SSE-only clients, mirroringMedia::Multipart) and as a streaming framing. A bare entry matches (Exact);charsetother than utf-8 excludes it; other parameters are ignored;text/*and*/*do not count, as they do not for multipart.nextevent carryingerrors, thencomplete. graphql-sse requires this; a browserEventSourcelearns nothing from a 4xx. This overrides ADR-0012 for that client class only (ADR-0014).text/event-streamis accepted at q > 0, whatever its rank, since nothing else can carry it. Otherwise 406 with a message namingtext/event-stream, replacing today's 422.nextper payload in the pending/completed format,completeafter thehasNext: falsepayload. When multipart and SSE both qualify: higher q, then higher specificity (incrementalSpec=v0.2isFormat), then multipart on a full tie, as the transport the incremental RFC defines.EventSourcecan only GET) and QUERY; mutations keep 405/422. HTTP-level rejections (405, 406, 415) stay plain status codes for every client: the protocol's "validation steps" are GraphQL's, not method or body checks.Surface
greem::httpgainssse_event(&Payload) -> Vec<u8>(event: next+data:+ compact JSON),sse_complete()with the emptydata:lineEventSourceneeds, andSSE_CONTENT_TYPE(text/event-stream; charset=utf-8), besidemultipart_partandMULTIPART_CONTENT_TYPE.Body::Partsis renamedBody::Stream, framing-neutral.handlechains thecompletechunk after the payload stream ends, since the payload encoder never sees the end.Responsekeeps{status, content_type, allow, body}.Cache-Control: no-cacheand SSE comment-line keepalives (:\n\nmerged into the body stream on a timer) are adapter concerns, documented onhandleandBody::Stream. The axum example setsCache-Controland adds no keepalive.Proof
greem/tests/http.rs: SSE-only query asnext+complete, request error as events, subscription over GET, incremental delivery over SSE, the multipart-versus-SSE tie, the 406 for a subscription without SSE.Subscriptionroot with a finite event source over the seeded data, and a test reading a subscription through tocomplete.http-auditruns thegraphql-sseclient once against the axum example (a query and a subscription), so a real client's parser accepts the frames.#55 (Apollo multipart subscriptions) is unblocked once this lands: it is a second multipart label in the same negotiation, and the multipart-versus-SSE tie-break will then need a subscription case.
record the SSE framing design: transport and framing terms, ADR-0014