Overview
Register WebSocket endpoints on host.ws with inline handlers or controller classes.
The WebSocket arc exposes two registration surfaces on host.ws, mirroring HTTP:
| Style | API | Docs |
|---|---|---|
| Inline handlers | host.ws.route(path, opts?) | Inline handlers |
| Controller classes | host.ws.controller(path, Cls) or host.ws.controller(path, opts, Cls) | Controller classes |
Both forms share route options: contract or loose descriptor fields (incoming, outgoing, params, query, subprotocols), state tokens, optional channel selector, and hibernate on Durable Object routes. The function form additionally accepts a named inject map; controllers use static deps instead.
Upgrade matching
Section titled “Upgrade matching”host.build() compiles registrations into a router. When a client requests a WebSocket upgrade, the runtime calls the arc’s upgrade entry, matches the pathname, parses declared params and query, and returns a live connection (or rejects the handshake when nothing matches).
On Node, unmatched paths get a 404 before the handshake completes. Invalid param or query values reject the upgrade at match time.
Lifecycle hooks
Section titled “Lifecycle hooks”Inline routes attach behaviors to the handle route() returns:
openruns once when the connection reaches OPEN (inbound messages wait if it is async)messageruns per inbound frame (validated whenincomingis declared)closeruns at terminal closeerrorruns on transport errors (a close still follows)
Controller classes override the same four methods on WebSocketControllerBase.
The upgrade hook
Section titled “The upgrade hook”Both forms can attach one pre-handshake hook with .upgrade() on the handle that route() or controller() returns. It is the only WebSocket moment with request context: the handler receives a WebSocketUpgrade view of the HTTP request (url plus case-insensitive header()) and a scope carrying typed input, injected services, and a state writer. It may be async, and it decides the upgrade by what it returns:
- Nothing: proceed. Values written to
scope.stateseed the connection’sws.state, so an identity verified at upgrade reaches every handler without being re-derived inopen. - A
FlareResponse: deny before the handshake. HTTP-speaking clients (curl, server-side clients) read the real status and body; on Node it is written on the raw socket in place of the101. - A
WebSocketRefusal(code, reason): accept, then immediately close. A denied handshake exposes nothing to browser JavaScript, but a close frame’s code and reason arrive in the browser’scloseevent, so this is the refusal a browser can read. The code must be1000or an application code in3000-4999, and the reason at most 123 bytes of UTF-8.
The hook resolves dependencies from the same per-connection container the handlers use, so a service it touches is an ordinary connection-scoped service. On a denial or refusal the container is disposed and the route’s channels and controller are never touched.
Upgrade hooks are front-door only. A hooked route on a Durable Object’s .ws arc fails host.build() with WS_UPGRADE_IN_DURABLE_OBJECT; gate a Durable Object’s WebSocket routes with the mount’s resolve handler instead, which already runs in the Worker.
Registration and examples: Inline handlers and Controller classes.
Durable Object routes
Section titled “Durable Object routes”On Cloudflare, a registered Durable Object carries its own .ws surface with these same route forms. Durable Objects covers registration, mounting, and upgrades.
Related
Section titled “Related”- Contracts:
socketContractandscope.input - Channels: subscribe and broadcast
- Connection state:
ws.statefor data that survives hibernation - The arc model: WebSocket arc in the host graph