Skip to main content
doc0blade47/honodocumentation home
Sign in

WebSocket Support

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

WebSocket support in Hono provides a unified, cross-runtime abstraction for establishing, managing, and interacting with persistent bidirectional socket connections. Built around the core WSContext class and defineWebSocketHelper builder, the helper architecture normalizes event lifecycles such as onOpen, onMessage, onClose, and onError alongside message sending and connection termination across different server environments. Runtime-specific adapters like Cloudflare Workers and Bun handle low-level handshake protocols, socket pair generation, and event bridge delegation seamlessly.

Sources: src/adapter/bun/websocket.ts:33-102, src/adapter/cloudflare-workers/websocket.ts:5-52, src/helper/websocket/index.ts:14-91, src/helper/websocket/index.ts:111-139

WebSocket helper architecture and interfaces

Core Contracts and Event Types

The WebSocket helper exposes public TypeScript interfaces and type definitions that govern connection upgrades, message payloads, and socket event callbacks. The WSEvents interface defines four optional lifecycle listeners parameterized by a generic type T representing the raw underlying socket reference: onOpen, onMessage, onClose, and onError. Each listener receives runtime event objects alongside a normalized WSContext<T> instance.

export interface WSEvents<T = unknown> {
  onOpen?: (evt: Event, ws: WSContext<T>) => void
  onMessage?: (evt: MessageEvent<WSMessageReceive>, ws: WSContext<T>) => void
  onClose?: (evt: CloseEvent, ws: WSContext<T>) => void
  onError?: (evt: Event, ws: WSContext<T>) => void
}

Sources: src/helper/websocket/index.ts:14-19

Connection readiness is tracked using the numeric WSReadyState union type corresponding to standard WebSocket lifecycle states. Message payloads accepted during transmission or received via events conform to WSMessageReceive.

ReadyState ValueState NameDescription
0CONNECTINGThe connection has not yet been established.
1OPENThe connection is open and ready to communicate.
2CLOSINGThe connection is in the process of closing.
3CLOSEDThe connection is closed or could not be opened.

Sources: src/helper/websocket/index.ts:43-45, src/helper/websocket/index.ts:93

Note

WSReadyState maps directly to standard browser and runtime WebSocket ready states (0 through 3), allowing runtime-agnostic status checking inside WSContext.

Sources: src/helper/websocket/index.ts:43-45, src/helper/websocket/index.ts:83-85

Factory Signatures and Upgrade Overloads

The UpgradeWebSocket type signature supports two distinct calling conventions: middleware creation for route handlers, and direct execution taking an active Hono Context. The defineWebSocketHelper factory function implements both overloads, dynamically returning either a Hono MiddlewareHandler or a promise resolving to an upgraded HTTP Response.

export interface UpgradeWebSocket<T = unknown, U = any, _WSEvents = WSEvents<T>> {
  (
    createEvents: (c: Context) => _WSEvents | Promise<_WSEvents>,
    options?: U
  ): MiddlewareHandler<
    any,
    string,
    {
      outputFormat: 'ws'
    }
  >
  (
    c: Context,
    events: _WSEvents,
    options?: U
  ): Promise<Response & TypedResponse<{}, StatusCode, 'ws'>>
}

Sources: src/helper/websocket/index.ts:24-40

When defineWebSocketHelper receives a factory function as its first argument, it inspects arguments at runtime to return a middleware handler that awaits createEvents(c) before executing the underlying upgrade handler and calling next(). When invoked directly with a Context and event map, it executes the handler immediately and throws an error if the upgrade fails.

