Back to writing

How to setup hocospocus in Hono

Mar 21, 2025 @dhrjarun 3 min read

In Hocuspocus documentation, it has examples on how to set it up with express and koa, but I couldn’t find any example on integrating in with Hono js.

Hono has middleware and helpers to work with websocket but I had to modify the code a bit to make it work with Hocuspocus.

I removed upgradeWebSocket and added websocket method.

// hono-websocket.ts
import type { IncomingMessage } from "node:http";
import type { Server } from "node:http";
import type { Http2SecureServer, Http2Server } from "node:http2";
import type { Duplex } from "node:stream";
import type { Hono } from "hono";
import { createMiddleware } from "hono/factory";
import type { WebSocket } from "ws";
import { WebSocketServer } from "ws";

export interface NodeWebSocketInit {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  app: Hono<any, any, any>;
  baseUrl?: string | URL;
}

/**
 * Create WebSockets for Node.js
 * @param init Options
 * @returns NodeWebSocket
 */
export const createNodeWebSocket = (init: NodeWebSocketInit) => {
  const wss = new WebSocketServer({ noServer: true });
  const waiterMap = new Map<
    IncomingMessage,
    { resolve: (ws: WebSocket) => void; response: Response }
  >();

  wss.on("connection", (ws, request) => {
    const waiter = waiterMap.get(request);
    if (waiter) {
      waiter.resolve(ws);
      waiterMap.delete(request);
    }
  });

  const nodeUpgradeWebSocket = (
    request: IncomingMessage,
    response: Response,
  ) => {
    return new Promise<WebSocket>((resolve) => {
      waiterMap.set(request, { resolve, response });
    });
  };

  return {
    injectWebSocket(server: Server | Http2Server | Http2SecureServer) {
      server.on("upgrade", async (request, socket: Duplex, head) => {
        const url = new URL(
          request.url ?? "/",
          init.baseUrl ?? "http://localhost",
        );

        const headers = new Headers();
        for (const key in request.headers) {
          const value = request.headers[key];
          if (!value) {
            continue;
          }
          headers.append(key, Array.isArray(value) ? value[0] : value);
        }

        const response = await init.app.request(
          url,
          { headers: headers },
          { incoming: request, outgoing: undefined },
        );

        const waiter = waiterMap.get(request);
        if (!waiter || waiter.response !== response) {
          socket.end(
            "HTTP/1.1 400 Bad Request\r\n" +
              "Connection: close\r\n" +
              "Content-Length: 0\r\n" +
              "\r\n",
          );
          waiterMap.delete(request);
          return;
        }

        wss.handleUpgrade(request, socket, head, (ws) => {
          wss.emit("connection", ws, request);
        });
      });
    },

    websocket: () =>
      createMiddleware<{
        Variables: {
          ws: ((response: Response) => Promise<WebSocket>) | null;
        };
      }>(async (c, next) => {
        if (c.req.header("upgrade")?.toLowerCase() !== "websocket") {
          // Not websocket
          await next();
          c.set("ws", null);
          return;
        }

        const request = (c.env as any).incoming;

        const ws = async (response: Response) => {
          const ws = await nodeUpgradeWebSocket(request, response);
          return ws;
        };

        c.set("ws", ws);
        return next();
      }),
  };
};

Here is how to integrate hocupocus

import { Server } from "@hocuspocus/server"
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { createNodeWebSocket } from './hono-websocket'

const app = new Hono();

// Configure Hocuspocus
const hocuspocusServer = Server.configure({
  // …
});

const { injectWebSocket, websocket } = createNodeWebSocket({
  app,
});

app
  .get("/collaboration")
  .use(websocket())
  .use(async (c) => {
    const ws = c.get("ws");

    if (!ws) {
      throw new HTTPException(400, {
        message: "No websocket found",
      });
    }

    const appId = c.req.param("appId");

    // DO NOT use await here, it will block the response
    const response = new Response();
    ws(response)
      .then((websocket) => {
        if (!appId) {
          throw new HTTPException(400, {
            message: "No app ID provided",
          });
        }

        const request = (c.env as any).incoming;
        hocuspocusServer.handleConnection(websocket, request, {
          appId,
        });
      })
      .catch((error) => {
        console.error("Error handling connection", error);
      });

    return response;
  });

const server = serve(app)
injectWebSocket(server)
..............................................