loading
Note:
A live endpoint is running This demo talks to /ws/echo on the demo server. It echoes text back with an echo: prefix, echoes binary frames back with every byte incremented (so a real round trip is distinguishable from keeping your own array), offers the butil-echo sub-protocol, and understands two commands - close to make the server close with an application code, and burst to flood the connection.

Support check

IsSupported

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

C#
@inject Bit.Butil.WebSocket webSocket

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

Open, send, receive

Open / SendText / WebSocketHandle

A page served over HTTPS may only open wss:// - a ws:// URL is blocked as mixed content before the connection is attempted, which is why Open returns null rather than reporting an error. Sending before the connection is established fails rather than queueing, so SendText returns false until onOpen has run.

@implements IAsyncDisposable
@inject Bit.Butil.WebSocket webSocket

@code {
    private WebSocketHandle? _socket;
    private readonly List<string> _messages = [];

    private async Task Open()
    {
        _socket = await webSocket.Open(
            "wss://example.com/ws/echo",
            onMessage: m => InvokeAsync(() =>
            {
                // m.IsBinary says which payload carries this frame
                _messages.Insert(0, m.IsBinary ? $"{m.Data!.Length} bytes" : m.Text!);
                StateHasChanged();
            }),
            protocols: ["butil-echo"],
            onOpen: (protocol, extensions) => InvokeAsync(StateHasChanged),
            onClose: c => InvokeAsync(StateHasChanged),
            onError: () => InvokeAsync(StateHasChanged));
    }

    // False until onOpen has run: sending before the connection is established fails rather than
    // queueing.
    private async Task Send() => await _socket!.SendText("hello");

    public async ValueTask DisposeAsync()
    {
        if (_socket is not null) await _socket.DisposeAsync();
    }
}
Live sample
Connection Not connected.
Received 0 frame(s)
socket output
Results will appear here when you interact with the samples.

Binary frames

SendBytes / WebSocketMessage.Data

The socket is opened in arraybuffer mode, so an incoming binary frame arrives as bytes rather than a Blob that would need an extra asynchronous read per frame. The echo endpoint increments every byte it sends back, so a round trip is visible in the values rather than only in the length.

Razor
@code {
    private WebSocketHandle? _socket;   // from webSocket.Open

    // The socket is opened in arraybuffer mode, so a binary frame arrives as bytes rather than as a
    // Blob that would need an extra asynchronous read per frame.
    private async Task Send() => await _socket!.SendBytes([1, 2, 3, 4]);

    // ... and arrives in the onMessage callback as:
    //     m.IsBinary == true, m.Data == [2, 3, 4, 5]
}
Live sample
Connection Not connected.
binary output
Results will appear here when you interact with the samples.

Negotiation and identity

GetProtocol / GetExtensions / GetUrl / GetState

Sub-protocols are offered most-preferred first and the server picks one; offering a protocol the server does not know fails the connection outright rather than falling back to none. Extensions are what the server agreed to - permessage-deflate, usually - and are not something a page chooses.

Razor
@code {
    private WebSocketHandle? _socket;   // from webSocket.Open

    private async Task Identify()
    {
        var protocol = await _socket!.GetProtocol();     // "butil-echo"
        var extensions = await _socket.GetExtensions();  // e.g. "permessage-deflate"
        var url = await _socket.GetUrl();
        var state = await _socket.GetState();            // Connecting / Open / Closing / Closed
    }
}
Live sample
Connection Not connected.
negotiation output
Results will appear here when you interact with the samples.

Back-pressure

GetBufferedAmount

Sending never blocks and never fails for being too fast, so a producer that outruns the connection just grows this number until the tab runs out of memory. It is the only back-pressure signal a browser socket has. Press Burst to make the server flood the connection, then read it while the frames are still arriving.

