fix(node-ws): make adapter uncrashable (#1141)

* fix(node-ws): make adapter uncrashable

* add changeset

* unnessesary diff

* update yarn.lock

* make changeset patch
pull/1144/head
Shotaro Nakamura 2025-04-27 20:34:43 +09:00 committed by GitHub
parent 247f7705b3
commit 1765a9a3aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 75 additions and 22 deletions

View File

@ -0,0 +1,5 @@
---
'@hono/node-ws': patch
---
Make it uncrashable

View File

@ -214,6 +214,26 @@ describe('WebSocket helper', () => {
}) })
}) })
it('Should onError works well', async () => {
const mainPromise = new Promise<unknown>((resolve) =>
app.get(
'/',
upgradeWebSocket(
() => {
throw 0
},
{
onError(err) {
resolve(err)
},
}
)
)
)
const ws = new WebSocket('ws://localhost:3030/')
expect(await mainPromise).toBe(0)
})
describe('Types', () => { describe('Types', () => {
it('Should not throw a type error with an app with Variables generics', () => { it('Should not throw a type error with an app with Variables generics', () => {
const app = new Hono<{ const app = new Hono<{

View File

@ -1,5 +1,5 @@
import type { Hono } from 'hono' import type { Hono } from 'hono'
import type { UpgradeWebSocket, WSContext } from 'hono/ws' import type { UpgradeWebSocket, WSContext, WSEvents } from 'hono/ws'
import type { WebSocket } from 'ws' import type { WebSocket } from 'ws'
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
import type { IncomingMessage } from 'http' import type { IncomingMessage } from 'http'
@ -9,7 +9,12 @@ import type { Duplex } from 'node:stream'
import { CloseEvent } from './events' import { CloseEvent } from './events'
export interface NodeWebSocket { export interface NodeWebSocket {
upgradeWebSocket: UpgradeWebSocket<WebSocket> upgradeWebSocket: UpgradeWebSocket<
WebSocket,
{
onError: (err: unknown) => void
}
>
injectWebSocket(server: Server | Http2Server | Http2SecureServer): void injectWebSocket(server: Server | Http2Server | Http2SecureServer): void
} }
export interface NodeWebSocketInit { export interface NodeWebSocketInit {
@ -80,7 +85,7 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
}) })
}) })
}, },
upgradeWebSocket: (createEvents) => upgradeWebSocket: (createEvents, options) =>
async function upgradeWebSocket(c, next) { async function upgradeWebSocket(c, next) {
if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') { if (c.req.header('upgrade')?.toLowerCase() !== 'websocket') {
// Not websocket // Not websocket
@ -91,7 +96,14 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
const response = new Response() const response = new Response()
;(async () => { ;(async () => {
const ws = await nodeUpgradeWebSocket(c.env.incoming, response) const ws = await nodeUpgradeWebSocket(c.env.incoming, response)
const events = await createEvents(c) let events: WSEvents<WebSocket>
try {
events = await createEvents(c)
} catch (e) {
;(options?.onError ?? console.error)(e)
ws.close()
return
}
const ctx: WSContext<WebSocket> = { const ctx: WSContext<WebSocket> = {
binaryType: 'arraybuffer', binaryType: 'arraybuffer',
@ -110,32 +122,48 @@ export const createNodeWebSocket = (init: NodeWebSocketInit): NodeWebSocket => {
}, },
url: new URL(c.req.url), url: new URL(c.req.url),
} }
events.onOpen?.(new Event('open'), ctx) try {
events?.onOpen?.(new Event('open'), ctx)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
ws.on('message', (data, isBinary) => { ws.on('message', (data, isBinary) => {
const datas = Array.isArray(data) ? data : [data] const datas = Array.isArray(data) ? data : [data]
for (const data of datas) { for (const data of datas) {
events.onMessage?.( try {
new MessageEvent('message', { events?.onMessage?.(
data: isBinary new MessageEvent('message', {
? data instanceof ArrayBuffer data: isBinary
? data ? data instanceof ArrayBuffer
: data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) ? data
: 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)
}
} }
}) })
ws.on('close', (code, reason) => { ws.on('close', (code, reason) => {
events.onClose?.(new CloseEvent('close', { code, reason: reason.toString() }), ctx) try {
events?.onClose?.(new CloseEvent('close', { code, reason: reason.toString() }), ctx)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
}) })
ws.on('error', (error) => { ws.on('error', (error) => {
events.onError?.( try {
new ErrorEvent('error', { events?.onError?.(
error: error, new ErrorEvent('error', {
}), error: error,
ctx }),
) ctx
)
} catch (e) {
;(options?.onError ?? console.error)(e)
}
}) })
})() })()