0

I'm trying to test (using Vitest) a part of my Fastify app that uses @fastify/websocket (and transitively ws), but my tests keep timing out despite running to completion. Here's a relevant excerpt from my test suite:

type CloseMessage = {
  code: number,
  reason: Buffer,
};

describe('Signalling Endpoint Protocol', () => {
  let app: FastifyInstance;
  let ws: WebSocket;

  beforeEach(async () => {
    app = await buildApp();
    await app.ready();
    ws = await app.injectWS('/signal', {
      headers: {
        authorization: `Bearer ${MOCK_TOKEN}`,
        'sec-websocket-protocol': SIGNALLING_PROTOCOL,
      }
    });
  });

  afterEach(async () => {
    await app.close();
    ws?.terminate();
  });

  it('disconnects if the client sends malformed JSON', async () => {
    const { promise, resolve } = Promise.withResolvers<CloseMessage>();

    ws.once('close', (code, reason) => {
      resolve({ code, reason });
    });

    ws.send('fgsfds'); // Invalid JSON

    const { code } = await promise;

    // prints, but wrongly attributed to a different test
    // (the one that normally comes after this one)
    console.log(`awaited code ${code}`);

    expect(code).toBe(WebSocketStatus.INVALID_FRAME_PAYLOAD_DATA);
    expect(ws.readyState).toBe(WebSocket.CLOSED);
    console.log("tests are done");
    // runs to here, then times out
  });
});

The timeout happens some time between the ws.send() and await promise calls (as indicated by console.log). So the test times out, but then finishes.

Any idea why this happens? I'm using Fastify 5.4.0, @fastify/websocket 11.2.0, Node 22.13.1, and Vitest 3.2.4.

2
  • I would try to disable the vitest parallelism feature first - it seems that the it test runs in parallel and everyone use the same ws thing - why don't isolate it by creating a factory instead of using before/after? Commented Aug 13 at 8:16
  • @ManuelSpigolon I'm not using parallelism in my tests, but disabling it explicitly does not change the results. I made a minimal reproduction here using the built-in test runner instead of Vitest, but the issue persists. Commented Aug 13 at 22:24

1 Answer 1

1

The issue is the 30 sec close timeout:

  // Use `WebSocket#terminate()`, which immediately destroys the connection,
  // instead of `WebSocket#close()`, which waits for the close timer.

So your code should be like:

  fastify.get('/signal', {
    websocket: true,
    preHandler: [ /* some handlers for authentication and validation (can post if relevant) */],
  }, async (socket, request) => {
    socket.on('message', async (data) => {
      try {
        console.log("Got a message");
        const message = JSON.parse(data.toString());
        console.log(message);
      } catch (error) {
        if (error instanceof SyntaxError) {
          console.debug("Error parsing message:");
          // If the message isn't valid JSON...
          socket.close(1007, 'Invalid JSON format');
+          socket.terminate();
          return;
        }
      }
    });
  });

As side note, I would expect the close handshake to complete immediately, so it may be a bug here too: https://github.com/fastify/fastify-websocket/ but I will check

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.