Handling WebSocket events

Learn how to intercept and mock WebSocket events.

MSW supports intercepting and mocking WebSocket connections using its designated ws API. This page will guide you through the basics of handling WebSocket events, explain the mental model behind MSW when intercepting duplex connections, and elaborate on the defaults the library ships to promote good developer experience.

Respecting standards

Mock Service Worker is dedicated to respecting, promoting, and teaching you about the web standards. The way you intercept and mock WebSocket communications will be according to the WHATWG WebSocket Standard, which means treating clients as EventTarget, listening to events like "message" and "close", and reading the sent and received data from the MessageEvent objects.

We do not plan to support custom WebSocket protocols, such as those using HTTP polling. Those are proprietary to the third-party tooling that implements them, and there is no reliable way for MSW to intercept such protocols without introducing non-standard, library-specific logic.

That being said, we acknowledge that the standard WebSocket interface is rarely used in production systems as-is. Often, it’s used as the underlying implementation detail for more convenient third-party abstractions like SocketIO or PartyKit. We firmly believe in mock-as-you-use philosophy and want to provide you with the mocking experience that resembles the actual usage of the third-party libraries you may be relying on through the concept of Bindings.

Event types

Unlike HTTP, a WebSocket communication is duplex, which means that the client and the server may send events independently. There are two types of events you can handle with MSW:

  • Outgoing client events. These are the events the client sends via .send();
  • Incoming server events. These are the events the server sends and the client receives via its "message" event listener.

Intercepting connections

To support the duplex nature of the WebSocket communication and allow you to intercept both client-sent and server-sent events, MSW effectively acts as a middleware layer that sits between your client and a WebSocket server.

client ⇄ MSW ⇄ server

You are in control of how you want to utilize MSW. It can become a full substitute for a WebSocket server in a mock-first development, act as a proxy to observe and modify the events coming from the production server, or emulate client-sent events to test various server behaviors.

Handling WebSocket events starts by defining the server URL that the client connects to. This is done using the ws.link() method.

import { ws } from 'msw'
 
const chat = ws.link('wss://chat.example.com')

You can use the same URL predicate for WebSocket as you use for the http handlers: relative and absolute URLs, regular expressions, and paths with parameters and wildcards.

Next, add an event handler to the list of your handlers:

export const handlers = [
  chat.on('connection', () => {
    console.log('outgoing WebSocket connection')
  }),
]

You will be handling both client-sent and server-sent events within the "connection" event listener.

Important defaults

MSW implements a set of default behaviors to ensure good developer experience in different testing and development scenarios concerning WebSockets. You can opt-out from all of those, and fine-tune the interception behavior to suit your needs.

Client connections

By default, no intercepted WebSocket connections are opened. This encourages mock-first development and makes it easier to manage connections to non-existing servers. You can establish the actual server connection by calling server.connect().

Client-to-server event forwarding

By default, once you establish the actual server connection, outgoing client events are forwarded to the original server. If the server connection hasn’t been established, no forwarding occurs (nowhere to forward). You can opt-out from this behavior by calling event.preventDefault() on the client message event.

Learn more about client-to-server forwarding.

Server-to-client event forwarding

By default, once you establish the actual server connection, all incoming server events are forwarded to the client. You can opt-out from this behavior by calling event.preventDefault() on the server message event.

Learn more about server-to-client forwarding.

Client events

Intercepting client events

To intercept an outgoing client event, grab the client object from the "connection" event listener argument and add a "message" listener on that object.

chat.on('connection', ({ client }) => {
  client.addEventListener('message', (event) => {
    console.log('from client:', event.data)
  })
})

Now, whenever a WebSocket client sends data via the .send() method, the "message" listener in this handler will be called. The listener exposes a single event argument, which is a MessageEvent received from the client, with the sent data available as event.data.

Sending data to the client

To send data to the connected client, grab the client object from the "connection" event listener argument and call its .send() method with the data you wish to send.

chat.on('connection', ({ client }) => {
  client.send('Hello from the server!')
})

MSW supports sending strings, Blob, and ArrayBuffer.

client.send(data)

The `client.send()` API.

