Installations
npm install @fastify/websocket
Developer
fastify
Developer Guide
Module System
CommonJS
Min. Node Version
Typescript Support
Yes
Node Version
20.8.0
NPM Version
10.1.0
Statistics
404 Stars
274 Commits
75 Forks
20 Watching
5 Branches
63 Contributors
Updated on 23 Nov 2024
Languages
JavaScript (90.54%)
TypeScript (9.46%)
Total Downloads
Cumulative downloads
Total Downloads
13,204,091
Last day
-11.3%
51,209
Compared to previous day
Last week
-20%
283,314
Compared to previous week
Last month
90.1%
1,072,420
Compared to previous month
Last year
342.1%
10,374,449
Compared to previous year
Daily Downloads
Weekly Downloads
Monthly Downloads
Yearly Downloads
@fastify/websocket
WebSocket support for Fastify. Built upon ws@8.
Install
1npm i @fastify/websocket 2# or 3yarn add @fastify/websocket
If you're a TypeScript user, this package has its own TypeScript types built in, but you will also need to install the types for the ws
package:
1npm i @types/ws -D 2# or 3yarn add -D @types/ws
Usage
After registering this plugin, you can choose on which routes the WS server will respond. This can be achieved by adding websocket: true
property to routeOptions
on a fastify's .get
route. In this case two arguments will be passed to the handler, the socket connection, and the fastify
request object:
1'use strict' 2 3const fastify = require('fastify')() 4fastify.register(require('@fastify/websocket')) 5fastify.register(async function (fastify) { 6 fastify.get('/', { websocket: true }, (socket /* WebSocket */, req /* FastifyRequest */) => { 7 socket.on('message', message => { 8 // message.toString() === 'hi from client' 9 socket.send('hi from server') 10 }) 11 }) 12}) 13 14fastify.listen({ port: 3000 }, err => { 15 if (err) { 16 fastify.log.error(err) 17 process.exit(1) 18 } 19})
In this case, it will respond with a 404 error on every unregistered route, closing the incoming upgrade connection requests.
However, you can still define a wildcard route, that will be used as default handler:
1'use strict' 2 3const fastify = require('fastify')() 4 5fastify.register(require('@fastify/websocket'), { 6 options: { maxPayload: 1048576 } 7}) 8 9fastify.register(async function (fastify) { 10 fastify.get('/*', { websocket: true }, (socket /* WebSocket */, req /* FastifyRequest */) => { 11 socket.on('message', message => { 12 // message.toString() === 'hi from client' 13 socket.send('hi from wildcard route') 14 }) 15 }) 16 17 fastify.get('/', { websocket: true }, (socket /* WebSocket */, req /* FastifyRequest */) => { 18 socket.on('message', message => { 19 // message.toString() === 'hi from client' 20 socket.send('hi from server') 21 }) 22 }) 23}) 24 25fastify.listen({ port: 3000 }, err => { 26 if (err) { 27 fastify.log.error(err) 28 process.exit(1) 29 } 30})
Attaching event handlers
It is important that websocket route handlers attach event handlers synchronously during handler execution to avoid accidentally dropping messages. If you want to do any async work in your websocket handler, say to authenticate a user or load data from a datastore, ensure you attach any on('message')
handlers before you trigger this async work. Otherwise, messages might arrive whilst this async work is underway, and if there is no handler listening for this data it will be silently dropped.
Here is an example of how to attach message handlers synchronously while still accessing asynchronous resources. We store a promise for the async thing in a local variable, attach the message handler synchronously, and then make the message handler itself asynchronous to grab the async data and do some processing:
1fastify.get('/*', { websocket: true }, (socket, request) => { 2 const sessionPromise = request.getSession() // example async session getter, called synchronously to return a promise 3 4 socket.on('message', async (message) => { 5 const session = await sessionPromise() 6 // do something with the message and session 7 }) 8})
Using hooks
Routes registered with @fastify/websocket
respect the Fastify plugin encapsulation contexts, and so will run any hooks that have been registered. This means the same route hooks you might use for authentication or error handling of plain old HTTP handlers will apply to websocket handlers as well.
1fastify.addHook('preValidation', async (request, reply) => { 2 // check if the request is authenticated 3 if (!request.isAuthenticated()) { 4 await reply.code(401).send("not authenticated"); 5 } 6}) 7fastify.get('/', { websocket: true }, (socket, req) => { 8 // the connection will only be opened for authenticated incoming requests 9 socket.on('message', message => { 10 // ... 11 }) 12})
NB
This plugin uses the same router as the fastify
instance, this has a few implications to take into account:
- Websocket route handlers follow the usual
fastify
request lifecycle, which means hooks, error handlers, and decorators all work the same way as other route handlers. - You can access the fastify server via
this
in your handlers - When using
@fastify/websocket
, it needs to be registered before all routes in order to be able to intercept websocket connections to existing routes and close the connection on non-websocket routes.
1import Fastify from 'fastify' 2import websocket from '@fastify/websocket' 3 4const fastify = Fastify() 5await fastify.register(websocket) 6 7fastify.get('/', { websocket: true }, function wsHandler (socket, req) { 8 // bound to fastify server 9 this.myDecoration.someFunc() 10 11 socket.on('message', message => { 12 // message.toString() === 'hi from client' 13 socket.send('hi from server') 14 }) 15}) 16 17await fastify.listen({ port: 3000 })
If you need to handle both HTTP requests and incoming socket connections on the same route, you can still do it using the full declaration syntax, adding a wsHandler
property.
1'use strict' 2 3const fastify = require('fastify')() 4 5function handle (socket, req) { 6 socket.on('message', (data) => socket.send(data)) // creates an echo server 7} 8 9fastify.register(require('@fastify/websocket'), { 10 handle, 11 options: { maxPayload: 1048576 } 12}) 13 14fastify.register(async function () { 15 fastify.route({ 16 method: 'GET', 17 url: '/hello', 18 handler: (req, reply) => { 19 // this will handle http requests 20 reply.send({ hello: 'world' }) 21 }, 22 wsHandler: (socket, req) => { 23 // this will handle websockets connections 24 socket.send('hello client') 25 26 socket.once('message', chunk => { 27 socket.close() 28 }) 29 } 30 }) 31}) 32 33fastify.listen({ port: 3000 }, err => { 34 if (err) { 35 fastify.log.error(err) 36 process.exit(1) 37 } 38})
Custom error handler:
You can optionally provide a custom errorHandler
that will be used to handle any cleaning up of established websocket connections. The errorHandler
will be called if any errors are thrown by your websocket route handler after the connection has been established. Note that neither Fastify's onError
hook or functions registered with fastify.setErrorHandler
will be called for errors thrown during a websocket request handler.
Neither the errorHandler
passed to this plugin or fastify's onError
hook will be called for errors encountered during message processing for your connection. If you want to handle unexpected errors within your message
event handlers, you'll need to use your own try { } catch {}
statements and decide what to send back over the websocket.
1const fastify = require('fastify')() 2 3fastify.register(require('@fastify/websocket'), { 4 errorHandler: function (error, socket /* WebSocket */, req /* FastifyRequest */, reply /* FastifyReply */) { 5 // Do stuff 6 // destroy/close connection 7 socket.terminate() 8 }, 9 options: { 10 maxPayload: 1048576, // we set the maximum allowed messages size to 1 MiB (1024 bytes * 1024 bytes) 11 verifyClient: function (info, next) { 12 if (info.req.headers['x-fastify-header'] !== 'fastify is awesome !') { 13 return next(false) // the connection is not allowed 14 } 15 next(true) // the connection is allowed 16 } 17 } 18}) 19 20fastify.get('/', { websocket: true }, (socket /* WebSocket */, req /* FastifyRequest */) => { 21 socket.on('message', message => { 22 // message.toString() === 'hi from client' 23 socket.send('hi from server') 24 }) 25}) 26 27fastify.listen({ port: 3000 }, err => { 28 if (err) { 29 fastify.log.error(err) 30 process.exit(1) 31 } 32})
Note: Fastify's onError
and error handlers registered by setErrorHandler
will still be called for errors encountered before the websocket connection is established. This means errors thrown by onRequest
hooks, preValidation
handlers, and hooks registered by plugins will use the normal error handling mechanisms in Fastify. Once the websocket is established and your websocket route handler is called, fastify-websocket
's errorHandler
takes over.
Custom preClose hook:
By default, all ws connections are closed when the server closes. If you wish to modify this behaviour, you can pass your own preClose
function.
Note that preClose
is responsible for closing all connections and closing the websocket server.
1const fastify = require('fastify')() 2 3fastify.register(require('@fastify/websocket'), { 4 preClose: (done) => { // Note: can also use async style, without done-callback 5 const server = this.websocketServer 6 7 for (const socket of server.clients) { 8 socket.close(1001, 'WS server is going offline in custom manner, sending a code + message') 9 } 10 11 server.close(done) 12 } 13})
Testing
Testing the ws handler can be quite tricky, luckily fastify-websocket
decorates fastify instance with injectWS
.
It allows to test easily a websocket endpoint.
The signature of injectWS is the following: ([path], [upgradeContext])
.
Creating a stream from the WebSocket
1const Fastify = require('fastify') 2const FastifyWebSocket = require('@fastify/websocket') 3const ws = require('ws') 4 5const fastify = Fastify() 6await fastify.register(FastifyWebSocket) 7 8fastify.get('/', { websocket: true }, (socket, req) => { 9 const stream = ws.createWebSocketStream(socket, { /* options */ }) 10 stream.setEncoding('utf8') 11 stream.write('hello client') 12 13 stream.on('data', function (data) { 14 // Make sure to set up a data handler or read all the incoming 15 // data in another way, otherwise stream backpressure will cause 16 // the underlying WebSocket object to get paused. 17 }) 18}) 19 20await fastify.listen({ port: 3000 })
App.js
1'use strict' 2 3const Fastify = require('fastify') 4const FastifyWebSocket = require('@fastify/websocket') 5 6const App = Fastify() 7 8App.register(FastifyWebSocket); 9 10App.register(async function(fastify) { 11 fastify.addHook('preValidation', async (request, reply) => { 12 if (request.headers['api-key'] !== 'some-random-key') { 13 return reply.code(401).send() 14 } 15 }) 16 17 fastify.get('/', { websocket: true }, (socket) => { 18 socket.on('message', message => { 19 socket.send('hi from server') 20 }) 21 }) 22}) 23 24module.exports = App
App.test.js
1'use strict' 2 3const { test } = require('tap') 4const Fastify = require('fastify') 5const App = require('./app.js') 6 7test('connect to /', async (t) => { 8 t.plan(1) 9 10 const fastify = Fastify() 11 fastify.register(App) 12 t.teardown(fastify.close.bind(fastify)) 13 await fastify.ready() 14 15 const ws = await fastify.injectWS('/', {headers: { "api-key" : "some-random-key" }}) 16 let resolve; 17 const promise = new Promise(r => { resolve = r }) 18 19 ws.on('message', (data) => { 20 resolve(data.toString()); 21 }) 22 ws.send('hi from client') 23 24 t.assert(await promise, 'hi from server') 25 // Remember to close the ws at the end 26 ws.terminate() 27})
Things to know
- Websocket need to be closed manually at the end of each test.
fastify.ready()
needs to be awaited to ensure that fastify has been decorated.- You need to register the event listener before sending the message if you need to process server response.
Options
@fastify/websocket
accept these options for ws
:
host
- The hostname where to bind the server.port
- The port where to bind the server.backlog
- The maximum length of the queue of pending connections.server
- A pre-created Node.js HTTP/S server.verifyClient
- A function which can be used to validate incoming connections.handleProtocols
- A function which can be used to handle the WebSocket subprotocols.clientTracking
- Specifies whether or not to track clients.perMessageDeflate
- Enable/disable permessage-deflate.maxPayload
- The maximum allowed message size in bytes.
For more information, you can check ws
options documentation.
NB By default if you do not provide a server
option @fastify/websocket
will bind your websocket server instance to the scoped fastify
instance.
NB The path
option from ws
should not be provided since the routing is handled by fastify itself
NB The noServer
option from ws
should not be provided since the point of @fastify/websocket is to listen on the fastify server. If you want a custom server, you can use the server
option, and if you want more control, you can use the ws
library directly
ws does not allow you to set objectMode
or writableObjectMode
to true
Acknowledgements
This project is kindly sponsored by nearForm.
License
Licensed under MIT.
Stable Version
The latest stable version of the package.
Stable Version
11.0.1
HIGH
2
7.5/10
Summary
fastify/websocket vulnerable to uncaught exception via crash on malformed packet
Affected Versions
>= 5.0.0, < 5.0.1
Patched Versions
5.0.1
7.5/10
Summary
fastify/websocket vulnerable to uncaught exception via crash on malformed packet
Affected Versions
>= 6.0.0, < 7.1.1
Patched Versions
7.1.1
Reason
no binaries found in the repo
Reason
no dangerous workflow patterns detected
Reason
0 existing vulnerabilities detected
Reason
license file detected
Details
- Info: project has a license file: LICENSE:0
- Info: FSF or OSI recognized license: MIT License: LICENSE:0
Reason
security policy file detected
Details
- Info: security policy file detected: github.com/fastify/.github/SECURITY.md:1
- Info: Found linked content: github.com/fastify/.github/SECURITY.md:1
- Info: Found disclosure, vulnerability, and/or timelines in security policy: github.com/fastify/.github/SECURITY.md:1
- Info: Found text in security policy: github.com/fastify/.github/SECURITY.md:1
Reason
SAST tool is not run on all commits -- score normalized to 7
Details
- Warn: 14 commits out of 19 are checked with a SAST tool
Reason
6 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 5
Reason
Found 12/26 approved changesets -- score normalized to 4
Reason
detected GitHub workflow tokens with excessive permissions
Details
- Warn: no topLevel permission defined: .github/workflows/ci.yml:1
- Info: no jobLevel write permissions found
Reason
no effort to earn an OpenSSF best practices badge detected
Reason
project is not fuzzed
Details
- Warn: no fuzzer integrations found
Score
6.3
/10
Last Scanned on 2024-11-25
The Open Source Security Foundation is a cross-industry collaboration to improve the security of open source software (OSS). The Scorecard provides security health metrics for open source projects.
Learn More