Skip to content

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:

typescript
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:

typescript
listener: '/ws' // Matches /ws, /ws/chat, /ws/room/123
listener: '/api/ws' // Matches /api/ws, /api/ws/data

Matching 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: websocket header
  • Use the GET method
  • Match the listener path 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:

typescript
allowedOrigins: '*' // Accept any origin
allowedOrigins: ['https://example.com', 'https://app.example.com'] // Allowlist

When 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:

typescript
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:

typescript
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:

typescript
onDisconnect: (socket: WebSocket, event: CloseEvent, ctx: Context) => {
  console.log('Client disconnected:', event.code, event.reason, event.wasClean)
}

onError

Handle WebSocket errors:

typescript
onError: (socket: WebSocket, event: Event, ctx: Context) => {
  console.error('WebSocket error:', event, 'on', ctx.get.url())
}

Complete Example

typescript
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:

typescript
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:

typescript
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: 13 and Upgrade: websocket headers
  • 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:

typescript
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.

Released under the MIT License.