WebSocket

v2.0a

WebSocket support with per-connection V8 isolate isolation and Cloudflare Workers compatible API.

Phase 23 — In Progress. Plans 01–04 complete (upgrade detection, relay, ws_messages loop). Plan 05 (WebSocketPair V8 binding) in progress.

Overview

NANO-RS supports WebSocket connections via HTTP Upgrade. Each WebSocket connection runs on a dedicated worker thread that owns a V8 isolate for the duration of the connection. When the connection closes, the isolate is recycled — ensuring no state leaks between clients.

JavaScript handlers use addEventListener (Cloudflare Workers pattern) to register message, close, and error callbacks before the first frame arrives.

Echo Server

handler.js
export default {
  async fetch(request) {
    // Reject non-WebSocket requests
    if (request.headers.get('Upgrade') !== 'websocket') {
      return new Response('Expected WebSocket upgrade', { status: 426 });
    }

    // Create linked pair: client goes in Response, server stays in handler
    const [client, server] = new WebSocketPair();

    server.addEventListener('message', (event) => {
      // event.data is string for text frames, ArrayBuffer for binary
      console.log('Received:', event.data);
      server.send(`Echo: ${event.data}`);
    });

    server.addEventListener('close', (event) => {
      console.log('Closed with code', event.code, event.reason);
    });

    server.addEventListener('error', (event) => {
      console.error('Error:', event.message);
    });

    // Must call accept() before frames are dispatched
    server.accept();

    return new Response(null, {
      status: 101,
      webSocket: client,
    });
  }
};

WebSocketPair

new WebSocketPair() returns two linked WebSocket objects as a destructured array.

const [client, server] = new WebSocketPair();

// client → pass in Response (sent to the browser)
// server → keep in handler (send/receive frames)

WebSocket Methods & Properties

Member Type Description
accept() method Accept connection. Must call before send().
send(data) method Send text (string) or binary (ArrayBuffer) frame.
close(code?, reason?) method Send Close frame. Default code 1000.
addEventListener(type, fn) method Register handler for message, close, or error.
readyState property 0 CONNECTING · 1 OPEN · 2 CLOSING · 3 CLOSED

Events

message

Fired when a frame arrives.

event.datastring for text frames · ArrayBuffer for binary frames

close

Fired when the connection closes.

event.codeWebSocket close code (1000 normal, 1001 going away, 1006 abnormal, …)
event.reasonClose reason string

error

Fired on transport error or OOM termination.

event.messageError description string

Limits

Configured in AppLimits / config.json:

Limit Default Config Key Behavior on Breach
Max concurrent connections 100 ws_max_connections 503 Service Unavailable
Max message size 32 MiB ws_max_message_bytes Close 1009 (message too large)
Idle timeout 60 000 ms ws_idle_timeout_ms Close 1001 (going away)
Per-message enforcement: Each message also triggers a CPU timeout guard (same limit as HTTP requests) and an OOM check. If the isolate heap exceeds its limit during message handling, the connection is closed with code 1011 (internal error).

Upgrade Flow

HTTP GET /path
  Upgrade: websocket
  Connection: Upgrade
        │
        ▼  router.rs — detect_ws_upgrade()
        │  axum 101 handshake
        ▼
  tokio relay task — WsChannels
        │  WsInbound (client→worker)
        │  WsOutbound (worker→client)
        ▼
  TenantPool::dispatch_ws()
        │  lazy-spawn WS worker thread
        ▼
  Worker thread:
    1. JS fetch() handler called
       (registers addEventListener callbacks)
    2. 'ws_messages loop starts
       recv_timeout(idle_timeout_ms) per frame
    3. Per frame:
       • CpuTimeoutGuard started
       • OOM check
       • JS event dispatched (MessageEvent / CloseEvent)
    4. On Close or Disconnect:
       set_ws_readystate(3)
       break 'requests  →  isolate recycled

Cloudflare Workers Compatibility

NANO-RS WebSocket matches the Cloudflare Workers WebSocket API:

  • new WebSocketPair() constructor
  • server.accept() required before sending
  • addEventListener for event registration
  • Response with webSocket property for upgrade response
Not yet implemented: Hibernatable WebSockets (Durable Objects pattern), cf.webSocket properties.