Broadcasting data to clients

To broadcast data to all connected clients, use the .broadcast() method on the event handler object (the one returned from the ws.link() call) and provide it with the data you wish to broadcast.

chat.on('connection', () => {
  chat.broadcast('Hello everyone!')
})

You can also broadcast data to all clients except a subset of clients by using the .boardcastExcept() method on the event handler object.

chat.on('connection', ({ client }) => {
  // Broadcast data to all clients except the current one.
  chat.broadcastExcept(client, 'Hello everyone except you!')
 
  // Broadcast data to all the clients matching a predicate.
  chat.boardcastExcept(chat.clients.filter((client) => {
    return client
  }, "Hello to some of you!")
})

.broadcast(data)

The `.broadcast()` API.

.broadcastExcept(clients, data)

The `.broadcastExcept()` API.

Closing client connections

You can close an existing client connection at any time by calling client.close().

chat.on('connection', ({ client }) => {
  client.close()
})

By default, the .close() method will result in a graceful closure of the connection (1000 code). You can control the nature of the connection closure by providing the custom code and reason arguments to the .close() method.

chat.on('connection', ({ client }) => {
  client.addEventListener('message', (event) => {
    if (event.data === 'hello') {
      client.close(1003)
    }
  })
})

For example, in this handler, once the client sends a "hello" message, its connection will be terminated with the 1003 code (received data the server cannot accept).

Unlike the WebSocket.prototype.close() method, the .close() method on the client connection can accept even non user-configurable closure codes like 1001, 1002, 1003, etc, which gives you more flexibility in describing the WebSocket communication.

client.close(code, reason)

The `client.close()` API.

Server events

Establishing server connection

To establish the connection to the actual WebSocket server, grab the server object from the "connection" event listener argument and call its .connect() method.

chat.on('connection', ({ server }) => {
  server.connect()
})

server.connect()

The `server.connect()` API.

Client-to-server forwarding

Once the server connection has been established, all outgoing client message events are forwarded to the server. To prevent this behavior, call event.preventDefault() on the client message event. You can use this to modify the client-sent data before it reaches the server or ignore it completely.

chat.on('connection', ({ client }) => {
  client.addEventListener('message', (event) => {
    // Prevent the default client-to-server forwarding.
    event.preventDefault()
 
    // Modify the original client-sent data and send
    // it to the server instead.
    server.send(event.data + 'mocked')
  })
})

Intercepting server events

To intercept an incoming event from the actual sever, grab the server object from the "connection" event listener argument and add a "message" event listener on that object.

chat.on('connection', ({ server }) => {
  server.addEventListener('message', (event) => {
    console.log('from server:', event.data)
  })
})

Now, whenever the actual server sends data, the "message" listener in this handler will be called. The listener exposes a single event argument, which is a MessageEvent received from the client, with the sent data available as event.data.

Server-to-client forwarding

By default, all server events are forwarded to the connected client. You can opt-out from this behavior by calling event.preventDefault() on the server message event. This is handy if you wish to modify the server-sent data before it reaches the client or prevent some server events from arriving at the client completely.

chat.on('connection', ({ client, server }) => {
  server.addEventListener('message', (event) => {
    // Prevent the default server-to-client forwarding.
    event.preventDefault()
 
    // Modify the original server-sent data and send
    // it to the client instead.
    client.send(event.data + 'mocked')
  })
})

Sending data to the server

To send data to the actual server, grab the server object from the "connection" event listener argument and call its .send() method with the data you wish to send to the server.

chat.on('connection', ({ server }) => {
  server.send('hello from client!')
}

This is equivalent to a client sending that data to the server.

server.send(data)

The `server.send()` API.

Client-to-server forwarding

By default, the actual server will not receive any outgoing client events—they will short-circuit on your event handler’s level. If you wish to forward client-to-server events, establish the actual server connection by calling server.connect(), listen to the outgoing events via the "message" event listener on the client object, and use the server.send() method to forward the data.

chat.on('connection', ({ client, server }) => {
  // Establish the actual server connection.
  server.connect()
 
  // Listen to all outgoing client events.
  client.addEventListener('message', (event) => {
    // And send them to the actual server as-is.
    server.send(event.data)
  })
})

You can control what messages to forward to the actual server in the "message' event listener on the client object. Feel free to introduce conditions, analyze the message, or modify the data to forward.

Logging

Since MSW implements the WebSocket interception mock-first, no actual connections will be established until you explicitly say so. This means that the mocked scenarios won’t appear as network entries in your browser’s DevTools and you won’t be able to observe them.

MSW enables custom logging for both mocked and original WebSocket connections in the browser to allow you to:

  • Observe any WebSocket connection if you have an event handler for it;
  • See previews for binary messages (Blob/ArrayBuffer);
  • Tell apart the messages sent by your application/original server from those initiated by the event handler (i.e. mocked messages).

Message colors and styles

All the printed messages are color-graded by the following criteria: system events, outgoing messages, incoming messages, and mocked events. In addition to the colors, some messages are represented by solid (↑↓) or dotted icons (⇡⇣), indicating whether the event occurred in your application or in the event handler, respectively.

Connection events

Connection opened

[MSW] 12:34:56 ▶ wss://example.com

Dispatched when the connection is open (i.e. the WebSocket client emits the open event).

× Connection errored

[MSW] 12:34:56 × wss://example.com

Dispatched when the client receives an error (i.e. the WebSocket client emits the error event).

Connection closed

[MSW] 12:34:56 ■ wss://example.com

Dispatched when the connection is closed (i.e. the WebSocket client emits the close event).

Message events

Any message, be it outgoing or incoming message, follows the same structure:

      ▼ timestamp    ▼ sent data      ▼ data length (bytes)
[MSW] 00:00:00.000 ↑ hello from client 17
                   ▲ icon

Binary messages print a text preview of the sent binary alongside its full byte length:

[MSW] 12:34:56.789 ↑ Blob(hello world) 11
[MSW] 12:34:56.789 ↑ ArrayBuffer(preview) 7

Long text messages and text previews are truncated:

[MSW] 12:34:56.789 ↑ this is a very long stri… 17

You can access the full message by clicking on its console group and inspecting the original MessageEvent reference.

Outgoing client message

[MSW] 12:34:56.789 ↑ hello from client 17

A message sent by the client in your application.

Outgoing mocked client message

[MSW] 12:34:56.789 ⇡ hello from mock 15

A message sent from the client by the event handler (via server.send()). Requires the actual server connection to be opened via server.connect(). The client itself never sent this, thus the icon is dotted.

Incoming client message

[MSW] 12:34:56.789 ↓ hello from server 17

The end message the client received (i.e. the message that triggered the “message” event on the WebSocket client). The message can be either from the event handler or from the actual WebSocket server.

Incoming mocked client message

[MSW] 12:34:56.789 ⇣ hello from mock 15

A mocked message sent to the client from the event handler via client.send(). The actual server has never sent this, thus the icon is dotted.

Incoming server message

[MSW] 12:34:56.789 ⇣ hello from server 17

An incoming message from the actual server. Requires the actual server connection to be opened via server.connect(). The incoming server messages can be modified or skipped by the event handler, thus the icon is dotted.

Bindings

To provide a more familiar experience when mocking third-party WebSocket clients, MSW uses bindings. A binding is a wrapper over the standard WebSocket class that encapsulates the third-party-specific behaviors, such as message parsing, and gives you a public API similar to that of the bound third-party library.

For example, here’s how to handle SocketIO communication using MSW and a designated SocketIO binding:

import { ws } from 'msw'
import { bind } from '@mswjs/socket.io-binding'
 
const chat = ws.link('wss://chat.example.com')
 
export const handlers = [
  chat.on('connection', (connection) => {
    const io = bind(connection)
 
    io.client.on('hello', (username) => {
      io.client.emit('message', `hello, ${username}!`)
    })
  }),
]

@mswjs/socket.io-binding

Connection wrapper for mocking Socket.IO with MSW.

Note that binding is not meant to cover all the public APIs of the respective third-party library. Unless the binding is shipped by that library, maintaining full compatibility is not feasible.