Resolver API

When integrating WebSockets with larger projects, it is often needed to dynamically route an incoming event to websocket hooks. crossws provides a very simple mechanism to do this using resolver API.

Resolver supports async results. This allows implementing lazy loading.
// https://crossws.h3.dev/adapters
import crossws from "crossws/adapters/<adapter>";

import { defineHooks } from "crossws";

const websocket = crossws({
  async resolve(req) {
    // TODO: Resolve hooks based on req.url, req.headers
    // You can return undefined in case there is no match
    return {
      /* resolved hooks */
    };
  },
});

If you need to change resolve function (for cases like handling HMR):

let resolveWebSocketHooks = (req) => /* ... */

const websocket = crossws({
  async resolve(req) {
    return resolveWebSocketHooks(req)
  },
});

// Update reference to `resolveWebSocketHooks` later.

Server plugin

When using the crossws/server plugin (srvx integration), resolve is optional. If you omit it, hooks are resolved by calling the server's own fetch handler and reading the crossws property off the returned Response (the srvx convention for attaching WebSocket hooks):

import { plugin as ws } from "crossws/server";

// No `resolve` needed — hooks come from the app's fetch response `.crossws`.
serve(app, { plugins: [ws()] });

Provide resolve only to customize routing, e.g. to resolve hooks without invoking the app:

serve(app, {
  plugins: [ws({ resolve: (req) => resolveWebSocketHooks(req) })],
});

resolve is invoked once per connection — its result is cached against the connection for the lifetime of the peer — so the app's fetch runs on connect, not on every message.

Passing inline hooks directly (ws({ message })) opts out of the fetch-based default: those hooks run with zero per-event overhead instead.

Making your fetch handler upgrade-aware

With the default resolver, your app's fetch handler is called with the raw WebSocket upgrade request on connect. Keep two things in mind:

  • Attach hooks via .crossws. Hooks are read from the crossws property of the value returned for the upgrade request. A non-crossws error or redirect response (any status that is not 2xx or 101, e.g. new Response("Unauthorized", { status: 401 }) or a 302) is sent back to the client and the handshake is rejected — handy for auth. A 2xx (or 101) response without .crossws still completes the handshake but with no hooks attached, so every message is silently dropped. Make sure the upgrade path returns hooks, either on a Response:
    import { defineHooks } from "crossws";
    
    const hooks = defineHooks({
      message(peer, message) {
        peer.send(message.text());
      },
    });
    
    function fetch(req) {
      if (req.headers.get("upgrade") === "websocket") {
        const res = new Response(null);
        res.crossws = hooks; // opt this connection into WebSocket handling
        return res;
      }
      return new Response("Hello!"); // normal HTTP response
    }
    

    …or, as a shortcut for the upgrade path, a plain object { crossws, headers } (no Response needed). Any headers are sent on the WebSocket handshake response:
    function fetch(req) {
      if (req.headers.get("upgrade") === "websocket") {
        return {
          crossws: hooks,
          headers: { "x-powered-by": "crossws" }, // optional handshake headers
        };
      }
      return new Response("Hello!"); // normal HTTP response — must be a Response
    }
    

    !NOTE The plain-object form is only for upgrade requests. Non-upgrade paths must still return a real Response. An HTTP Response's own headers are not applied to the handshake — use the { crossws, headers } shortcut (or an upgrade hook returning { headers }) to set handshake headers.

  • fetch runs per connection, so keep it side-effect-safe. Any auth, logging, metrics, or database work in fetch now executes once for every WebSocket upgrade. If a code path must run only for regular HTTP requests, branch on the upgrade header first.
  • Throwing/rejecting fails the handshake. If fetch throws (or rejects) on the upgrade request, the WebSocket handshake is rejected rather than left hanging. Throw a Response to control the rejection; otherwise the client sees a 500. To reject an upgrade gracefully, prefer returning a non-2xx Response (which is rendered to the client) over throwing.