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()],
});
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().

Full example:

websocket.mjs
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 API to create server-sent events using createEventStream(event) utility.

Example

server-sent-events.mjs
import { H3, serve, createEventStream } from "h3";

export const app = new H3();

app.get("/", (event) => {
  const eventStream = createEventStream(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.send();
});

serve(app);