From 69243f7610b801fca3e8c381d5cd402931ccc541 Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Wed, 18 Feb 2026 15:12:40 +0530 Subject: [PATCH 1/4] feat: ws support --- README.md | 41 +++++++ bun.lock | 12 +- package.json | 6 +- src/index.ts | 1 + src/server.ts | 4 + src/types.ts | 3 + src/websocket.ts | 255 +++++++++++++++++++++++++++++++++++++++++ test/websocket.test.ts | 97 ++++++++++++++++ tsup.config.ts | 8 +- 9 files changed, 423 insertions(+), 4 deletions(-) create mode 100644 src/websocket.ts create mode 100644 test/websocket.test.ts diff --git a/README.md b/README.md index af0e931..b0936b3 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,32 @@ serve(app, (info) => { }) ``` +## WebSocket + +You can upgrade WebSocket connections with `upgradeWebSocket` from `@hono/node-server`. +To enable upgrade handling, pass `websocket: true` to `serve()` or `createAdaptorServer()` options. + +```ts +import { serve, upgradeWebSocket } from '@hono/node-server' +import { Hono } from 'hono' + +const app = new Hono() + +app.get( + '/ws', + upgradeWebSocket(() => ({ + onMessage(event, ws) { + ws.send(event.data) + }, + })) +) + +serve({ + fetch: app.fetch, + websocket: true, +}) +``` + For example, run it using `ts-node`. Then an HTTP server will be launched. The default port is `3000`. ```sh @@ -138,6 +164,21 @@ serve({ }) ``` +### `websocket` + +The default value is `false`. Set `true` when using `upgradeWebSocket`. + +```ts +import { serve, upgradeWebSocket } from '@hono/node-server' + +// ... + +serve({ + fetch: app.fetch, + websocket: true, +}) +``` + ## Middleware Most built-in middleware also works with Node.js. diff --git a/bun.lock b/bun.lock index 1a0df1c..8384dd0 100644 --- a/bun.lock +++ b/bun.lock @@ -4,14 +4,18 @@ "workspaces": { "": { "name": "@hono/node-server", + "dependencies": { + "ws": "^8.18.3", + }, "devDependencies": { "@hono/eslint-config": "^1.0.1", "@types/jest": "^29.5.3", "@types/node": "^20.10.0", "@types/supertest": "^2.0.12", + "@types/ws": "^8.18.1", "@whatwg-node/fetch": "^0.9.14", "eslint": "^9.10.0", - "hono": "^4.4.10", + "hono": "^4.11.9", "jest": "^29.6.1", "np": "^7.7.0", "prettier": "^3.2.4", @@ -325,6 +329,8 @@ "@types/supertest": ["@types/supertest@2.0.16", "", { "dependencies": { "@types/superagent": "*" } }, "sha512-6c2ogktZ06tr2ENoZivgm7YnprnhYE4ZoXGMY+oA7IuAf17M8FWvujXZGmxLv8y0PTyts4x5A+erSwVUFA8XSg=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], @@ -701,7 +707,7 @@ "hexoid": ["hexoid@1.0.0", "", {}, "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g=="], - "hono": ["hono@4.6.20", "", {}, "sha512-5qfNQeaIptMaJKyoJ6N/q4gIq0DBp2FCRaLNuUI3LlJKL4S37DY/rLL1uAxA4wrPB39tJ3s+f7kgI79O4ScSug=="], + "hono": ["hono@4.11.9", "", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="], "hosted-git-info": ["hosted-git-info@3.0.8", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw=="], @@ -1321,6 +1327,8 @@ "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "xdg-basedir": ["xdg-basedir@4.0.0", "", {}, "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], diff --git a/package.json b/package.json index 58fbc79..85fa74d 100644 --- a/package.json +++ b/package.json @@ -84,9 +84,10 @@ "@types/jest": "^29.5.3", "@types/node": "^20.10.0", "@types/supertest": "^2.0.12", + "@types/ws": "^8.18.1", "@whatwg-node/fetch": "^0.9.14", "eslint": "^9.10.0", - "hono": "^4.4.10", + "hono": "^4.11.9", "jest": "^29.6.1", "np": "^7.7.0", "prettier": "^3.2.4", @@ -96,6 +97,9 @@ "tsup": "^7.2.0", "typescript": "^5.3.2" }, + "dependencies": { + "ws": "^8.18.3" + }, "peerDependencies": { "hono": "^4" }, diff --git a/src/index.ts b/src/index.ts index dd9c3de..c083a0e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ export { serve, createAdaptorServer } from './server' +export { upgradeWebSocket } from './websocket' export { getRequestListener } from './listener' export { RequestError } from './request' export type { HttpBindings, Http2Bindings, ServerType } from './types' diff --git a/src/server.ts b/src/server.ts index 12b565b..ce2661f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,6 +2,7 @@ import { createServer as createServerHTTP } from 'node:http' import type { AddressInfo } from 'node:net' import { getRequestListener } from './listener' import type { Options, ServerType } from './types' +import { setupWebSocket } from './websocket' export const createAdaptorServer = (options: Options): ServerType => { const fetchCallback = options.fetch @@ -14,6 +15,9 @@ export const createAdaptorServer = (options: Options): ServerType => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const createServer: any = options.createServer || createServerHTTP const server: ServerType = createServer(options.serverOptions || {}, requestListener) + if (options.websocket) { + setupWebSocket(server, fetchCallback) + } return server } diff --git a/src/types.ts b/src/types.ts index ce4a357..1ef07b6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -30,6 +30,8 @@ export type Http2Bindings = { outgoing: Http2ServerResponse } +export type { UpgradeBindings } from './websocket' + export type FetchCallback = ( request: Request, env: HttpBindings | Http2Bindings @@ -73,6 +75,7 @@ export type Options = { autoCleanupIncoming?: boolean port?: number hostname?: string + websocket?: boolean } & ServerOptions export type CustomErrorHandler = (err: unknown) => void | Response | Promise diff --git a/src/websocket.ts b/src/websocket.ts new file mode 100644 index 0000000..72ab1b4 --- /dev/null +++ b/src/websocket.ts @@ -0,0 +1,255 @@ +import type { UpgradeWebSocket } from 'hono/ws' +import { defineWebSocketHelper, WSContext } from 'hono/ws' +import type { RawData, WebSocket } from 'ws' +import { WebSocketServer } from 'ws' +import type { IncomingMessage } from 'node:http' +import { STATUS_CODES } from 'node:http' +import type { Duplex } from 'node:stream' +import type { FetchCallback, ServerType } from './types' + +interface CloseEventInit extends EventInit { + code?: number + reason?: string + wasClean?: boolean +} + +/** + * @link https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent + */ +export const CloseEvent: typeof globalThis.CloseEvent = + globalThis.CloseEvent ?? + class extends Event { + #eventInitDict + + constructor(type: string, eventInitDict: CloseEventInit = {}) { + super(type, eventInitDict) + this.#eventInitDict = eventInitDict + } + + get wasClean(): boolean { + return this.#eventInitDict.wasClean ?? false + } + + get code(): number { + return this.#eventInitDict.code ?? 0 + } + + get reason(): string { + return this.#eventInitDict.reason ?? '' + } + } + +const generateConnectionSymbol = () => Symbol('connection') + +type WaitForWebSocket = (request: IncomingMessage, connectionSymbol: symbol) => Promise + +const CONNECTION_SYMBOL_KEY: unique symbol = Symbol('CONNECTION_SYMBOL_KEY') +const WAIT_FOR_WEBSOCKET_SYMBOL: unique symbol = Symbol('WAIT_FOR_WEBSOCKET_SYMBOL') + +export type UpgradeBindings = { + incoming: IncomingMessage + outgoing: undefined + wss: WebSocketServer + [CONNECTION_SYMBOL_KEY]?: symbol + [WAIT_FOR_WEBSOCKET_SYMBOL]?: WaitForWebSocket +} + +type UpgradeWebSocketOptions = { + onError: (err: unknown) => void +} + +const rejectUpgradeRequest = (socket: Duplex, status: number) => { + socket.end( + `HTTP/1.1 ${status.toString()} ${STATUS_CODES[status] ?? ''}\r\n` + + 'Connection: close\r\n' + + 'Content-Length: 0\r\n' + + '\r\n' + ) +} + +const createUpgradeRequest = (request: IncomingMessage): Request => { + const protocol = (request.socket as { encrypted?: boolean }).encrypted ? 'https' : 'http' + const url = new URL(request.url ?? '/', `${protocol}://${request.headers.host ?? '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) + } + return new Request(url, { + headers, + }) +} + +export const setupWebSocket = (server: ServerType, fetchCallback: FetchCallback): void => { + const waiterMap = new Map< + IncomingMessage, + { resolve: (ws: WebSocket) => void; connectionSymbol: symbol } + >() + const wss = new WebSocketServer({ noServer: true }) + + wss.on('connection', (ws, request) => { + const waiter = waiterMap.get(request) + if (waiter) { + waiter.resolve(ws) + waiterMap.delete(request) + } + }) + + const waitForWebSocket: WaitForWebSocket = (request, connectionSymbol) => { + return new Promise((resolve) => { + waiterMap.set(request, { resolve, connectionSymbol }) + }) + } + + server.on('upgrade', async (request, socket: Duplex, head) => { + if (request.headers.upgrade?.toLowerCase() !== 'websocket') { + return + } + + const env: UpgradeBindings = { + incoming: request, + outgoing: undefined, + wss, + [WAIT_FOR_WEBSOCKET_SYMBOL]: waitForWebSocket, + } + + let status = 400 + try { + const response = (await fetchCallback( + createUpgradeRequest(request), + env as unknown as Parameters[1] + )) as Response + if (response instanceof Response) { + status = response.status + } + } catch { + if (server.listenerCount('upgrade') === 1) { + rejectUpgradeRequest(socket, 500) + } + return + } + + const waiter = waiterMap.get(request) + + if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { + waiterMap.delete(request) + if (server.listenerCount('upgrade') === 1) { + rejectUpgradeRequest(socket, status) + } + return + } + + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request) + }) + }) + + server.on('close', () => { + wss.close() + }) +} + +export const upgradeWebSocket: UpgradeWebSocket = + defineWebSocketHelper(async (c, events, options) => { + if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') { + return + } + + const env = c.env as UpgradeBindings + const waitForWebSocket = env[WAIT_FOR_WEBSOCKET_SYMBOL] + + if (!waitForWebSocket || !env.incoming) { + return new Response(null, { status: 500 }) + } + + const connectionSymbol = generateConnectionSymbol() + env[CONNECTION_SYMBOL_KEY] = connectionSymbol + ;(async () => { + const ws = await waitForWebSocket(env.incoming, connectionSymbol) + + const messagesReceivedInStarting: [data: RawData, isBinary: boolean][] = [] + const bufferMessage = (data: RawData, isBinary: boolean) => { + messagesReceivedInStarting.push([data, isBinary]) + } + ws.on('message', bufferMessage) + + const ctx: WSContext = { + binaryType: 'arraybuffer', + close(code, reason) { + ws.close(code, reason) + }, + protocol: ws.protocol, + raw: ws, + get readyState() { + return ws.readyState + }, + send(source, opts) { + ws.send(source, { + compress: opts?.compress, + }) + }, + url: new URL(c.req.url), + } + + try { + events?.onOpen?.(new Event('open'), ctx) + } catch (e) { + ;(options?.onError ?? console.error)(e) + } + + const handleMessage = (data: RawData, isBinary: boolean) => { + const datas = Array.isArray(data) ? data : [data] + for (const data of datas) { + try { + events?.onMessage?.( + new MessageEvent('message', { + data: isBinary + ? data instanceof ArrayBuffer + ? data + : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) + : data.toString('utf-8'), + }), + ctx + ) + } catch (e) { + ;(options?.onError ?? console.error)(e) + } + } + } + + ws.off('message', bufferMessage) + for (const message of messagesReceivedInStarting) { + handleMessage(...message) + } + + ws.on('message', (data, isBinary) => { + handleMessage(data, isBinary) + }) + + ws.on('close', (code, reason) => { + try { + events?.onClose?.(new CloseEvent('close', { code, reason: reason.toString() }), ctx) + } catch (e) { + ;(options?.onError ?? console.error)(e) + } + }) + + ws.on('error', (error) => { + try { + events?.onError?.( + new ErrorEvent('error', { + error, + }), + ctx + ) + } catch (e) { + ;(options?.onError ?? console.error)(e) + } + }) + })() + + return new Response() + }) diff --git a/test/websocket.test.ts b/test/websocket.test.ts new file mode 100644 index 0000000..4a43847 --- /dev/null +++ b/test/websocket.test.ts @@ -0,0 +1,97 @@ +import { Hono } from 'hono' +import { WebSocket, WebSocketServer } from 'ws' +import type { AddressInfo } from 'node:net' +import { createAdaptorServer, upgradeWebSocket } from '../src' + +describe('WebSocket', () => { + const startServer = (app: Hono) => { + const server = createAdaptorServer({ fetch: app.fetch, websocket: true }) + return new Promise<{ server: ReturnType; address: AddressInfo }>( + (resolve) => { + server.listen(0, () => { + resolve({ server, address: server.address() as AddressInfo }) + }) + } + ) + } + + it('should connect with upgradeWebSocket without manual injection', async () => { + const app = new Hono() + + app.get( + '/ws', + upgradeWebSocket(() => ({ + onMessage(event, ws) { + ws.send(event.data as string) + }, + })) + ) + + const { server, address } = await startServer(app) + + try { + const ws = new WebSocket(`ws://127.0.0.1:${address.port}/ws`) + await new Promise((resolve, reject) => { + ws.once('open', () => { + ws.send('hello') + }) + ws.once('message', (data) => { + expect(data.toString()).toBe('hello') + resolve() + }) + ws.once('error', reject) + }) + ws.close() + } finally { + await new Promise((resolve) => server.close(() => resolve())) + } + }) + + it('should reject WebSocket upgrade when route is not upgraded', async () => { + const app = new Hono() + app.get('/ws', (c) => c.text('ok')) + + const { server, address } = await startServer(app) + + try { + const ws = new WebSocket(`ws://127.0.0.1:${address.port}/ws`) + await new Promise((resolve, reject) => { + ws.once('unexpected-response', (_, response) => { + expect(response.statusCode).toBe(200) + resolve() + }) + ws.once('open', () => reject(new Error('WebSocket must not be upgraded'))) + ws.once('error', () => resolve()) + }) + } finally { + await new Promise((resolve) => server.close(() => resolve())) + } + }) + + it('should not block other upgrade listeners', async () => { + const app = new Hono() + const { server, address } = await startServer(app) + const wss = new WebSocketServer({ noServer: true }) + + server.on('upgrade', (request, socket, head) => { + if (request.url !== '/custom') { + return + } + wss.handleUpgrade(request, socket, head, (ws) => { + wss.emit('connection', ws, request) + }) + }) + + try { + const ws = new WebSocket(`ws://127.0.0.1:${address.port}/custom`) + await new Promise((resolve, reject) => { + ws.once('open', resolve) + ws.once('error', reject) + }) + ws.close() + } finally { + wss.close() + await new Promise((resolve) => server.close(() => resolve())) + } + }) +}) diff --git a/tsup.config.ts b/tsup.config.ts index dcd44ca..5b37859 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,13 @@ import { defineConfig } from 'tsup' export default defineConfig({ - entry: ['./src/**/*.ts'], + entry: [ + './src/index.ts', + './src/serve-static.ts', + './src/vercel.ts', + './src/utils/*.ts', + './src/conninfo.ts', + ], format: ['esm', 'cjs'], dts: true, splitting: false, From e4018777d3d8c10e6f2ad40442f8d32aceb1d656 Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Thu, 5 Mar 2026 17:16:09 +0530 Subject: [PATCH 2/4] alternative: pass in your own server --- README.md | 12 ++++++++---- bun.lock | 5 ----- package.json | 3 --- src/server.ts | 6 ++++-- src/types.ts | 7 ++++--- src/websocket.ts | 14 +++++++++----- 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index b0936b3..71e4745 100644 --- a/README.md +++ b/README.md @@ -80,10 +80,11 @@ serve(app, (info) => { ## WebSocket You can upgrade WebSocket connections with `upgradeWebSocket` from `@hono/node-server`. -To enable upgrade handling, pass `websocket: true` to `serve()` or `createAdaptorServer()` options. +To enable this, install `ws` (and `@types/ws`) in your project, then create and provide a `WebSocketServer` as shown in the example below. ```ts import { serve, upgradeWebSocket } from '@hono/node-server' +import { WebsocketServer } from 'ws' import { Hono } from 'hono' const app = new Hono() @@ -97,9 +98,10 @@ app.get( })) ) +const wss = new WebSocketServer({ noServer: true }) // important to create with `noServer: true` serve({ fetch: app.fetch, - websocket: true, + websocket: { server: wss }, }) ``` @@ -166,16 +168,18 @@ serve({ ### `websocket` -The default value is `false`. Set `true` when using `upgradeWebSocket`. +provide a websocket server to enable websocket support. ```ts import { serve, upgradeWebSocket } from '@hono/node-server' +import { WebsocketServer } from 'ws' // ... +const wss = new WebSocketServer({ noServer: true }) serve({ fetch: app.fetch, - websocket: true, + websocket: { server: wss }, }) ``` diff --git a/bun.lock b/bun.lock index 8384dd0..f513528 100644 --- a/bun.lock +++ b/bun.lock @@ -4,9 +4,6 @@ "workspaces": { "": { "name": "@hono/node-server", - "dependencies": { - "ws": "^8.18.3", - }, "devDependencies": { "@hono/eslint-config": "^1.0.1", "@types/jest": "^29.5.3", @@ -1327,8 +1324,6 @@ "write-file-atomic": ["write-file-atomic@4.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" } }, "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg=="], - "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], - "xdg-basedir": ["xdg-basedir@4.0.0", "", {}, "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], diff --git a/package.json b/package.json index 85fa74d..2c29361 100644 --- a/package.json +++ b/package.json @@ -97,9 +97,6 @@ "tsup": "^7.2.0", "typescript": "^5.3.2" }, - "dependencies": { - "ws": "^8.18.3" - }, "peerDependencies": { "hono": "^4" }, diff --git a/src/server.ts b/src/server.ts index ce2661f..f4824c2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,8 +15,10 @@ export const createAdaptorServer = (options: Options): ServerType => { // eslint-disable-next-line @typescript-eslint/no-explicit-any const createServer: any = options.createServer || createServerHTTP const server: ServerType = createServer(options.serverOptions || {}, requestListener) - if (options.websocket) { - setupWebSocket(server, fetchCallback) + if (options.websocket && options.websocket.server) { + if (options.websocket.server.options.noServer !== true) + throw new Error('WebSocket server must be created with { noServer: true } option') + setupWebSocket({ server, fetchCallback, wss: options.websocket.server }) } return server } diff --git a/src/types.ts b/src/types.ts index 1ef07b6..d343808 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,7 @@ import type { createServer as createHttpsServer, ServerOptions as HttpsServerOptions, } from 'node:https' +import type { WebSocketServer } from 'ws' export type HttpBindings = { incoming: IncomingMessage @@ -30,8 +31,6 @@ export type Http2Bindings = { outgoing: Http2ServerResponse } -export type { UpgradeBindings } from './websocket' - export type FetchCallback = ( request: Request, env: HttpBindings | Http2Bindings @@ -75,7 +74,9 @@ export type Options = { autoCleanupIncoming?: boolean port?: number hostname?: string - websocket?: boolean + websocket?: { + server: WebSocketServer + } } & ServerOptions export type CustomErrorHandler = (err: unknown) => void | Response | Promise diff --git a/src/websocket.ts b/src/websocket.ts index 72ab1b4..ebcdd96 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,7 +1,6 @@ import type { UpgradeWebSocket } from 'hono/ws' import { defineWebSocketHelper, WSContext } from 'hono/ws' -import type { RawData, WebSocket } from 'ws' -import { WebSocketServer } from 'ws' +import type { RawData, WebSocket, WebSocketServer } from 'ws' import type { IncomingMessage } from 'node:http' import { STATUS_CODES } from 'node:http' import type { Duplex } from 'node:stream' @@ -83,13 +82,18 @@ const createUpgradeRequest = (request: IncomingMessage): Request => { }) } -export const setupWebSocket = (server: ServerType, fetchCallback: FetchCallback): void => { +export const setupWebSocket = (options: { + server: ServerType, + fetchCallback: FetchCallback, + wss: WebSocketServer +}): void => { + const { server, fetchCallback, wss } = options + const waiterMap = new Map< IncomingMessage, { resolve: (ws: WebSocket) => void; connectionSymbol: symbol } >() - const wss = new WebSocketServer({ noServer: true }) - + wss.on('connection', (ws, request) => { const waiter = waiterMap.get(request) if (waiter) { From cc90f9a5b8739b8f621a48eb10614ef4d4ce125f Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Fri, 20 Mar 2026 13:07:44 +0530 Subject: [PATCH 3/4] fix: add ws as devDep for tests --- bun.lock | 3 +++ package.json | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/bun.lock b/bun.lock index 2d22a4a..b6a95fb 100644 --- a/bun.lock +++ b/bun.lock @@ -19,6 +19,7 @@ "tsdown": "^0.20.3", "typescript": "^5.3.2", "vitest": "^4.0.18", + "ws": "^8.19.0", }, "peerDependencies": { "hono": "^4", @@ -1070,6 +1071,8 @@ "write-file-atomic": ["write-file-atomic@3.0.3", "", { "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", "signal-exit": "^3.0.2", "typedarray-to-buffer": "^3.1.5" } }, "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q=="], + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + "xdg-basedir": ["xdg-basedir@4.0.0", "", {}, "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q=="], "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], diff --git a/package.json b/package.json index 27db8a7..76c37fe 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,8 @@ "supertest": "^6.3.3", "tsdown": "^0.20.3", "typescript": "^5.3.2", - "vitest": "^4.0.18" + "vitest": "^4.0.18", + "ws": "^8.19.0" }, "peerDependencies": { "hono": "^4" From 6ea41a5a2a102fcc0031cad9e283426fa7c8717e Mon Sep 17 00:00:00 2001 From: BlankParticle Date: Fri, 20 Mar 2026 13:11:39 +0530 Subject: [PATCH 4/4] lint/format/tests --- src/server.ts | 2 +- src/types.ts | 2 +- src/url.ts | 1 - src/websocket.ts | 10 +++++----- test/websocket.test.ts | 3 ++- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/server.ts b/src/server.ts index f4824c2..1a3fad0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,7 +16,7 @@ export const createAdaptorServer = (options: Options): ServerType => { const createServer: any = options.createServer || createServerHTTP const server: ServerType = createServer(options.serverOptions || {}, requestListener) if (options.websocket && options.websocket.server) { - if (options.websocket.server.options.noServer !== true) + if (options.websocket.server.options.noServer !== true) throw new Error('WebSocket server must be created with { noServer: true } option') setupWebSocket({ server, fetchCallback, wss: options.websocket.server }) } diff --git a/src/types.ts b/src/types.ts index d343808..82b0d8a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +import type { WebSocketServer } from 'ws' import type { createServer, IncomingMessage, @@ -19,7 +20,6 @@ import type { createServer as createHttpsServer, ServerOptions as HttpsServerOptions, } from 'node:https' -import type { WebSocketServer } from 'ws' export type HttpBindings = { incoming: IncomingMessage diff --git a/src/url.ts b/src/url.ts index 7a3de3e..dee2bf9 100644 --- a/src/url.ts +++ b/src/url.ts @@ -41,7 +41,6 @@ for (let c = 0x61; c <= 0x7a; c++) { allowedRequestUrlChar[c] = 1 } ;(() => { - // eslint-disable-next-line quotes const chars = "-./:?#[]@!$&'()*+,;=~_" for (let i = 0; i < chars.length; i++) { allowedRequestUrlChar[chars.charCodeAt(i)] = 1 diff --git a/src/websocket.ts b/src/websocket.ts index ebcdd96..fe0cd35 100644 --- a/src/websocket.ts +++ b/src/websocket.ts @@ -1,5 +1,5 @@ -import type { UpgradeWebSocket } from 'hono/ws' -import { defineWebSocketHelper, WSContext } from 'hono/ws' +import type { UpgradeWebSocket, WSContext } from 'hono/ws' +import { defineWebSocketHelper } from 'hono/ws' import type { RawData, WebSocket, WebSocketServer } from 'ws' import type { IncomingMessage } from 'node:http' import { STATUS_CODES } from 'node:http' @@ -83,8 +83,8 @@ const createUpgradeRequest = (request: IncomingMessage): Request => { } export const setupWebSocket = (options: { - server: ServerType, - fetchCallback: FetchCallback, + server: ServerType + fetchCallback: FetchCallback wss: WebSocketServer }): void => { const { server, fetchCallback, wss } = options @@ -93,7 +93,7 @@ export const setupWebSocket = (options: { IncomingMessage, { resolve: (ws: WebSocket) => void; connectionSymbol: symbol } >() - + wss.on('connection', (ws, request) => { const waiter = waiterMap.get(request) if (waiter) { diff --git a/test/websocket.test.ts b/test/websocket.test.ts index 4a43847..830ad9b 100644 --- a/test/websocket.test.ts +++ b/test/websocket.test.ts @@ -5,7 +5,8 @@ import { createAdaptorServer, upgradeWebSocket } from '../src' describe('WebSocket', () => { const startServer = (app: Hono) => { - const server = createAdaptorServer({ fetch: app.fetch, websocket: true }) + const wss = new WebSocketServer({ noServer: true }) + const server = createAdaptorServer({ fetch: app.fetch, websocket: { server: wss } }) return new Promise<{ server: ReturnType; address: AddressInfo }>( (resolve) => { server.listen(0, () => {