- Introduction
- Getting started
- Philosophy
- Comparison
- Limitations
- Debugging runbook
- FAQ
- Basics
- Concepts
- Network behavior
- Integrations
- API
- CLI
- Best practices
- Recipes
- Cookies
- Query parameters
- Response patching
- Polling
- Streaming
- Network errors
- File uploads
- Responding with binary
- Custom worker script location
- Global response delay
- GraphQL query batching
- Higher-order resolver
- Keeping mocks in sync
- Merging Service Workers
- Mock GraphQL schema
- Using CDN
- Using custom "homepage" property
- Using local HTTPS
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 working with duplex connections, and elaborate on the defaults the library ships to ensure great 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 have no plans of supporting custom WebSocket protocols, such as those using HTTP polling or XMLHttpRequest. 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 aim to address that experience gap via Bindings.
Event types
A WebSocket communication is duplex, which means that both the client and the server may send and receive events independently and simultaneously. There are two types of events you can handle with MSW:
- Outgoing client events. These are the events your application sends to the WebSocket server;
- Incoming server events. These are the events the original 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.addEventListener('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, get the client
object from the "connection"
event listener argument and add a "message"
listener on that object.
chat.addEventListener('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, get the client
object from the "connection"
event listener argument and call its .send()
method with the data you wish to send.
chat.addEventListener('connection', ({ client }) => {
client.send('Hello from the server!')
})
MSW supports sending strings,
Blob
, andArrayBuffer
.
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.addEventListener('connection', () => {
chat.broadcast('Hello everyone!')
})
You can also broadcast data to all clients except a subset of clients by using the .broadcastExcept()
method on the event handler object.
chat.addEventListener('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.broadcastExcept(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.addEventListener('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.addEventListener('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 handle any events from the actual WebSocket server, you must connect to that server first.
To establish the connection to the actual WebSocket server, get the server
object from the "connection"
event listener argument and call its .connect()
method.
chat.addEventListener('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.addEventListener('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, get the server
object from the "connection"
event listener argument and add a "message"
event listener on that object.
chat.addEventListener('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.addEventListener('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, get 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.addEventListener('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.
Closing server connections
You can close a previously established original WebSocket server connection by calling server.close()
.
chat.addEventListener('connection', ({ server }) => {
server.connect()
server.close()
})
Calling server.close()
will close the server connection and dispatch the close
event on the server
object.
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 there.
MSW provides custom logging for both mocked and original WebSocket connections in the browser.
Reading the log output
The logger will print out various events occurring during the WebSocket communication as collapsed console groups in your browser’s console.
There are four types of logs you can observe:
-
▶■×System events;
-
⬆⇡Client events;
-
⬇⇣Server events.
-
⬆⬇Mocked events.
System 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
[MSW] 00:00:00.000 ⬆ hello from client 17
icon byte length
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
instance.
⬆⇡ Outgoing client message
[MSW] 12:34:56.789 ⬆ hello from client 17
A raw message sent by the WebSocket client in your application. If the arrow is dashed, the forwarding of this message event has been prevented in the event handler.
⬆ 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 an open server connection.
⬇⇣ Incoming server message
[MSW] 12:34:56.789 ⬇ hello from server 17
An incoming message sent from the original server. Requires an open server connection. If the arrow is dashed, the forwarding of this message event has been prevented in the event handler.
⬇ Incoming mocked server message
[MSW] 12:34:56.789 ⬇ hello from mock 15
A mocked message sent to the client from the event handler via client.send()
.
Event flow
Much like the WebSocket communication, handling it with MSW is event-based. Your experience mocking WebSockets will involve the understanding of EventTarget
and how events work in JavaScript. Let’s have a quick reminder.
When your application establishes a WebSocket connection, the connection
event will be emitted on all matching WebSocket links.
const chat = ws.link('wss://example.com')
export const handlers = [
chat.addEventListener('connection', () => console.log('This is called')),
chat.addEventListener('connection', () => console.log('This is also called')),
]
This way, both the happy path and the runtime handlers can react to the same connection.
The client/server events are dispatched on all client
and server
objects of the same connection as well. Meanwhile, you can attach multiple listeners to the same object, or to different objects across different handlers.
export const handlers = [
chat.addEventListener('connection', ({ client }) => {
// Attaching multiple message listeners to the same `client` object.
client.addEventListener('message', () => console.log('This is called'))
client.addEventListener('message', () => console.log('This is also called'))
}),
chat.addEventListener('connection', ({ client }) => {
// Attaching another message listener to a different `client` object.
client.addEventListener('message', () =>
console.log('Hey, this gets called too!'),
)
}),
]
Since these events are dispatched against the same event target, you can utilize that to prevent them. That comes in handy when creating runtime handlers (i.e. network behavior overrides), as you can control whether your override augments or completely overrides particular event handling.
const server = setupServer(
chat.addEventListener('connection', ({ client }) => {
client.addEventListener('message', (event) => {
// In a happy path handler, send back the event data
// received from the WebSocket client.
client.send(event.data)
})
}),
)
it('handles error payload', async () => {
server.use(
chat.addEventListener('connection', ({ client }) => {
client.addEventListener('message', (event) => {
// In this runtime handler, prevent the "message" client from
// propagating to another event target (the happy path handler).
// Then, send a completely different message to the client.
event.stopPropagation()
client.send('error-payload')
})
}),
)
})
Omitting
event.stopPropagation()
will result in two messages being sent to the client upon receiving the same event—the'error-payload'
first, then the originalevent.data
second.
Just like with a regular EventTarget
, you can utilize event.preventImmediatePropagation()
to stop an event from propagating across sibling listeners. For example, when handling a particular WebSocket event, you can use that to short-circuit any other event listeners that would otherwise be called.
chat.addEventListener('connection', ({ client }) => {
client.addEventListener('message', (event) => {
if (event.data === 'special-scenario') {
// This prevents this "message" event from propagating
// to the "message" event listener below.
event.stopImmediatePropagation()
client.close()
}
})
client.addEventListener('message', (event) => {
client.send(event.data)
})
})
If the client sends a
'special-scenario'
payload in the message, it will be closed, and theclient.send(event.data)
logic from the second event listener will never be called.
Type safety
Bringing type safety to the WebSocket communication is essential when using TypeScript, and that includes your handlers too! That being said, MSW intentionally doesn’t support any type arguments to annotate the outgoing/incoming events:
import { ws } from 'msw'
ws.link<Arg1, Arg2>(url)
// ^^^^^^^^^^^^ Type error!
The reasoning behing this decision is twofold:
- Narrowing down the data type doesn’t guarantee that a different data type wouldn’t be sent over the network (the classic types vs runtime debate);
- The
event.data
value you receive in the message event listener will always be of typestring | Blob | ArrayBuffer
because MSW provides no message parsing.
If you are using objects to communicate with a WebSocket server, those objects have to be stringified and parsed when sending and receiving them, respectively, which already implies a parsing layer being present in your application.
You can achieve a proper type and runtime safety in WebSockets by introducing parsing utilities. Libraries like Zod can help you greatly in achieving type and runtime safety.
import { z } from 'zod'
// Define a Zod schema for the incoming events.
// Here, our WebSocket communication supports two
// events: "chat/join" and "chat/message".
const incomingSchema = z.union([
z.object({
type: z.literal('chat/join'),
user: userSchema,
}),
z.object({
type: z.literal('chat/message'),
message: z.object({
text: z.string(),
sentAt: z.string().datetime(),
}),
}),
])
chat.addEventListener('connection', ({ client, server }) => {
client.addEventListener('message', (event) => {
const result = incomingSchema.safeParse(event.data)
// Ignore non-matching events.
if (!result.success) {
return
}
const message = result.data
// Handle incoming events in type-safe way.
switch (message.type) {
case 'chat/join': {
// ...
break
}
case 'chat/message': {
// ...
break
}
}
})
})
Feel free to introduce a higher-order listener for the message event that abstracts that parsing, helping you reuse it across your handlers.
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 Socket.IO communication using MSW and a designated SocketIO binding:
import { ws } from 'msw'
import { toSocketIo } from '@mswjs/socket.io-binding'
const chat = ws.link('wss://chat.example.com')
export const handlers = [
chat.addEventListener('connection', (connection) => {
const io = toSocketIo(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.