WebSockets API
March 29, 2026 ยท View on GitHub
This document describes the current Watson 7 WebSocket surface.
Scope
Current implementation scope:
- HTTP/1.1 WebSockets only
- Watson-owned handshake and routing
- Watson-owned
WebSocketSession - whole-message receive semantics
- session enumeration and disconnect APIs
Not currently implemented:
- HTTP/2 WebSockets
- HTTP/3 WebSockets
- public raw-socket access
- public subprotocol negotiation configuration
Route Registration
Enable WebSockets through WebserverSettings.WebSockets.Enable, then register routes with server.WebSocket(...).
using WatsonWebserver;
using WatsonWebserver.Core;
using WatsonWebserver.Core.WebSockets;
WebserverSettings settings = new WebserverSettings("127.0.0.1", 9000);
settings.WebSockets.Enable = true;
Webserver server = new Webserver(settings, DefaultRoute);
server.WebSocket("/chat", HandleSocketAsync);
server.WebSocket("/chat/{room}", HandleSocketAsync);
Handler shape:
Task HandleSocketAsync(HttpContextBase context, WebSocketSession session)
Same-Path HTTP And WebSocket Routing
Watson allows the same path to be registered for both HTTP and WebSocket handling.
server.Get("/chat", async req => new { Mode = "http" });
server.WebSocket("/chat", HandleSocketAsync);
Dispatch rule:
- ordinary HTTP requests follow normal HTTP route matching
- WebSocket upgrade requests are matched against websocket routes first
WebSocketSession
WebSocketSession is the Watson-owned session abstraction.
Useful members:
Guid Idbool IsConnectedWebSocketState StateWebSocketCloseStatus? CloseStatusstring CloseStatusDescriptionstring Subprotocolstring RemoteIpint RemotePortWebSocketRequestDescriptor Requestobject MetadataWebSocketSessionStatistics Statistics
Request metadata
session.Request retains reduced immutable handshake data:
PathNormalizedPathQueryHeadersRequestedVersionRequestedSubprotocolsRemoteIpRemotePort
Receive Semantics
Use ReceiveAsync() to receive a single whole message:
WebSocketMessage message = await session.ReceiveAsync(ctx.Token);
if (message != null && message.MessageType == WebSocketMessageType.Text)
{
await session.SendTextAsync("echo:" + message.Text, ctx.Token);
}
Use ReadMessagesAsync() for continuous consumption:
await foreach (WebSocketMessage message in session.ReadMessagesAsync(ctx.Token))
{
if (message.MessageType == WebSocketMessageType.Text)
{
await session.SendTextAsync("echo:" + message.Text, ctx.Token);
}
}
Behavior:
- receive is whole-message, not frame-level
- fragmented frames are reassembled before delivery
- only one active receive operation is allowed per session
- oversized messages are rejected according to
MaxMessageSize
Sending
Text:
await session.SendTextAsync("hello", ctx.Token);
Binary:
await session.SendBinaryAsync(bytes, ctx.Token);
await session.SendBinaryAsync(new ArraySegment<byte>(bytes), ctx.Token);
Behavior:
- sends are serialized per session
- counters are updated on successful send
Closing
await session.CloseAsync(WebSocketCloseStatus.NormalClosure, "done", ctx.Token);
Watson also closes sessions when:
- the route completes while still connected
- the server stops
- a route handler throws
- an oversized message is received
Server-Level Session APIs
List sessions:
IEnumerable<WebSocketSession> sessions = server.ListWebSocketSessions();
Check connectivity:
bool connected = server.IsWebSocketSessionConnected(guid);
Disconnect by id:
await server.DisconnectWebSocketSessionAsync(
guid,
WebSocketCloseStatus.NormalClosure,
"disconnect");
Lifecycle Events
Observability events are exposed on server.Events:
WebSocketSessionStartedWebSocketSessionEndedWebSocketHandshakeFailed
Example:
server.Events.WebSocketSessionStarted += (sender, args) =>
{
Console.WriteLine("WS started " + args.Session.Id + " " + args.Session.Request.Path);
};
Settings
WebSocket settings live under WebserverSettings.WebSockets.
Common settings:
EnableMaxMessageSizeReceiveBufferSizeCloseHandshakeTimeoutMsAllowClientSuppliedGuidClientGuidHeaderNameSupportedVersionsEnableHttp1
Current defaults:
Enable = falseMaxMessageSize = 16777216ReceiveBufferSize = 65536CloseHandshakeTimeoutMs = 5000AllowClientSuppliedGuid = falseClientGuidHeaderName = "x-guid"SupportedVersions = ["13"]EnableHttp1 = true
Limitations
- Current support is HTTP/1.1 only
- HTTP/2 and HTTP/3 WebSockets are planned follow-up work
- Public subprotocol negotiation support is not yet available
- No public raw
System.Net.WebSockets.WebSocketescape hatch exists