WebSocket Middleware
Reference: Deno upgradeWebSocket API Documentation
WebSocket middleware handles WebSocket connection upgrades, allowing real-time bidirectional communication between client and server.
Basic Usage
Apply WebSocket middleware using Deserve's built-in middleware:
import { Mware, Router } from '@neabyte/deserve'
const router = new Router()
// Upgrade /ws and greet on connect
router.use(
Mware.websocket({
listener: '/ws',
onConnect: (socket, event, ctx) => {
console.log('WebSocket connected:', ctx.get.url())
socket.send('Welcome')
}
})
)
await router.serve(8000)WebSocket Options
listener
Specify the path prefix for WebSocket upgrades:
listener: '/ws' // Matches /ws, /ws/chat, /ws/room/123
listener: '/api/ws' // Matches /api/ws, /api/ws/dataMatching is boundary-aware, so the path must equal the listener exactly or continue with a /. With listener: '/ws', a request to /ws or /ws/chat upgrades, but /wsfoo does not. A trailing slash on the listener is stripped, so /ws/ behaves the same as /ws. Setting listener: '/' matches every path.
Important: The middleware only upgrades requests that:
- Carry the
Upgrade: websocketheader - Use the
GETmethod - Match the
listenerpath as described above
Without a listener, the middleware passes every request through and never upgrades.
allowedOrigins
Control which handshake origins are accepted, which guards against cross-site WebSocket hijacking:
allowedOrigins: '*' // Accept any origin
allowedOrigins: ['https://example.com', 'https://app.example.com'] // AllowlistWhen allowedOrigins is left undefined, only same-origin handshakes are accepted, and a handshake with no Origin header is waved through since no policy is set. The moment an allowlist or '*' is configured, a missing Origin fails closed and the upgrade is refused, which closes a gap where a header simply omitted could slip past the policy. A rejected origin returns 403 Forbidden.
onConnect
Handle new WebSocket connections:
onConnect: (socket: WebSocket, event: Event, ctx: Context) => {
console.log('Client connected:', ctx.get.url())
socket.send(JSON.stringify({
type: 'welcome',
message: 'Connected'
}))
}onMessage
Handle incoming WebSocket messages:
onMessage: (socket: WebSocket, event: MessageEvent, ctx: Context) => {
console.log('Received:', event.data)
try {
const data = JSON.parse(event.data as string)
socket.send(JSON.stringify({
echo: data
}))
} catch {
socket.send(JSON.stringify({
error: 'Invalid JSON'
}))
}
}onDisconnect
Handle WebSocket disconnections. The CloseEvent provides code, reason, and wasClean:
onDisconnect: (socket: WebSocket, event: CloseEvent, ctx: Context) => {
console.log('Client disconnected:', event.code, event.reason, event.wasClean)
}onError
Handle WebSocket errors:
onError: (socket: WebSocket, event: Event, ctx: Context) => {
console.error('WebSocket error:', event, 'on', ctx.get.url())
}Complete Example
import { Mware, Router } from '@neabyte/deserve'
const router = new Router({
routes: { directory: './routes' }
})
router.use(
Mware.websocket({
listener: '/ws',
onConnect: (socket, event, ctx) => {
console.log(`WebSocket connected: ${ctx.get.url()}`)
socket.send(
JSON.stringify({
type: 'welcome',
message: 'Connected to Deserve WebSocket server'
})
)
},
onMessage: (socket, event, ctx) => {
console.log(`Message from ${ctx.get.url()}:`, event.data)
try {
const data = JSON.parse(event.data as string)
if (data.type === 'ping') {
socket.send(
JSON.stringify({
type: 'pong',
timestamp: Date.now()
})
)
} else {
socket.send(
JSON.stringify({
type: 'echo',
original: data,
timestamp: Date.now()
})
)
}
} catch {
socket.send(
JSON.stringify({
type: 'error',
message: 'Invalid JSON'
})
)
}
},
onDisconnect: (socket, event, ctx) => {
console.log(`WebSocket disconnected: ${ctx.get.url()} code=${event.code} reason=${event.reason}`)
},
onError: (socket, event, ctx) => {
console.error(`WebSocket error on ${ctx.get.url()}:`, event)
}
})
)
await router.serve(8000)Client-Side Usage
Connect from a browser with the native WebSocket API:
const socket = new WebSocket('ws://localhost:8000/ws')
socket.addEventListener('open', () => {
console.log('Connected')
socket.send(
JSON.stringify({
type: 'message',
text: 'Hello'
})
)
})
socket.addEventListener('message', (event) => {
const data = JSON.parse(event.data)
console.log('Received:', data)
})
socket.addEventListener('close', () => {
console.log('Disconnected')
})
socket.addEventListener('error', (error) => {
console.error('Error:', error)
})WebSocket Properties
Access WebSocket properties inside handlers:
onConnect: (socket, event, ctx) => {
console.log('URL:', socket.url)
console.log('Protocol:', socket.protocol)
console.log('State:', socket.readyState)
// 0=CONNECTING, 1=OPEN, 2=CLOSING, 3=CLOSED
}Error Handling
A rejected handshake routes through the error handler instead of throwing at setup:
- Disallowed origin fails with 403 and message
WebSocket handshake rejected because the Origin is not allowed - Missing version fails with 400 and message
WebSocket handshake requires Sec-WebSocket-Version 13 - Wrong version returns 426 Upgrade Required with
Sec-WebSocket-Version: 13andUpgrade: websocketheaders - Malformed upgrade fails with 400 and message
WebSocket handshake is malformed because ...
Each rejection also emits a websocket:rejected event with the reason, covered in Event Reference. All failures route through the central error handler, so shape the response there or rely on the default behavior.
Integration with CORS
WebSocket pairs with CORS-enabled clients like this:
import { Mware, Router } from '@neabyte/deserve'
const router = new Router()
// CORS handles HTTP, WebSocket handles upgrades
router.use(Mware.cors({
origin: '*'
}))
router.use(Mware.websocket({
listener: '/ws'
}))
await router.serve(8000)CORS middleware handles HTTP requests, while WebSocket middleware handles the upgrades, and the two run together without conflict.