loading
Note:
A live endpoint is running This demo connects to /sse/ticks on the demo server, which emits one event a second - every third one named heartbeat rather than unnamed, so both listener kinds are exercised. Each event carries an id, which is what lets a reconnect resume the sequence instead of restarting it.

Support check

IsSupported

Returns true when the runtime exposes EventSource. Supported by every current engine. During prerender/SSR the check returns false rather than throwing, so defer it to OnAfterRenderAsync.

C#
@inject Bit.Butil.EventSource eventSource

var supported = await eventSource.IsSupported();
Live sample
support check output
Results will appear here when you interact with the samples.

Open a stream

Open / EventSourceHandle

Named events never reach the default message listener, so anything the server names has to be declared in eventNames to be seen at all. The onError callback's argument distinguishes 'the browser is about to retry' (false) from 'this stream is finished' (true) - the browser reconnects on its own by default, so a transient failure is not something you have to handle.

@implements IAsyncDisposable
@inject Bit.Butil.EventSource eventSource

@code {
    private EventSourceHandle? _stream;
    private readonly List<ServerSentEvent> _events = [];

    private async Task Open()
    {
        _stream = await eventSource.Open(
            "/sse/ticks",
            onMessage: e => InvokeAsync(() =>
            {
                // e.EventName is "message" for unnamed events, the server's name otherwise
                // e.Data is always text - deserialize it yourself
                // e.LastEventId is what the browser replays on reconnect
                _events.Insert(0, e);
                StateHasChanged();
            }),
            // Named events never reach the default listener, so anything the server names has to be
            // declared here to be seen at all.
            eventNames: ["heartbeat"],
            onOpen: () => InvokeAsync(StateHasChanged),
            onError: fatal => InvokeAsync(StateHasChanged));
    }

    // Closing is what stops the browser reconnecting: an EventSource that is never closed keeps
    // retrying on the browser's own schedule for as long as the page lives.
    public async ValueTask DisposeAsync()
    {
        if (_stream is not null) await _stream.DisposeAsync();
    }
}
Live sample
Connection Not connected.
Received 0 event(s)
stream output
Results will appear here when you interact with the samples.

Reconnection is free

EventSourceHandle.GetState

Drop the server (or your network) while the stream is open and the browser reconnects by itself, sending the last id it saw as a Last-Event-ID header. The demo endpoint reads that header and carries on counting rather than starting from one - which is the whole point of ids. GetState reports Connecting during the gap, which is a normal mid-stream state rather than a failure.

Razor
@code {
    private EventSourceHandle? _stream;   // from eventSource.Open

    private async Task ReadState()
    {
        var state = await _stream!.GetState();
        // EventSourceState.Connecting - dropped, browser is retrying
        // EventSourceState.Open       - delivering
        // EventSourceState.Closed     - finished for good, no retry
    }
}
Live sample
Try it Open this section's own stream, then stop the demo server for a few seconds and start it again - the tick numbers continue rather than resetting.
Latest tick (nothing yet)
reconnect output
Results will appear here when you interact with the samples.
Warning:
Always close what you open An EventSource that is never closed keeps reconnecting on the browser's own schedule for as long as the page lives. Dispose the handle - as a safety net, the EventSource service closes any leaked stream when its scope is torn down.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes EventSource. Returns default (false) during prerender/SSR instead of throwing.
Open
ValueTask<EventSourceHandle?> Open(string url, Action<ServerSentEvent> onMessage, string[]? eventNames = null, Action? onOpen = null, Action<bool>? onError = null, bool withCredentials = false)
Opens a stream. Null when EventSource is missing or the URL is malformed. onError's argument is true only when the stream is finished for good.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, closes any stream whose handle was never disposed.
EventSourceHandle.GetState
ValueTask<EventSourceState> GetState()
Connecting, Open or Closed. Connecting mid-stream means the browser is retrying by itself.
EventSourceHandle.DisposeAsync
ValueTask DisposeAsync()
Closes the connection and stops the browser reconnecting. Idempotent.
ServerSentEvent
record (string EventName, string Data, string LastEventId)
EventName is "message" for unnamed events. Data is always text. LastEventId is what the browser replays as Last-Event-ID on reconnect.
An unhandled error has occurred. Reload 🗙