export const defineWebSocketHelper = <T = unknown, U = any>(
  handler: WebSocketHelperDefineHandler<T, U>
): UpgradeWebSocket<T, U> => {
  return ((
    ...args:
      | [createEvents: (c: Context) => WSEvents<T> | Promise<WSEvents<T>>, options?: U]
      | [c: Context, events: WSEvents<T>, options?: U]
  ) => {
    if (typeof args[0] === 'function') {
      const [createEvents, options] = args
      return async function upgradeWebSocket(c, next) {
        const events = await createEvents(c)
        const result = await handler(c, events, options as U)
        if (result) {
          return result
        }
        await next()
      }
    } else {
      const [c, events, options] = args as [c: Context, events: WSEvents<T>, options?: U]
      return (async () => {
        const upgraded = await handler(c, events, options as U)
        if (!upgraded) {
          throw new Error('Failed to upgrade WebSocket')
        }
        return upgraded
      })()
    }
  }) as UpgradeWebSocket<T, U>

Sources: src/helper/websocket/index.ts:111-139

Warning

Passing an invalid argument signature or failing to return an upgrade response from the handler function causes direct invocations to reject with a Failed to upgrade WebSocket error.

Sources: src/helper/websocket/index.ts:133-135

Normalized context and event abstraction

WSContext Initialization and Properties

The WSContext<T> class wraps runtime-specific socket implementations into a unified control interface. Constructed via a WSContextInit<T> descriptor, WSContext exposes properties for the underlying raw socket reference (raw), the connection URL parsed as a URL object or null, the negotiated subprotocol or null, and the active binary type defaulting to 'arraybuffer'.

export class WSContext<T = unknown> {
  #init: WSContextInit<T>
  constructor(init: WSContextInit<T>) {
    this.#init = init
    this.raw = init.raw
    this.url = init.url ? new URL(init.url) : null
    this.protocol = init.protocol ?? null
  }
  send(source: string | ArrayBuffer | Uint8Array<ArrayBuffer>, options?: SendOptions): void {
    this.#init.send(source, options ?? {})
  }
  raw?: T
  binaryType: BinaryType = 'arraybuffer'
  get readyState(): WSReadyState {
    return this.#init.readyState
  }
  url: URL | null
  protocol: string | null
  close(code?: number, reason?: string) {
    this.#init.close(code, reason)
  }
}

Sources: src/helper/websocket/index.ts:70-91

Property / MethodTypeDescription
rawT | undefinedThe runtime-specific underlying socket instance.
readyStateWSReadyStateGetter delegating to initialization ready state (0 to 3).
urlURL | nullParsed connection URL.
protocolstring | nullNegotiated subprotocol string.
binaryTypeBinaryTypeConfigured binary data format, defaulting to 'arraybuffer'.
send(source, options?)(source: string | ArrayBuffer | Uint8Array, options?: SendOptions) => voidTransmits text or binary data with optional compression.
close(code?, reason?)(code?: number, reason?: string) => voidTerminates the WebSocket connection with optional code and reason.

Sources: src/helper/websocket/index.ts:70-91

Note

WSContext lazily wraps init.url into a JavaScript URL instance during construction if present, converting raw string endpoints into structured URI objects automatically.

Sources: src/helper/websocket/index.ts:71-76

Event Normalization and Message Wrapping

Incoming raw payloads are normalized into standard browser-compatible event constructs. The helper provides the createWSMessageEvent utility function, which wraps received data streams conforming to WSMessageReceive into standard MessageEvent objects destined for onMessage listeners.

export type WSMessageReceive = string | Blob | ArrayBufferLike
 
export const createWSMessageEvent = (source: WSMessageReceive): MessageEvent<WSMessageReceive> => {
  return new MessageEvent<WSMessageReceive>('message', {
    data: source,
  })
}

Sources: src/helper/websocket/index.ts:93-99

export type WSMessageReceive = string | Blob | ArrayBufferLike
 
export const createWSMessageEvent = (source: WSMessageReceive): MessageEvent<WSMessageReceive> => {
  return new MessageEvent<WSMessageReceive>('message', {
    data: source,
  })
}

Sources: src/helper/websocket/index.ts:93-99

Cloudflare Workers WebSocket upgrade adapter

WebSocketPair Instantiation and Response Handshaking

The Cloudflare Workers upgrade adapter utilizes defineWebSocketHelper to process incoming HTTP requests and establish WebSocket connections. It validates the request by checking if the Upgrade header equals 'websocket', returning early if the precondition fails.

> = defineWebSocketHelper(async (c, events) => {
  const upgradeHeader = c.req.header('Upgrade')
  if (upgradeHeader !== 'websocket') {
    return
  }

Sources: src/adapter/cloudflare-workers/websocket.ts:10-14

Once validated, the adapter instantiates a global WebSocketPair to yield a client-server socket pair, mapping webSocketPair[0] to the client socket and webSocketPair[1] to the worker server socket. It wraps the server socket inside a WSContext<WebSocket> instance that delegates lifecycle operations such as close, send, readyState, and URL parsing directly to the underlying Workers socket.

// @ts-expect-error WebSocketPair is not typed
const webSocketPair = new WebSocketPair()
const client: WebSocket = webSocketPair[0]
const server: WebSocket = webSocketPair[1]
 
const wsContext = new WSContext<WebSocket>({
  close: (code, reason) => server.close(code, reason),
  get protocol() {
    return server.protocol
  },
  raw: server,
  get readyState() {
    return server.readyState as WSReadyState
  },
  url: server.url ? new URL(server.url) : null,
  send: (source) => server.send(source),
})

Sources: src/adapter/cloudflare-workers/websocket.ts:16-32

Note

Cloudflare Workers do not support the 'open' event lifecycle hook, so event listener attachments are restricted to close, message, and error event types.

Sources: src/adapter/cloudflare-workers/websocket.ts:34-44

The server socket accepts incoming connections via server.accept?.(), and the adapter returns a custom Response object with status 101 and the client socket attached under the webSocket property.

// @ts-expect-error - server.accept is not typed
server.accept?.()
return new Response(null, {
  status: 101,
  // @ts-expect-error - webSocket is not typed
  webSocket: client,
})

Sources: src/adapter/cloudflare-workers/websocket.ts:46-52

Runtime Testing Harness

The workerd runtime test suite configures a Hono application instance to verify environment bindings, runtime identification via getRuntimeKey(), and WebSocket echo behavior under Cloudflare Workers. The /ws route invokes upgradeWebSocket with a callback factory returning an onMessage handler that echoes received event data back to the client using ws.send(event.data as string).

app.get(
  '/ws',
  upgradeWebSocket(() => {
    return {
      onMessage(event, ws) {
        ws.send(event.data as string)
      },
    }
  })
)

Sources: runtime-tests/workerd/index.ts:15-24

Bun server WebSocket integration

Native Server Upgrade Delegation

The Bun WebSocket adapter leverages Bun's native HTTP server upgrade capabilities through upgradeWebSocket, which is defined using defineWebSocketHelper. It retrieves the underlying Bun server instance via getBunServer, throwing a TypeError if the server instance is missing from the environment.

export const upgradeWebSocket: UpgradeWebSocket<any> = defineWebSocketHelper((c, events) => {
  const server = getBunServer<{
    upgrade<T>(
      req: Request,
      options?: {
        data: T
      }
    ): boolean
  }>(c)
 
  if (!server) {
    throw new TypeError('env has to include the 2nd argument of fetch.')
  }

Sources: src/adapter/bun/websocket.ts:48-60

Once the server instance is acquired, the adapter calls server.upgrade on the raw request object (c.req.raw), passing custom BunWebSocketData containing registered event listeners, the request URL, and protocol details. If the upgrade succeeds, it returns an empty Response(null) to complete the handshake.

  const upgradeResult = server.upgrade<BunWebSocketData>(c.req.raw, {
    data: {
      events,
      url: new URL(c.req.url),
      protocol: c.req.url,
    },
  })
  if (upgradeResult) {
    return new Response(null)
  }
  return // failed
})

Sources: src/adapter/bun/websocket.ts:61-72

Warning

Calling upgradeWebSocket without supplying the Bun server instance in the second argument of fetch throws a TypeError.

Sources: src/adapter/bun/websocket.ts:58-60

Context Registry and Event Dispatch Bridge

The Bun adapter implements a BunWebSocketHandler interface that bridges native socket lifecycle hooks to Hono's unified WebSocket event handlers. The handler defines three core callback methods (open, close, and message) which interface directly with BunServerWebSocket instances.

Callback MethodParametersAction
openws: BunServerWebSocket<T>Invokes onOpen with an Event and a newly created WSContext.
closews: BunServerWebSocket<T>, code?: number, reason?: stringDispatches onClose with a CloseEvent containing status codes and reasons.
messagews: BunServerWebSocket<T>, message: string | { buffer: ArrayBufferLike }Normalizes incoming strings or buffer objects and triggers onMessage.

Sources: src/adapter/bun/websocket.ts:15-19, src/adapter/bun/websocket.ts:74-102

The createWSContext helper builds the runtime WSContext bridge around a BunServerWebSocket<BunWebSocketData>, mapping transmission calls, connection readiness states, URL endpoints, sub-protocols, and closure methods back to the native socket.

export const createWSContext = (ws: BunServerWebSocket<BunWebSocketData>): WSContext => {
  return new WSContext({
    send: (source, options) => {
      ws.send(source, options?.compress)
    },
    raw: ws,
    readyState: ws.readyState,
    url: ws.data.url,
    protocol: ws.data.protocol,
    close(code, reason) {
      ws.close(code, reason)
    },
  })
}

Sources: src/adapter/bun/websocket.ts:33-46

Was this page helpful?