Razor
@code {
    private WebSocketHandle? _socket;   // from webSocket.Open

    private async Task Pressure()
    {
        await _socket!.SendText("burst");

        // The only back-pressure signal a browser socket has: sending never blocks and never fails
        // for being too fast, so a producer that outruns the connection just grows this number.
        var queued = await _socket.GetBufferedAmount();
        if (queued > 1_000_000) { /* stop producing until it drains */ }
    }
}
Live sample
Connection Not connected.
back-pressure output
Results will appear here when you interact with the samples.

Closing, and being closed

Close / onClose / WebSocketClose

Only 1000 and 3000-4999 may be sent from script; anything else is refused and the socket closes without a code instead. Code 1006 with WasClean false is the browser's stand-in for 'the connection dropped' - no close frame ever arrived, so there is nothing more specific to report. Nothing here reconnects: unlike EventSource, a WebSocket that drops stays dropped until you open another.

Razor
@code {
    private WebSocketHandle? _socket;   // from webSocket.Open

    // Only 1000 and 3000-4999 may be sent from script; anything else is refused and the socket
    // closes without a code instead.
    private async Task CloseFromHere() => await _socket!.Close(4000, "done for now");

    // ... or let the server close: this demo endpoint answers "close" with code 4001.
    private async Task AskServerToClose() => await _socket!.SendText("close");

    // Either way the same callback runs - the one given to Open:
    //     onClose: c => { /* c.Code, c.Reason, c.WasClean */ }
}
Live sample
Connection Not connected.
close output
Results will appear here when you interact with the samples.
Warning:
Always close what you open A socket that is never closed holds a connection open for as long as the page lives, and the server holds one with it. Dispose the handle - as a safety net, the WebSocket service closes any leaked socket when its scope is torn down.

API reference

Member
Signature
Description
IsSupported
ValueTask<bool> IsSupported()
True when the runtime exposes WebSocket. Returns default (false) during prerender/SSR instead of throwing.
Open
ValueTask<WebSocketHandle?> Open(string url, Action<WebSocketMessage> onMessage, string[]? protocols = null, Action<string, string>? onOpen = null, Action<WebSocketClose>? onClose = null, Action? onError = null)
Opens a ws:// or wss:// connection. Null when WebSocket is missing, the URL is malformed, or mixed content blocked it. onOpen receives the negotiated protocol and extensions.
DisposeAsync
ValueTask DisposeAsync()
On scope/circuit teardown, closes any socket whose handle was never disposed.
WebSocketHandle.SendText
ValueTask<bool> SendText(string text)
Sends a text frame. False when the socket is not open - including sending before the connection is established.
WebSocketHandle.SendBytes
ValueTask<bool> SendBytes(byte[] data)
Sends a binary frame. False when the socket is not open.
WebSocketHandle.GetState
ValueTask<WebSocketState> GetState()
Connecting, Open, Closing or Closed. Closed is also what a disposed handle reports.
WebSocketHandle.GetBufferedAmount
ValueTask<long> GetBufferedAmount()
Bytes queued by send but not yet on the wire - the only back-pressure signal a browser socket offers.
WebSocketHandle.GetProtocol
ValueTask<string> GetProtocol()
The sub-protocol the server chose, or an empty string when none was negotiated.
WebSocketHandle.GetExtensions
ValueTask<string> GetExtensions()
The extensions the server agreed to, such as permessage-deflate.
WebSocketHandle.GetUrl
ValueTask<string> GetUrl()
The absolute URL the socket resolved to.
WebSocketHandle.Close
ValueTask Close(int? code = null, string? reason = null)
Closes with a close frame. Only 1000 and 3000-4999 may be sent from script. The reason is at most 123 UTF-8 bytes.
WebSocketHandle.DisposeAsync
ValueTask DisposeAsync()
Closes the connection. Idempotent.
WebSocketMessage
record (bool IsBinary, string? Text, byte[]? Data)
One received frame. IsBinary says which payload carries it - a frame is text or binary, never both.
WebSocketClose
record (int Code, string Reason, bool WasClean)
How the connection ended. 1006 with WasClean false means it dropped without a closing handshake.
An unhandled error has occurred. Reload 🗙