WebSockets
H3 has built-in utilities for cross platform WebSocket and Server-Sent Events.
You can add cross platform WebSocket support to H3 servers using 🔌 CrossWS.
#Usage
WebSocket handlers can be defined using the defineWebSocketHandler() utility and registered to any route like event handlers.
You need to register CrossWS as a server plugin in the serve function. The plugin resolves the correct hooks from your matched route automatically.
import { H3, serve, defineWebSocketHandler } from "h3";
import { plugin as ws } from "crossws/server";
const app = new H3();
app.get("/_ws", defineWebSocketHandler({ message: console.log }));
serve(app, {
plugins: [ws()],
});Note
Passing a custom resolve to ws() is only needed to resolve hooks yourself (for example without invoking the app). By default, CrossWS calls the app's fetch handler and reads the hooks attached by defineWebSocketHandler().
defineWebSocketHandler() attaches the hooks to the request (CrossWS reads them back with getWebSocketHooks(request), keyed by Symbol.for("crossws.hooks")), and answers the upgrade request with 426 Upgrade Required for anything that is not a WebSocket client.
They are attached to the request rather than to that response because a Response gets rebuilt on its way out of an app — merging a header staged by a route rule or CORS middleware, or any middleware doing new Response(res.body, res) — and a rebuilt response carries none of the original's own properties. The response also exposes the hooks as res.crossws for convenience, but only when nothing rebuilt it; if you write a custom resolve, read the request instead:
import { getWebSocketHooks } from "crossws";
serve(app, {
plugins: [ws({ resolve: (req) => app.fetch(req).then(() => getWebSocketHooks(req)) })],
});Full example:
import { H3, serve, html, defineWebSocketHandler } from "h3";
import { plugin as ws } from "crossws/server";
export const app = new H3();
// A minimal self-contained WebSocket playground served for plain HTTP requests.
const playground = html`<!doctype html>
<title>H3 WebSocket Playground</title>
<h1>H3 WebSocket Playground</h1>
<form id="form">
<input id="input" placeholder="Type a message..." autocomplete="off" autofocus />
<button type="submit">Send</button>
</form>
<div id="log"></div>
<script type="module">
const log = (msg) => {
const line = document.createElement("div");
line.textContent = msg;
document.getElementById("log").append(line);
};
const url = location.href.replace(/^http/, "ws");
const ws = new WebSocket(url);
ws.addEventListener("open", () => log("[open] connected to " + url));
ws.addEventListener("message", (e) => log("[message] " + e.data));
ws.addEventListener("close", () => log("[close] disconnected"));
document.getElementById("form").addEventListener("submit", (e) => {
e.preventDefault();
const input = document.getElementById("input");
ws.send(input.value);
input.value = "";
});
</script>`;
// A single route serves both the playground page (plain HTTP) and the
// WebSocket endpoint (upgrade requests). The page connects back to itself.
app.get(
"/",
defineWebSocketHandler(
{
open(peer) {
console.log("[open]", peer);
// Send welcome to the new client
peer.send("Welcome to the server!");
// Join new client to the "chat" channel
peer.subscribe("chat");
// Notify every other connected client
peer.publish("chat", `[system] ${peer} joined!`);
},
message(peer, message) {
console.log("[message]", peer);
if (message.text() === "ping") {
// Reply to the client with a ping response
peer.send("pong");
return;
}
// The server re-broadcasts incoming messages to everyone
peer.publish("chat", `[${peer}] ${message}`);
// Echo the message back to the sender
peer.send(message);
},
close(peer) {
console.log("[close]", peer);
peer.publish("chat", `[system] ${peer} has left the chat!`);
peer.unsubscribe("chat");
},
},
// Non-upgrade requests get the playground page.
() => playground,
),
);
serve(app, {
plugins: [ws()],
});#Handling HTTP requests
By default, a WebSocket route responds with 426 Upgrade Required to any request that is not a WebSocket upgrade.
You can pass an optional HTTP handler as the second argument to defineWebSocketHandler() to serve regular (non-upgrade) requests on the same route. WebSocket upgrade requests still go to the hooks.
app.get(
"/_ws",
defineWebSocketHandler(
{ message: (peer, message) => peer.send(message.text()) },
() => "Send a WebSocket upgrade request to connect.",
),
);#Server-Sent Events (SSE)
As an alternative to WebSockets, you can use Server-sent events.
H3 has a built-in EventStream class to create server-sent events. Construct it directly with new EventStream(event) and return it from a handler.
#Example
import { H3, serve, EventStream } from "h3";
export const app = new H3();
app.get("/", (event) => {
const eventStream = new EventStream(event);
// Send a message every second
const interval = setInterval(async () => {
await eventStream.push("Hello world");
}, 1000);
// cleanup the interval when the connection is terminated or the writer is closed
eventStream.onClosed(() => {
console.log("Connection closed");
clearInterval(interval);
});
return eventStream;
});
serve(app);