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.
// 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.
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 thecrosswsproperty of the value returned for the upgrade request. A non-crosswserror or redirect response (any status that is not2xxor101, e.g.new Response("Unauthorized", { status: 401 })or a302) is sent back to the client and the handshake is rejected — handy for auth. A2xx(or101) response without.crosswsstill completes the handshake but with no hooks attached, so every message is silently dropped. Make sure the upgrade path returns hooks, either on aResponse: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 }(noResponseneeded). Anyheadersare 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 HTTPResponse's own headers are not applied to the handshake — use the{ crossws, headers }shortcut (or anupgradehook returning{ headers }) to set handshake headers. fetchruns per connection, so keep it side-effect-safe. Any auth, logging, metrics, or database work infetchnow executes once for every WebSocket upgrade. If a code path must run only for regular HTTP requests, branch on theupgradeheader first.- Throwing/rejecting fails the handshake. If
fetchthrows (or rejects) on the upgrade request, the WebSocket handshake is rejected rather than left hanging. Throw aResponseto control the rejection; otherwise the client sees a500. To reject an upgrade gracefully, prefer returning a non-2xxResponse(which is rendered to the client) over throwing.