Skip to content

Add EventSource.fromReadableStream()#12272

Open
lachlanhunt wants to merge 1 commit intowhatwg:mainfrom
lachlanhunt:eventsource-fromreadablestream
Open

Add EventSource.fromReadableStream()#12272
lachlanhunt wants to merge 1 commit intowhatwg:mainfrom
lachlanhunt:eventsource-fromreadablestream

Conversation

@lachlanhunt
Copy link

@lachlanhunt lachlanhunt commented Mar 17, 2026

Allows developers use any ReadableStream source, such as from the fetch() API, while using EventSource for event-stream parsing.

This separates networking concerns from parsing, enabling use cases like custom authentication or other headers, and allows application developers to handle their own reconnection behaviour.

The lastEventId and reconnectionTime getters are added for use by applications in their own reconnection flows.

The about:event-stream URL is defined for use as the URL for EventSource instances created from a ReadableStream.

Fixes #2177

(See WHATWG Working Mode: Changes for more details.)


/infrastructure.html ( diff )
/references.html ( diff )
/server-sent-events.html ( diff )
/urls-and-fetching.html ( diff )

Allows developers use any ReadableStream source, such as from
the fetch() API, while using EventSource for event-stream parsing.

This separates networking concerns from parsing, enabling use cases
like custom authentication or other headers, and allows application
developers to handle their own reconnection behaviour.

The lastEventId and reconnectionTime getters are added for use by
applications in their own reconnection flows.

The about:event-stream URL is defined for use as the URL for EventSource
instances created from a ReadableStream.
@lachlanhunt
Copy link
Author

These are some of the design decisions I made while writing this spec.

Where possible, I preferred to keep the changes as simple for implementers as possible, reusing existing algorithms, with the goal of making it as easy as possible to be implemented.

Defining fromReadableStream() as a static factory method, rather than overloading the constructor, keeps the API simple, following similar patterns in other APIs, and allows for easy feature detection and polyfilling.

Since there can't be any automatic reconnection and streams can't be reopened, it was easiest to have the connection fail when the stream ends. However, the custom reconnection use case can be handled by the application layer by using a TransformStream that can prevent the stream from unexpected closures. A non-normative example demonstrates this with response.body.pipeTo(ts.writable, { preventClose: true }) to keep the EventSource alive across multiple fetch responses.

Since the EventSource API required a URL both for the exposed url property and for the purpose of defining the origin of message events, I defined the about:event-stream URL. This avoids a breaking change of making the url property nullable, provides a clear signal that allows applications to see that the EventSource is from a ReadableStream. Like other about: URLs, it's an opaque origin that serialises to "null", and I didn't have to change the message dispatching algorithm to accommodate it.

The lastEventId getter was added as a convenience property. While applications could already obtain this from dispatched messages, having this allows them to simply read it from the source whenever they need to, without having to keep track of it themselves. It enables reconnection flows where the application needs to send Last-Event-ID header in subsequent fetch requests.

The reconnectionTime getter was added to allow applications to know how long they should wait before trying to reconnect. This was previously handled internally and not exposed to applications.

I considered different options for what this should return prior to the server sending its own retry field. Making it null in this case would force applications to check for null and handle their own default, and it would force implementations to keep track of the default reconnection time separately from the server-set value. The approach I settled on exposes the implementation defined default value (currently 5000ms for Gecko, 3000ms for Chrome/WebKit), and it's a very low entropy value for browser fingerprinting. The final alternative would be to have the spec define an explicit default value, if implementers agreed.

I considered what to do with the readyState value, such as making it immediately OPEN by default. However, while CONNECTING doesn't really make sense in the context of a ReadableStream, keeping it as the initial value and immediately triggering the algorithm to announce the connection, meant it doesn't require any special case handling for the states.

Invoking close() will cancel the underlying ReadableStream, which propagates through any piped streams (e.g., cancelling a fetch response body).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Setting headers for EventSource

1 participant