honojs/hono · error · Error
Failed to upgrade WebSocket
Error message
Failed to upgrade WebSocket
What it means
defineWebSocketHelper wraps runtime-specific WebSocket upgrade handlers. In the three-argument form (Context, events, options) it awaits the handler and, if the handler returns a falsy value (the upgrade did not succeed), throws this generic Error indicating the HTTP->WebSocket upgrade was rejected.
Source
Thrown at src/helper/websocket/index.ts:134
| [createEvents: (c: Context) => WSEvents<T> | Promise<WSEvents<T>>, options?: U]
| [c: Context, events: WSEvents<T>, options?: U]
) => {
if (typeof args[0] === 'function') {
const [createEvents, options] = args
return async function upgradeWebSocket(c, next) {
const events = await createEvents(c)
const result = await handler(c, events, options as U)
if (result) {
return result
}
await next()
}
} else {
const [c, events, options] = args as [c: Context, events: WSEvents<T>, options?: U]
return (async () => {
const upgraded = await handler(c, events, options as U)
if (!upgraded) {
throw new Error('Failed to upgrade WebSocket')
}
return upgraded
})()
}
}) as UpgradeWebSocket<T, U>
}
View on GitHub (pinned to e2740d5a1b)
Solutions
- Check the runtime/adapter pairing: use createBunWebSocket on Bun.serve, @hono/node-server's WS helper on Node, and the Cloudflare/Deno helpers accordingly
- Ensure the app is the direct fetch handler of the runtime server so upgrade machinery is available
- Verify the client sends a real WebSocket upgrade request (Upgrade: websocket, Sec-WebSocket-Key headers) to the WS route
- In tests use the runtime's WS test utilities (e.g. @hono/node-server createAdaptorServer + ws client), not app.request()
Example fix
// before
import { createBunWebSocket } from 'hono/bun'
// but app served by @hono/node-server -> handler returns falsy -> throws
// after (Node)
import { createNodeWebSocket } from '@hono/node-server'
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket(app)
const server = serve({ fetch: app.fetch })
injectWebSocket(server) Defensive patterns
Strategy: try-catch
Validate before calling
const isUpgradeRequest = (c: Context) =>
c.req.header('upgrade')?.toLowerCase() === 'websocket' &&
!!c.req.header('sec-websocket-key')
app.get('/ws', (c) => {
if (!isUpgradeRequest(c)) return c.text('WebSocket endpoint', 426)
return upgradeWebSocket(handler)(c)
}) Type guard
const isUpgradeRequest = (c: { req: { header(k: string): string | undefined } }): boolean =>
c.req.header('upgrade')?.toLowerCase() === 'websocket' Try / catch
try { return await upgradeWebSocket(handler)(c, events) } catch (e) { if (e instanceof Error && e.message === 'Failed to upgrade WebSocket') return c.text('upgrade failed', 500); throw e } Prevention
- Match the WebSocket helper to your runtime/adapter (Bun vs Node vs Workers)
- Serve the app through the runtime's own server so upgrade() exists
- Test WS routes with real upgrade headers or runtime WS test helpers
When it happens
Trigger: A WebSocket route using upgradeWebSocket() where the underlying runtime handler returns falsy: Bun upgrade() failing (c.env.server missing — see Bun env error), wrong route/method, subprotocol/version mismatch, or handler returning undefined instead of a Response/context.
Common situations: Bun/Node adapter mismatch (using the wrong runtime's WS helper); app not mounted via the expected server (Bun.serve) so upgrade can't run; request not actually a WebSocket upgrade (missing Upgrade header); testing WS routes with plain app.request() without upgrade headers.
Related errors
- env has to include the 2nd argument of fetch.
- env has to include the 2nd argument of fetch.
- This context has no FetchEvent
- This context has no ExecutionContext
- server.requestIP is not a function.
AI-assisted analysis of honojs/hono@e2740d5a1b (2026-08-28).
Data as JSON: /api/errors/c00ecbe4d40fd059.
Report an issue: GitHub.