feat: HTTP Events Query as an opt-in alternative to SSE - #1358
Draft
dunglas wants to merge 11 commits into
Draft
Conversation
The protocol has no way to express what happened to a topic: an update on a book URL could be a creation, a status change or a deletion. Document the envelope options for the opaque data field, with ActivityStreams 2.0 as the worked example, including the Update-means-full-replacement caveat and the split between the activity id and the hub-assigned SSE id.
Publishers can declare the media type of the data payload. The value is validated as a media type (rejected with a 400 otherwise, since it ends up on the wire as a header of negotiated response encodings) and stored with the update; text/event-stream defines no field for it, so SSE output is unchanged.
Implement the HTTP Events Query Internet-Draft (draft-gupta-httpapi-events-query) as an opt-in alternative to Server-Sent Events, enabled with WithEventsQuery(). A QUERY subscriber whose Accept header prefers multipart/mixed receives each update as a body part: the raw data as the part body, the event ID in Content-ID and the publisher-declared media type in Content-Type. The response carries Events: duration (bounded by the write deadline), Incremental: ?1 and Accept-Query; the Events request header caps the connection duration, for SSE subscribers too. text/event-stream remains the default for every other request, and resumption via Last-Event-ID, last_event_id and Mercure-Last-Event-ID works identically on both encodings.
Expose mercure.WithEventsQuery() as a Caddyfile directive and JSON option. Experimental.
Add the directive to the configuration table, a subscribing section showing an HTTP Events Query request/response pair with resumption, and the content_type row to the publish form fields.
Incremental: ?1 (draft-ietf-httpbis-incremental) is the standardized equivalent of X-Accel-Buffering: no and benefits SSE responses just as much as negotiated encodings; Accept-Query (RFC 10008) advertises the form-encoded QUERY body the hub accepts unconditionally. The non-standard Events header stays gated on the events_query option.
Content-ID's msg-id syntax requires <left@right>, which cannot hold arbitrary Mercure IDs, and Content-Location cannot either since IDs may be non-URI strings by design. Content-Event-Id is a MIME extension field (RFC 2045 reserves the Content- prefix for those; RFC 2046 gives body-part meaning to Content-* fields only) carrying the update ID verbatim, so it round-trips exactly into last_event_id.
Gated behind the events query support. The data part keeps its raw bytes and its Content-Type header declares the event media type, replacing the content_type form field. Binary updates are base64-encoded when serialized to text/event-stream or JSON, and delivered verbatim in multipart subscription responses.
The core specification stays untouched: everything Events Query-specific (multipart/mixed subscription responses, the Events header, Content-Event-Id, multipart/form-data publications and binary payloads) moves to spec/mercure-events-query.md, a standalone RFC-style extension that updates the core requirements only for hubs implementing it.
dunglas
force-pushed
the
feat/events-query
branch
from
August 17, 2026 11:56
e3e89a6 to
6ceb923
Compare
|
Hi @dunglas — I noticed the I’m testing RunBack, an early alpha CLI that reconstructs a failed public Actions run locally with runback doctor
runback https://github.com/dunglas/mercure/actions/runs/32027374001The Linux amd64 binary and checksum are in the v0.1.0-alpha release. If you try it, the verdict ( |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the HTTP Events Query Internet-Draft as an opt-in alternative delivery mechanism to Server-Sent Events, plus binary event payloads.
What
With the new
events_querydirective (WithEventsQuery()in Go), aQUERYsubscriber whoseAcceptheader prefersmultipart/mixedreceives each update as a body part: the rawdataas the part body, the event ID inContent-Event-Id(a MIME extension field carrying the ID verbatim), and the publisher-declared media type inContent-Type. Everything else stays byte-identical SSE, so enabling the option cannot regress existing clients.QUERYbody (topics +last_event_id) serves as the draft's subscription data model realization; no new request format.Events: duration=N(request) caps the connection; the response advertises the effective bound, never longer thanwrite_timeoutor the token expiration. Gated onevents_querysince the field is not standard yet.Last-Event-ID/last_event_idin,Mercure-Last-Event-Idout (now CORS-exposed, fixing a pre-existing gap for fetch-based clients).Incremental: ?1andAccept-Query: application/x-www-form-urlencodedare sent on every subscription response: the former is the standardized no-buffering signal SSE benefits from too, the latter advertises the unconditional QUERY support (RFC 10008).mercure_events_queryin the RFC 9728 protected resource metadata.Binary payloads
The publish endpoint also accepts
multipart/form-databodies (gated onevents_query,415otherwise). Thedatapart carries raw bytes and itsContent-Typeheader declares the event media type, which travels to multipart subscribers as the part'sContent-Type.database64-encoded, always (even for text payloads), so decoding is deterministic without a per-event metadata slot. This also gives byte-exact round-tripping, which SSE line splitting cannot (CR/CRLF normalize to LF).Update.MarshalJSONbase64-encodes binary data (encoding/jsonwould silently corrupt invalid UTF-8 to U+FFFD), so the bolt JSON history is lossless.Spec
spec/mercure.mdis untouched. Everything Events Query-specific lives in a new standalone RFC-style extension,spec/mercure-events-query.md(draft-dunglas-mercure-events-query), which updates the core requirements only for hubs implementing it: multipart/mixed negotiation on QUERY subscriptions, the notification encoding (Content-Event-Idrationale included), bounded responses and theEventsheader, resumption via the Mercure reconciliation model, multipart/form-data publications with the SSE base64 rule, discovery, and the generalized field-injection security considerations. Unlike the core spec, the extension cites the Events Query draft directly.Design choices
application/json-seqsupport was considered and dropped; the multipart realization only uses native slots (aContent-Event-IdMIME extension field,Content-Type, raw body). SSE-specific properties (type,retry) are not represented.datapart's ownContent-Typeheader — the native MIME slot — rather than acontent_typeform field (an earlier revision of this branch had one; it was removed to keep a single, native way to declare it).streamEncoderinterface (sseEncoder,multipartEncoder), one instance per connection; the SSE path is byte-for-byte the previous behavior.WithEventsQueryis marked experimental: individual I-D, subject to change.