Events Query Modular Implementation - #1364
Draft
CxRes wants to merge 14 commits into
Draft
Conversation
t.Cleanup removed the database file before closing the transport that still held it open. POSIX permits unlinking an open file, so this passes on Linux CI; Windows refuses, failing every test built on createBoltTransport. Closing first is correct on both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
registerSubscriber both registered a subscriber and began answering it. Those are separate concerns, and only the second belongs to the handler that goes on to hold the connection open -- which is also the only place with a framing to answer in, once there is more than one. Sending the headers immediately after registerSubscriber returns keeps the preamble on the same side of setDefaultWriteDeadline as before, so nothing about the response changes. The subscriber log and the connected metric now run before the headers rather than after, neither being observable to the subscriber. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This is a refactor of the Event Stream encoder in preperation of Events Query implementation. eventStreamEncoder holds Server-Sent Events pieces that were spread across two functions in subscribe.go: + the media type in sendHeaders, + the comment it writes to flush the headers, + the comment the loop sends as a keep-alive, and + the update serialization the loop passes to write. A heartbeat is not sent when the streamEncoder generates an empty heartbeat string. Some comments changed with the code they describe. TestSubscribeContentType checks for the media type the hub answers with It goes through SubscribeHandler, so it covers sendHeaders using the encoder's value and not just the value itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Refactors the subscription parsing, to accomodate multiple request parsers in preparation for Events Query. Request parsing has been moved out from `subscribeValues` in **subscribe.go** to `parseSubscribeRequest` in **parser.go**. `parseSubscribeRequest` also resolves lastEventID which was also previously handled by **subscribe.go**. The parsing of a request body is moved to **parser_urlencoded.go**. This will enable the reuse of the parser for Events Query. The malformed-body test has been moved out of **subscribe_test.go** to **parser_test.go**. Behaviour is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Mercure hub expects QUERY to be form-encoded, so a request naming another media type is answered with HTTP error code 415 and one naming none at all is answered with HTTP error code 400, as specified in RFC 10008, Section 2.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A subscription answers with Mercure-Last-Event-Id, the cursor a subscriber resumes from. CORS exposed only Link, so a browser using fetch could not read it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Implements the subscription data model of the HTTP Events Query Internet-Draft (draft-gupta-httpapi-events-query) as an opt-in alternative to EventSource, enabled with WithEventsQuery() or the `events_query` Caddy directive, and advertised as `mercure_events_query`. The implementation still uses URL form-encoding: but it does not take parameters from the URL and the body is checked for the `events` property. The response is still text/event-stream, as the draft does not impose a response media type. Nothing changes for Event Source, nor for a QUERY on a hub without the option. Co-Authored-By: Kévin Dunglas <kevin@dunglas.fr> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Accept-Query (RFC 10008) advertises the media type of the subscription requests sent using the QUERY method supported by the hub, currently just form-encoded. A hub with Events Query enabled derives the header field from directly from the list of registered parsers; the list will evolve automatically as new parsers are added. The field is exposed to cross-origin subscribers, which cannot otherwise read it. Co-Authored-By: Kévin Dunglas <kevin@dunglas.fr> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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. It is only sent when the subscription response intends to send updates. The field is exposed to cross-origin subscribers. Co-Authored-By: Kévin Dunglas <kevin@dunglas.fr> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The media type of the subscription response is driven by content negotiation. In particular, the negotiated media type selects the encoder to frame notifications sent from the hub. `text/event-stream` is the only response format currently implemented. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When processing an Events Query request, the parser reads the `duration` property of the `Events` request field, which states the period a subscriber wants updates for. The response controller uses it as guidance to determine the response duration — it may only shorten the period the hub would have served — and communicates it back with the same property on the response. The disconnection timer is armed on that stopping time rather than on the connection's write deadline, which a request deliberately leaves alone. A hub with no deadline of its own now stops when a subscription said to, instead of advertising a bound it would not have enforced. Events is allowed and exposed to cross-origin subscribers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The hub can read an application/events+json formatted request that expresses the Events Query data model as JSON. The request reuses the "url" member carrying the matcher parameters and "last_event_id" meber from the Mercure specification. The only operative difference is that the "events" property is used to ask for a stream of notifications as per Section 6 of draft-gupta-httpapi-events-query. That is, the parser decodes the same values a form-encoded request does, so one implementation validates both and the two cannot drift apart. Registering the parser is all it takes for the media type to be offered: Accept-Query is derived from the same list and now advertises application/events+json ahead of the form-encoded media type. It is only read by a hub with Events Query enabled. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Events Query notifications are sent in a multipart/digest response by default. Individual notifications are sent as message/rfc822, the default part format in a multipart/digest response (RFC 2046, Section 5.1.5). Each notification is a message with the event's fields as header fields and its data as the body, declaring a Content-Length so a payload containing the boundary cannot end the part early. An encoder is now built per connection rather than shared, a multipart one carrying a boundary of its own, so the registry holds constructors. The framing also gains a trailer: written on the clean exit paths only, with a write deadline of its own, since the response ends precisely because the connection's deadline arrived. The content negotiation added previously allows an Events Query subscriber to select between the Multipart Digest and Event Stream response formats. Event Stream response is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Publishers can declare the media type of the data payload in a `content_type` form field. The value is validated as a media type and rejected with a 400 otherwise, since it reaches the wire as a header field where a malformed value could inject others. It is stored with the update, so a transport serving history carries it too. An Events Query notification declares it as the `Content-Type` of its message, in place of the `text/plain; charset=utf-8` the hub assumes of data no publisher described. Nothing changes for Event Source: `text/event-stream` defines no field for a media type. Co-Authored-By: Kévin Dunglas <kevin@dunglas.fr> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
A more detailed and (hopefully) instructive Events Query implementation.
AI (Claude Opus 5) has been used to develop this code. I have checked verified all changes/additions to the code, but have not verified all the tests.
The implementation proceeds in three stages:
The only change to existing functionality is a subtle fixes to Accept and Content-Type handling that is now consistent with RFC9110 and RFC10008 respectively. Otherwise existing functionality is essentially untouched. Events Query is operated under a flag as designed by Kevin and is off by default.
There are no docs in this PR; as it is this took a lot of time.