From ad185b686988b55788605258b7bf7c50f23ad2aa Mon Sep 17 00:00:00 2001 From: PeterZhenhh Date: Wed, 4 Feb 2026 23:29:12 +0800 Subject: [PATCH 1/7] Add Env type to NodeWebSocketInit interface --- packages/node-ws/src/index.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/node-ws/src/index.ts b/packages/node-ws/src/index.ts index f72441c7a..54e7443c1 100644 --- a/packages/node-ws/src/index.ts +++ b/packages/node-ws/src/index.ts @@ -1,4 +1,4 @@ -import type { Hono } from 'hono' +import type { Hono, Env } from 'hono' import { defineWebSocketHelper } from 'hono/ws' import type { UpgradeWebSocket, WSContext } from 'hono/ws' import type { WebSocket } from 'ws' @@ -23,6 +23,7 @@ export interface NodeWebSocketInit { // eslint-disable-next-line @typescript-eslint/no-explicit-any app: Hono baseUrl?: string | URL + env?: Env["Bindings"] } const generateConnectionSymbol = () => Symbol('connection') @@ -77,6 +78,7 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => { } = { incoming: request, outgoing: undefined, + ...init.env } const response = await init.app.request(url, { headers: headers }, env) const waiter = waiterMap.get(request) From cbb74dc8a8ebe73cdb89ef715e442d6ea220eb2f Mon Sep 17 00:00:00 2001 From: PeterZhenhh Date: Wed, 4 Feb 2026 23:33:09 +0800 Subject: [PATCH 2/7] Enhance WebSocket tests with environment variable support Added environment variable handling in WebSocket tests. --- packages/node-ws/src/index.test.ts | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/node-ws/src/index.test.ts b/packages/node-ws/src/index.test.ts index e7fd7a847..d94a6a64a 100644 --- a/packages/node-ws/src/index.test.ts +++ b/packages/node-ws/src/index.test.ts @@ -2,7 +2,7 @@ import { serve } from '@hono/node-server' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import type { ServerType } from '@hono/node-server/dist/types' -import { Hono } from 'hono' +import { Env, Hono } from 'hono' import { cors } from 'hono/cors' import { HTTPException } from 'hono/http-exception' import type { WSMessageReceive } from 'hono/ws' @@ -15,10 +15,12 @@ describe('WebSocket helper', () => { let injectWebSocket: ReturnType['injectWebSocket'] let upgradeWebSocket: ReturnType['upgradeWebSocket'] let wss: ReturnType['wss'] + let env: Env["Bindings"] beforeEach(async () => { app = new Hono() - ;({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app })) + env = { customVar: '1' } as Env["Bindings"] + ({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app, env })) server = await new Promise((resolve) => { const server = serve({ fetch: app.fetch, port: 3030 }, () => { @@ -329,4 +331,20 @@ describe('WebSocket helper', () => { }) ) }) + + it('Should server can obtain environment variables', async () => { + const mainPromise = new Promise((resolve) => + app.get( + '/', + (c, next) => { + let ws = upgradeWebSocket((c) => ({})) + resolve(c.env.customVar) + return ws(c, next) + } + ) + ) + + new WebSocket('ws://localhost:3030/') + expect(await mainPromise).toBe('1') + }) }) From f2033acc530a6ef1832e95e0a936495a3156c460 Mon Sep 17 00:00:00 2001 From: PeterZhenhh Date: Wed, 4 Feb 2026 23:43:48 +0800 Subject: [PATCH 3/7] changeset --- .changeset/modern-clubs-dig.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/modern-clubs-dig.md diff --git a/.changeset/modern-clubs-dig.md b/.changeset/modern-clubs-dig.md new file mode 100644 index 000000000..b8a67e5a2 --- /dev/null +++ b/.changeset/modern-clubs-dig.md @@ -0,0 +1,5 @@ +--- +'@hono/node-ws': major +--- + +Environment variable support From 4a9556fc428a2887c55663930bcb7e4a0184f2f5 Mon Sep 17 00:00:00 2001 From: PeterZhenhh Date: Tue, 10 Feb 2026 18:47:30 +0800 Subject: [PATCH 4/7] add getEnv when init --- packages/node-ws/src/index.ts | 210 ++++++++++++++++++---------------- 1 file changed, 113 insertions(+), 97 deletions(-) diff --git a/packages/node-ws/src/index.ts b/packages/node-ws/src/index.ts index 54e7443c1..f6110f2d4 100644 --- a/packages/node-ws/src/index.ts +++ b/packages/node-ws/src/index.ts @@ -1,35 +1,35 @@ -import type { Hono, Env } from 'hono' -import { defineWebSocketHelper } from 'hono/ws' -import type { UpgradeWebSocket, WSContext } from 'hono/ws' -import type { WebSocket } from 'ws' -import { WebSocketServer } from 'ws' -import { STATUS_CODES } from 'node:http' -import type { IncomingMessage, Server } from 'node:http' -import type { Http2SecureServer, Http2Server } from 'node:http2' -import type { Duplex } from 'node:stream' -import { CloseEvent } from './events' +import type { Hono, Env } from "hono"; +import { defineWebSocketHelper } from "hono/ws"; +import type { UpgradeWebSocket, WSContext } from "hono/ws"; +import type { WebSocket } from "ws"; +import { WebSocketServer } from "ws"; +import { STATUS_CODES } from "node:http"; +import type { IncomingMessage, Server } from "node:http"; +import type { Http2SecureServer, Http2Server } from "node:http2"; +import type { Duplex } from "node:stream"; +import { CloseEvent } from "./events.ts"; export interface NodeWebSocket { upgradeWebSocket: UpgradeWebSocket< WebSocket, { - onError: (err: unknown) => void + onError: (err: unknown) => void; } - > - injectWebSocket(server: Server | Http2Server | Http2SecureServer): void - wss: WebSocketServer + >; + injectWebSocket(server: Server | Http2Server | Http2SecureServer): void; + wss: WebSocketServer; } export interface NodeWebSocketInit { // eslint-disable-next-line @typescript-eslint/no-explicit-any - app: Hono - baseUrl?: string | URL - env?: Env["Bindings"] + app: Hono; + baseUrl?: string | URL; + getEnv?: (request: IncomingMessage) => Env["Bindings"]; } -const generateConnectionSymbol = () => Symbol('connection') +const generateConnectionSymbol = () => Symbol("connection"); /** @example `c.env[CONNECTION_SYMBOL_KEY]` */ -const CONNECTION_SYMBOL_KEY: unique symbol = Symbol('CONNECTION_SYMBOL_KEY') +const CONNECTION_SYMBOL_KEY: unique symbol = Symbol("CONNECTION_SYMBOL_KEY"); /** * Create WebSockets for Node.js @@ -37,158 +37,174 @@ const CONNECTION_SYMBOL_KEY: unique symbol = Symbol('CONNECTION_SYMBOL_KEY') * @returns NodeWebSocket */ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => { - const wss = new WebSocketServer({ noServer: true }) + const wss = new WebSocketServer({ noServer: true }); const waiterMap = new Map< IncomingMessage, { resolve: (ws: WebSocket) => void; connectionSymbol: symbol } - >() + >(); - wss.on('connection', (ws, request) => { - const waiter = waiterMap.get(request) + wss.on("connection", (ws, request) => { + const waiter = waiterMap.get(request); if (waiter) { - waiter.resolve(ws) - waiterMap.delete(request) + waiter.resolve(ws); + waiterMap.delete(request); } - }) + }); - const nodeUpgradeWebSocket = (request: IncomingMessage, connectionSymbol: symbol) => { + const nodeUpgradeWebSocket = ( + request: IncomingMessage, + connectionSymbol: symbol, + ) => { return new Promise((resolve) => { - waiterMap.set(request, { resolve, connectionSymbol }) - }) - } + waiterMap.set(request, { resolve, connectionSymbol }); + }); + }; return { wss, injectWebSocket(server) { - server.on('upgrade', async (request, socket: Duplex, head) => { - const url = new URL(request.url ?? '/', init.baseUrl ?? 'http://localhost') - const headers = new Headers() + 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] + const value = request.headers[key]; if (!value) { - continue + continue; } - headers.append(key, Array.isArray(value) ? value[0] : value) + headers.append(key, Array.isArray(value) ? value[0] : value); } + const customEnv = init.getEnv?.(request) ?? {}; const env: { - incoming: IncomingMessage - outgoing: undefined - [CONNECTION_SYMBOL_KEY]?: symbol + incoming: IncomingMessage; + outgoing: undefined; + [CONNECTION_SYMBOL_KEY]?: symbol; } = { + ...customEnv, incoming: request, outgoing: undefined, - ...init.env - } - const response = await init.app.request(url, { headers: headers }, env) - const waiter = waiterMap.get(request) + }; + const response = await init.app.request(url, { headers: headers }, env); + const waiter = waiterMap.get(request); if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { socket.end( - `HTTP/1.1 ${response.status.toString()} ${STATUS_CODES[response.status] ?? ''}\r\n` + - 'Connection: close\r\n' + - 'Content-Length: 0\r\n' + - '\r\n' - ) - waiterMap.delete(request) - return + `HTTP/1.1 ${response.status.toString()} ${STATUS_CODES[response.status] ?? ""}\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) - }) - }) + wss.emit("connection", ws, request); + }); + }); }, upgradeWebSocket: defineWebSocketHelper(async (c, events, options) => { - if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') { + if (c.req.header("upgrade")?.toLowerCase() !== "websocket") { // Not websocket - return + return; } - const connectionSymbol = generateConnectionSymbol() - c.env[CONNECTION_SYMBOL_KEY] = connectionSymbol - ;(async () => { - const ws = await nodeUpgradeWebSocket(c.env.incoming, connectionSymbol) + const connectionSymbol = generateConnectionSymbol(); + c.env[CONNECTION_SYMBOL_KEY] = connectionSymbol; + (async () => { + const ws = await nodeUpgradeWebSocket(c.env.incoming, connectionSymbol); // buffer messages to handle messages received before the events are set up - const messagesReceivedInStarting: [data: WebSocket.RawData, isBinary: boolean][] = [] + const messagesReceivedInStarting: [ + data: WebSocket.RawData, + isBinary: boolean, + ][] = []; const bufferMessage = (data: WebSocket.RawData, isBinary: boolean) => { - messagesReceivedInStarting.push([data, isBinary]) - } - ws.on('message', bufferMessage) + messagesReceivedInStarting.push([data, isBinary]); + }; + ws.on("message", bufferMessage); const ctx: WSContext = { - binaryType: 'arraybuffer', + binaryType: "arraybuffer", close(code, reason) { - ws.close(code, reason) + ws.close(code, reason); }, protocol: ws.protocol, raw: ws, get readyState() { - return ws.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) + events?.onOpen?.(new Event("open"), ctx); } catch (e) { - ;(options?.onError ?? console.error)(e) + (options?.onError ?? console.error)(e); } const handleMessage = (data: WebSocket.RawData, isBinary: boolean) => { - const datas = Array.isArray(data) ? data : [data] + const datas = Array.isArray(data) ? data : [data]; for (const data of datas) { try { events?.onMessage?.( - new MessageEvent('message', { + new MessageEvent("message", { data: isBinary ? data instanceof ArrayBuffer ? data - : data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) - : data.toString('utf-8'), + : data.buffer.slice( + data.byteOffset, + data.byteOffset + data.byteLength, + ) + : data.toString("utf-8"), }), - ctx - ) + ctx, + ); } catch (e) { - ;(options?.onError ?? console.error)(e) + (options?.onError ?? console.error)(e); } } - } - ws.off('message', bufferMessage) + }; + ws.off("message", bufferMessage); for (const message of messagesReceivedInStarting) { - handleMessage(...message) + handleMessage(...message); } - ws.on('message', (data, isBinary) => { - handleMessage(data, isBinary) - }) - ws.on('close', (code, reason) => { + ws.on("message", (data, isBinary) => { + handleMessage(data, isBinary); + }); + ws.on("close", (code, reason) => { try { - events?.onClose?.(new CloseEvent('close', { code, reason: reason.toString() }), ctx) + events?.onClose?.( + new CloseEvent("close", { code, reason: reason.toString() }), + ctx, + ); } catch (e) { - ;(options?.onError ?? console.error)(e) + (options?.onError ?? console.error)(e); } - }) - ws.on('error', (error) => { + }); + ws.on("error", (error) => { try { events?.onError?.( - new ErrorEvent('error', { + new ErrorEvent("error", { error: error, }), - ctx - ) + ctx, + ); } catch (e) { - ;(options?.onError ?? console.error)(e) + (options?.onError ?? console.error)(e); } - }) - })() + }); + })(); - return new Response() + return new Response(); }), - } -} + }; +}; From 2fe6bbb7fd1cd271a28e510822028a04cab5969a Mon Sep 17 00:00:00 2001 From: PeterZhenhh Date: Tue, 10 Feb 2026 14:07:55 +0000 Subject: [PATCH 5/7] load custom env when init --- .changeset/eleven-corners-smash.md | 5 + packages/node-ws/src/index.test.ts | 21 ++- packages/node-ws/src/index.ts | 283 ++++++++++++++--------------- 3 files changed, 153 insertions(+), 156 deletions(-) create mode 100644 .changeset/eleven-corners-smash.md diff --git a/.changeset/eleven-corners-smash.md b/.changeset/eleven-corners-smash.md new file mode 100644 index 000000000..2dd847ea9 --- /dev/null +++ b/.changeset/eleven-corners-smash.md @@ -0,0 +1,5 @@ +--- +'@hono/node-ws': minor +--- + +load custom env when init diff --git a/packages/node-ws/src/index.test.ts b/packages/node-ws/src/index.test.ts index d94a6a64a..7803f4e01 100644 --- a/packages/node-ws/src/index.test.ts +++ b/packages/node-ws/src/index.test.ts @@ -2,12 +2,20 @@ import { serve } from '@hono/node-server' // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import type { ServerType } from '@hono/node-server/dist/types' -import { Env, Hono } from 'hono' +import { Env, Hono, Context } from 'hono' import { cors } from 'hono/cors' import { HTTPException } from 'hono/http-exception' import type { WSMessageReceive } from 'hono/ws' import { WebSocket } from 'ws' import { createNodeWebSocket } from '.' +import type { IncomingMessage } from "node:http"; +import type { Http2ServerRequest } from 'node:http2' + +interface CustomEnv extends Env { + Bindings: { + customVar: string + }; +} describe('WebSocket helper', () => { let app: Hono @@ -15,12 +23,11 @@ describe('WebSocket helper', () => { let injectWebSocket: ReturnType['injectWebSocket'] let upgradeWebSocket: ReturnType['upgradeWebSocket'] let wss: ReturnType['wss'] - let env: Env["Bindings"] beforeEach(async () => { app = new Hono() - env = { customVar: '1' } as Env["Bindings"] - ({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app, env })) + const getEnv = (req: IncomingMessage | Http2ServerRequest) => { return { customVar: req.headers.upgrade } } + ({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app, getEnv })) server = await new Promise((resolve) => { const server = serve({ fetch: app.fetch, port: 3030 }, () => { @@ -336,8 +343,8 @@ describe('WebSocket helper', () => { const mainPromise = new Promise((resolve) => app.get( '/', - (c, next) => { - let ws = upgradeWebSocket((c) => ({})) + (c: Context, next) => { + let ws = upgradeWebSocket((_c) => ({})) resolve(c.env.customVar) return ws(c, next) } @@ -345,6 +352,6 @@ describe('WebSocket helper', () => { ) new WebSocket('ws://localhost:3030/') - expect(await mainPromise).toBe('1') + expect(await mainPromise).toBe('websocket') }) }) diff --git a/packages/node-ws/src/index.ts b/packages/node-ws/src/index.ts index f6110f2d4..9292995d0 100644 --- a/packages/node-ws/src/index.ts +++ b/packages/node-ws/src/index.ts @@ -1,35 +1,35 @@ -import type { Hono, Env } from "hono"; -import { defineWebSocketHelper } from "hono/ws"; -import type { UpgradeWebSocket, WSContext } from "hono/ws"; -import type { WebSocket } from "ws"; -import { WebSocketServer } from "ws"; -import { STATUS_CODES } from "node:http"; -import type { IncomingMessage, Server } from "node:http"; -import type { Http2SecureServer, Http2Server } from "node:http2"; -import type { Duplex } from "node:stream"; -import { CloseEvent } from "./events.ts"; +import type { Hono, Env } from 'hono' +import { defineWebSocketHelper } from 'hono/ws' +import type { UpgradeWebSocket, WSContext } from 'hono/ws' +import type { WebSocket } from 'ws' +import { WebSocketServer } from 'ws' +import { STATUS_CODES } from 'node:http' +import type { IncomingMessage, Server } from 'node:http' +import type { Http2SecureServer, Http2Server, Http2ServerRequest } from 'node:http2' +import type { Duplex } from 'node:stream' +import { CloseEvent } from './events' export interface NodeWebSocket { upgradeWebSocket: UpgradeWebSocket< WebSocket, { - onError: (err: unknown) => void; + onError: (err: unknown) => void } - >; - injectWebSocket(server: Server | Http2Server | Http2SecureServer): void; - wss: WebSocketServer; + > + injectWebSocket(server: Server | Http2Server | Http2SecureServer): void + wss: WebSocketServer } export interface NodeWebSocketInit { // eslint-disable-next-line @typescript-eslint/no-explicit-any - app: Hono; - baseUrl?: string | URL; - getEnv?: (request: IncomingMessage) => Env["Bindings"]; + app: Hono + baseUrl?: string | URL + getEnv?: (request: IncomingMessage | Http2ServerRequest) => Env['Bindings'] } -const generateConnectionSymbol = () => Symbol("connection"); +const generateConnectionSymbol = () => Symbol('connection') /** @example `c.env[CONNECTION_SYMBOL_KEY]` */ -const CONNECTION_SYMBOL_KEY: unique symbol = Symbol("CONNECTION_SYMBOL_KEY"); +const CONNECTION_SYMBOL_KEY: unique symbol = Symbol('CONNECTION_SYMBOL_KEY') /** * Create WebSockets for Node.js @@ -37,174 +37,159 @@ const CONNECTION_SYMBOL_KEY: unique symbol = Symbol("CONNECTION_SYMBOL_KEY"); * @returns NodeWebSocket */ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => { - const wss = new WebSocketServer({ noServer: true }); + const wss = new WebSocketServer({ noServer: true }) const waiterMap = new Map< IncomingMessage, { resolve: (ws: WebSocket) => void; connectionSymbol: symbol } - >(); + >() - wss.on("connection", (ws, request) => { - const waiter = waiterMap.get(request); + wss.on('connection', (ws, request) => { + const waiter = waiterMap.get(request) if (waiter) { - waiter.resolve(ws); - waiterMap.delete(request); + waiter.resolve(ws) + waiterMap.delete(request) } - }); + }) - const nodeUpgradeWebSocket = ( - request: IncomingMessage, - connectionSymbol: symbol, - ) => { + const nodeUpgradeWebSocket = (request: IncomingMessage, connectionSymbol: symbol) => { return new Promise((resolve) => { - waiterMap.set(request, { resolve, connectionSymbol }); - }); - }; + waiterMap.set(request, { resolve, connectionSymbol }) + }) + } return { wss, injectWebSocket(server) { - server.on("upgrade", async (request, socket: Duplex, head) => { - const url = new URL( - request.url ?? "/", - init.baseUrl ?? "http://localhost", - ); - const headers = new Headers(); + 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]; + const value = request.headers[key] if (!value) { - continue; + continue } - headers.append(key, Array.isArray(value) ? value[0] : value); + headers.append(key, Array.isArray(value) ? value[0] : value) } - const customEnv = init.getEnv?.(request) ?? {}; + const customEnv = init.getEnv?.(request) ?? {} const env: { - incoming: IncomingMessage; - outgoing: undefined; - [CONNECTION_SYMBOL_KEY]?: symbol; + incoming: IncomingMessage + outgoing: undefined + [CONNECTION_SYMBOL_KEY]?: symbol } = { ...customEnv, incoming: request, outgoing: undefined, - }; - const response = await init.app.request(url, { headers: headers }, env); - const waiter = waiterMap.get(request); + } + const response = await init.app.request(url, { headers: headers }, env) + const waiter = waiterMap.get(request) if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { socket.end( - `HTTP/1.1 ${response.status.toString()} ${STATUS_CODES[response.status] ?? ""}\r\n` + - "Connection: close\r\n" + - "Content-Length: 0\r\n" + - "\r\n", - ); - waiterMap.delete(request); - return; + `HTTP/1.1 ${response.status.toString()} ${STATUS_CODES[response.status] ?? ''}\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); - }); - }); + wss.emit('connection', ws, request) + }) + }) }, upgradeWebSocket: defineWebSocketHelper(async (c, events, options) => { - if (c.req.header("upgrade")?.toLowerCase() !== "websocket") { + if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') { // Not websocket - return; + return } - const connectionSymbol = generateConnectionSymbol(); - c.env[CONNECTION_SYMBOL_KEY] = connectionSymbol; - (async () => { - const ws = await nodeUpgradeWebSocket(c.env.incoming, connectionSymbol); - - // buffer messages to handle messages received before the events are set up - const messagesReceivedInStarting: [ - data: WebSocket.RawData, - isBinary: boolean, - ][] = []; - const bufferMessage = (data: WebSocket.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 connectionSymbol = generateConnectionSymbol() + c.env[CONNECTION_SYMBOL_KEY] = connectionSymbol + ; (async () => { + const ws = await nodeUpgradeWebSocket(c.env.incoming, connectionSymbol) - const handleMessage = (data: WebSocket.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); - } + // buffer messages to handle messages received before the events are set up + const messagesReceivedInStarting: [data: WebSocket.RawData, isBinary: boolean][] = [] + const bufferMessage = (data: WebSocket.RawData, isBinary: boolean) => { + messagesReceivedInStarting.push([data, isBinary]) } - }; - 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('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), } - }); - ws.on("error", (error) => { try { - events?.onError?.( - new ErrorEvent("error", { - error: error, - }), - ctx, - ); + events?.onOpen?.(new Event('open'), ctx) } catch (e) { - (options?.onError ?? console.error)(e); + ; (options?.onError ?? console.error)(e) + } + + const handleMessage = (data: WebSocket.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) } - }); - })(); - return new Response(); + 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: error, + }), + ctx + ) + } catch (e) { + ; (options?.onError ?? console.error)(e) + } + }) + })() + + return new Response() }), - }; -}; + } +} \ No newline at end of file From 075347e80e441a1dbd0aa219e7040ec6d8f20be3 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Mon, 16 Feb 2026 18:13:56 +0900 Subject: [PATCH 6/7] format --- packages/node-ws/src/index.test.ts | 23 +++-- packages/node-ws/src/index.ts | 152 ++++++++++++++--------------- 2 files changed, 87 insertions(+), 88 deletions(-) diff --git a/packages/node-ws/src/index.test.ts b/packages/node-ws/src/index.test.ts index 7803f4e01..a9ad0e102 100644 --- a/packages/node-ws/src/index.test.ts +++ b/packages/node-ws/src/index.test.ts @@ -8,13 +8,13 @@ import { HTTPException } from 'hono/http-exception' import type { WSMessageReceive } from 'hono/ws' import { WebSocket } from 'ws' import { createNodeWebSocket } from '.' -import type { IncomingMessage } from "node:http"; +import type { IncomingMessage } from 'node:http' import type { Http2ServerRequest } from 'node:http2' interface CustomEnv extends Env { Bindings: { customVar: string - }; + } } describe('WebSocket helper', () => { @@ -26,8 +26,10 @@ describe('WebSocket helper', () => { beforeEach(async () => { app = new Hono() - const getEnv = (req: IncomingMessage | Http2ServerRequest) => { return { customVar: req.headers.upgrade } } - ({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app, getEnv })) + const getEnv = (req: IncomingMessage | Http2ServerRequest) => { + return { customVar: req.headers.upgrade } + } + ;({ injectWebSocket, upgradeWebSocket, wss } = createNodeWebSocket({ app, getEnv })) server = await new Promise((resolve) => { const server = serve({ fetch: app.fetch, port: 3030 }, () => { @@ -341,14 +343,11 @@ describe('WebSocket helper', () => { it('Should server can obtain environment variables', async () => { const mainPromise = new Promise((resolve) => - app.get( - '/', - (c: Context, next) => { - let ws = upgradeWebSocket((_c) => ({})) - resolve(c.env.customVar) - return ws(c, next) - } - ) + app.get('/', (c: Context, next) => { + let ws = upgradeWebSocket((_c) => ({})) + resolve(c.env.customVar) + return ws(c, next) + }) ) new WebSocket('ws://localhost:3030/') diff --git a/packages/node-ws/src/index.ts b/packages/node-ws/src/index.ts index 9292995d0..2a64ac734 100644 --- a/packages/node-ws/src/index.ts +++ b/packages/node-ws/src/index.ts @@ -87,9 +87,9 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => { if (!waiter || waiter.connectionSymbol !== env[CONNECTION_SYMBOL_KEY]) { socket.end( `HTTP/1.1 ${response.status.toString()} ${STATUS_CODES[response.status] ?? ''}\r\n` + - 'Connection: close\r\n' + - 'Content-Length: 0\r\n' + - '\r\n' + 'Connection: close\r\n' + + 'Content-Length: 0\r\n' + + '\r\n' ) waiterMap.delete(request) return @@ -108,88 +108,88 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => { const connectionSymbol = generateConnectionSymbol() c.env[CONNECTION_SYMBOL_KEY] = connectionSymbol - ; (async () => { - const ws = await nodeUpgradeWebSocket(c.env.incoming, connectionSymbol) + ;(async () => { + const ws = await nodeUpgradeWebSocket(c.env.incoming, connectionSymbol) - // buffer messages to handle messages received before the events are set up - const messagesReceivedInStarting: [data: WebSocket.RawData, isBinary: boolean][] = [] - const bufferMessage = (data: WebSocket.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: WebSocket.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) - } + // buffer messages to handle messages received before the events are set up + const messagesReceivedInStarting: [data: WebSocket.RawData, isBinary: boolean][] = [] + const bufferMessage = (data: WebSocket.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) + } - 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) => { + const handleMessage = (data: WebSocket.RawData, isBinary: boolean) => { + const datas = Array.isArray(data) ? data : [data] + for (const data of datas) { try { - events?.onError?.( - new ErrorEvent('error', { - error: error, + 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) + ;(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: error, + }), + ctx + ) + } catch (e) { + ;(options?.onError ?? console.error)(e) + } + }) + })() return new Response() }), } -} \ No newline at end of file +} From bf59dbf2372d5476249b9a81d98886406cb2acb9 Mon Sep 17 00:00:00 2001 From: Yusuke Wada Date: Mon, 16 Feb 2026 18:16:14 +0900 Subject: [PATCH 7/7] remove the unused changeset --- .changeset/modern-clubs-dig.md | 5 ----- 1 file changed, 5 deletions(-) delete mode 100644 .changeset/modern-clubs-dig.md diff --git a/.changeset/modern-clubs-dig.md b/.changeset/modern-clubs-dig.md deleted file mode 100644 index b8a67e5a2..000000000 --- a/.changeset/modern-clubs-dig.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hono/node-ws': major ---- - -Environment variable support