@fastify/websocket

repository·main·Indexed 19 days ago

https://github.com/fastify/fastify-websocket

A Fastify plugin providing basic WebSocket support built on top of the ws library. It allows developers to enable WebSocket support on specific routes using the websocket: true property or a wsHandler, integrates with Fastify's lifecycle hooks (onRequest, preParsing, preValidation, and preHandler), and provides an injectWS method for testing WebSocket endpoints.

Tokens
4.9K
Snippets
18
Records
18
Agent score
17%

What's inside @fastify/websocket

  1. How to safely attach WebSocket event handlers with async work

    main

    WebSocket route handlers must attach event handlers (like socket.on('message', ...) ) synchronously during the handler's execution. If you perform asynchronous work (like database lookups or authentication) before attaching the handler, incoming messages might arrive while the async work is pending, causing them to be silently dropped because no listener is yet active.

    Best Practice:

    1. Initiate the async work synchronously (e.g., call a function that returns a Promise).
    2. Attach the socket.on('message', ...) handler immediately.
    3. Inside the message handler, await the previously initiated Promise to access the required data.
    fastify.get('/*', { websocket: true }, (socket, request) => {
      // 1. Start async work synchronously, returning a promise
      const sessionPromise = request.getSession()
    
      // 2. Attach handler synchronously
      socket.on('message', async (message) => {
        // 3. Await the data inside the handler
        const session = await sessionPromise()
        // do something with the message and session
      })
    })
  2. How Fastify hooks interact with WebSocket routes

    main

    WebSocket routes respect Fastify's plugin encapsulation and lifecycle hooks, but with specific limitations:

    • Supported Hooks: Hooks that run before the connection is established (upgraded) will work. This includes onRequest, preParsing, preValidation, and preHandler. These are ideal for authentication.
    • Unsupported Hooks: Hooks related to response serialization and transmission (preSerialization, onSend) do not run for WebSocket routes because message handling occurs outside the standard HTTP lifecycle once the connection is upgraded.

    To transform outgoing messages, you must implement that logic manually within your handler before calling socket.send().

    fastify.addHook('preValidation', async (request, reply) => {
      // This works for authentication
      if (!request.isAuthenticated()) {
        await reply.code(401).send("not authenticated");
      }
    })
    
    fastify.get('/', { websocket: true }, (socket, req) => {
      // The connection only opens if preValidation passes
      socket.on('message', message => {
        // ...
      })
    })
  3. Use @fastify/websocket in a route

    main

    To enable WebSocket support on a specific route, register the @fastify/websocket plugin and add the websocket: true property to the routeOptions of a .get route. The handler will receive two arguments: the socket (the WebSocket connection) and the req (the FastifyRequest object).

    Note: If you do not register a route with websocket: true, the server will respond with a 404 error for incoming upgrade requests on that path. You can use a wildcard route (e.g., /*) to provide a default WebSocket handler.

    'use strict'
    
    const fastify = require('fastify')()
    fastify.register(require('@fastify/websocket'))
    fastify.register(async function (fastify) {
      fastify.get('/', { websocket: true }, (socket /* WebSocket */, req /* FastifyRequest */) => {
        socket.on('message', message => {
          socket.send('hi from server')
        })
      })
    })
    
    fastify.listen({ port: 3000 }, err => {
      if (err) {
        fastify.log.error(err)
        process.exit(1)
      }
    })
  4. Configure TypeScript for @fastify/websocket

    main

    If you are using TypeScript, the package includes built-in types, but you must also install the types for the underlying ws package as a development dependency.

    If you are using TypeScript with Yarn 2, you must also add a packageExtension to your .yarnrc.yml file to ensure the fastify peer dependency is correctly recognized.

    npm i @types/ws -D
    # or
    yarn add -D @types/ws
    packageExtensions:
      "@fastify/websocket@*":
        peerDependencies:
          fastify: "*"
  5. Test WebSocket endpoints with injectWS

    main

    The @fastify/websocket plugin decorates the Fastify instance with injectWS, which simplifies testing WebSocket endpoints.

    Usage:

    • Call fastify.injectWS(path, [upgradeContext]).
    • fastify.ready() must be awaited before calling injectWS to ensure decorations are applied.
    • You must manually close/terminate the WebSocket at the end of your test.
    • Register your event listeners (e.g., ws.on('message', ...)) before sending messages to ensure you can capture the response.
    'use strict'
    const { test } = require('node:test')
    const fastify = require('./app.js')
    
    test('connect to /', async (t) => {
      t.plan(1)
    
      t.after(() => fastify.close())
      await fastify.ready()
    
      // Inject WebSocket connection
      const ws = await fastify.injectWS('/', {headers: { "api-key" : "some-random-key" }})
    
      let resolve;
      const promise = new Promise(r => { resolve = r })
    
      ws.on('message', (data) => {
        resolve(data.toString());
      })
      
      ws.send('hi from client')
    
      t.assert.deepStrictEqual(await promise, 'hi from server')
      
      // Manual cleanup
      ws.terminate()
    })
  6. Configure a custom preClose hook

    main

    By default, all WebSocket connections are closed when the server closes. You can override this by providing a preClose function in the plugin registration options. The preClose function is responsible for closing all active connections and closing the WebSocket server itself.

    const fastify = require('fastify')()
    
    fastify.register(require('@fastify/websocket'), {
      preClose: (done) => {
        // Access the websocket server via 'this' (if using regular functions)
        const server = this.websocketServer
    
        for (const socket of server.clients) {
          socket.close(1001, 'WS server is going offline')
        }
    
        server.close(done)
      }
    })
  7. Configure custom error handler for WebSocket connections

    main

    You can provide a custom errorHandler when registering the @fastify/websocket plugin. This handler is called when:

    1. An error is thrown by your WebSocket route handler after the connection is established.
    2. An error event is emitted by the WebSocket connection itself (e.g., an unclean client disconnect).

    Important Notes:

    • This errorHandler is not the same as Fastify's setErrorHandler or onError hook. Fastify's standard error handlers only work for errors encountered before the connection is upgraded (e.g., in onRequest or preValidation).
    • Neither the plugin's errorHandler nor Fastify's onError will catch errors thrown inside your socket.on('message', ...) handlers. You must use try/catch blocks within your message handlers to manage those errors.
    const fastify = require('fastify')()
    
    fastify.register(require('@fastify/websocket'), {
      errorHandler: function (error, socket /* WebSocket */, req /* FastifyRequest */, reply /* FastifyReply */) {
        // Handle cleanup or connection termination
        socket.terminate()
      },
      options: {
        maxPayload: 1048576,
        verifyClient: function (info, next) {
          if (info.req.headers['x-fastify-header'] !== 'fastify is awesome !') {
            return next(false)
          }
          next(true)
        }
      }
    })
    
    fastify.get('/', { websocket: true }, (socket, req) => {
      socket.on('message', message => {
        socket.send('hi from server')
      })
    })
    
    fastify.listen({ port: 3000 })
  8. Configure @fastify/websocket options

    main

    When registering @fastify/websocket, you can pass options that are passed directly to the underlying ws WebSocket server.

    Important Constraints:

    • Routing: Do NOT provide the path option from ws, as routing is managed by Fastify.
    • Server Binding: Do NOT provide the noServer option. By default, @fastify/websocket binds to your scoped Fastify instance. If you need a custom server, use the server option instead.
    • Object Mode: The ws library does not support setting objectMode or writableObjectMode to true.
    // Example of passing options during registration
    fastify.register(require('@fastify/websocket'), {
      maxPayload: 1024 * 1024, // 1MB
      clientTracking: true
    })
  9. Register @fastify/websocket plugin

    main

    To enable WebSocket support in your Fastify application, register the @fastify/websocket plugin. This plugin decorates the Fastify instance with a websocketServer and the request object with a ws property to identify WebSocket connections. It also allows you to define routes specifically for WebSockets using the wsHandler option.

    const fastify = require('fastify')()
    const websocket = require('@fastify/websocket')
    
    fastify.register(websocket)
    
    fastify.get('/ws', { websocket: true }, (connection, req) => {
      // connection is the WebSocket socket
      // req is the Fastify request
      connection.socket.send('hello')
    })
    
    fastify.listen({ port: 3000 })
  10. Handle both HTTP and WebSocket requests on the same route

    main

    If you need a single route to handle both standard HTTP requests and WebSocket connections, use the Fastify full declaration syntax by providing both a handler and a wsHandler property in the route configuration.

    'use strict'
    
    const fastify = require('fastify')()
    
    function handle (socket, req) {
      socket.on('message', (data) => socket.send(data))
    }
    
    fastify.register(require('@fastify/websocket'), {
      handle,
      options: { maxPayload: 1048576 }
    })
    
    fastify.register(async function () {
      fastify.route({
        method: 'GET',
        url: '/hello',
        handler: (req, reply) => {
          // Handles HTTP requests
          reply.send({ hello: 'world' })
        },
        wsHandler: (socket, req) => {
          // Handles WebSocket connections
          socket.send('hello client')
    
          socket.once('message', chunk => {
            socket.close()
          })
        }
      })
    })
    
    fastify.listen({ port: 3000 }, err => {
      if (err) {
        fastify.log.error(err)
        process.exit(1)
      }
    })
  11. Create a stream from a WebSocket connection

    main

    You can convert a WebSocket connection into a Node.js stream using ws.createWebSocketStream. This allows you to use standard stream methods like .write() and listen to 'data' events.

    const Fastify = require('fastify')
    const FastifyWebSocket = require('@fastify/websocket')
    const ws = require('ws')
    
    const fastify = Fastify()
    await fastify.register(FastifyWebSocket)
    
    fastify.get('/', { websocket: true }, (socket, req) => {
      const stream = ws.createWebSocketStream(socket, { /* options */ })
      stream.setEncoding('utf8')
      stream.write('hello client')
    
      stream.on('data', function (data) {
        // Handle incoming data
      })
    })
    
    await fastify.listen({ port: 3000 })