- 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
boundary()
Scope the network interception to the given boundary.
Call signature
The server.boundary()
function accepts a callback
function and returns a new function with the same call signature as the given callback
but bound.
function boundary<Callback extends (...args: Array<unknown>) => unknown>(
callback: Callback
): (...args: Parameters<Callback>) => ReturnType<Callback>
Usage
The server.boundary()
API is Node.js only, which means it can only be used
with setupServer()
. Network isolation in the browser is automatically
achieved by the client runtime (each tab is a separate, isolated runtime).
The server.boundary()
API is designed to provide the network behavior isolation. Any modifications to the request interception made within a boundary will only affect that boundary and nothing else.
import { HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer()
server.listen()
function app() {
fetch('https://example.com')
server.boundary(() => {
// This runtime handler override will only affect
// the network within this server boundary.
server.use(
http.get('https://example.com', () => {
return HttpResponse.error()
})
)
fetch('https://example.com')
})()
}
In the example above, the first fetch()
call will be handled by whichever request handlers initially provided to the setupServer()
call, which in this case is none. The same fetch()
call within the boundary, however, will receive a network error (HttpResponse.error()
) because the respective request handler override was added within the boundary.
Since the boundary provides scope isolation, you don’t need to reset the request handlers. You do need to reset them, however, if you wish to reset the network behavior within that server boundary.
The server.boundary()
API utilizes AsyncLocalStorage
, which means that all the network in the current scope and child scopes of the boundary will be affected by request handler overrides.
The server boundary accepts whichever arguments passed to the callback function and returns whatever that function returns. With that in mind, you can use it even in situations when the callback is expected to return something.
// Let's imagine you are using a server boundary
// in a backend framework that expects route handlers
// to return Fetch API Response instances.
router.get(
'/resource',
// This boundary will accept whatever arguments
// were given to it by "router.get", and return a
// new Response instance to the router.
server.boundary((request) => {
return new Response('Hello world')
})
)
Standalone
This API can be used standalone for various purposes, like a scoped network introspection, debugging, and development. In the example below, the server.boundary()
is used to introspect all the network requests that are happening as a part of the POST /resource
route handling in Express.
import express from 'express'
import { http } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer()
server.listen()
const app = express()
app.post('/resource', (req, res) => {
server.boundary(() => {
server.use(
http.all('*', ({ request }) => {
console.log(request.method, request.url)
})
)
handleRequest(req, res)
})()
})
Concurrent test runs
The server.boundary()
API is primarily designed to support concurrent test runs in modern test frameworks. Since the total list of request handlers is kept in-memory in the setupServer()
scope, this introduces a global state problem. If multiple concurrent tests call server.use()
, those request handler override will end up affecting irrelevant tests that run in parallel.
Introducing a server boundary in each test solves this problem and prevents request handler overrides from ever affecting irrelevant tests. Take a look at how server.boundary()
is used in practice in this concurrent test suite in Vitest:
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const server = setupServer(
http.get('https://example.com/user', () => {
return HttpResponse.json({ name: 'John' })
})
)
beforeAll(() => {
server.listen()
})
afterAll(() => {
server.close()
})
it.concurrent(
'fetches the user',
server.boundary(async () => {
// This test doesn't introduce any request handlers override.
// The network within this test will be resolved against the
// initial request handlers provided to "setupServer()" call.
const response = await fetch('https://example.com/user')
const user = await response.json()
expect(user).toEqual({ name: 'John' })
})
)
it.concurrent(
'handles the server error',
server.boundary(async () => {
// This test makes the user requests return a 500 response.
// Fetching the user in this scope, and any nested scopes,
// will always result in a 500 response.
server.use(
http.get('https://example.com/user', () => {
return new HttpResponse(null, { status: 500 })
})
)
const response = await fetch('https://example.com/user')
expect(response.status).toBe(500)
})
)
it.concurrent(
'handles network errors',
server.boundary(async () => {
// This test makes the user requests fail with a network error.
server.use(
http.get('https://example.com/user', () => {
return HttpResponse.error()
})
)
await expect(fetch('https://example.com/user')).rejects.toThrow(
'Failed to fetch'
)
})
)
Although each test case relies on a particular network state, using the server.boundary()
API allows to scope and “freeze” that state, resulting in predictable network behavior in concurrent test runs.
Nested boundaries
Whenever a server boundary is created, it treats whichever existing request handlers from the higher scope as the initial request handlers.
const server = setupServer(
http.get('https://example.com/user', () => {
return HttpResponse.json({ name: 'John' })
})
)
server.boundary(async () => {
// The user request will return a 200 JSON response
// as described in the initial request handlers
// provided to the "setupServer" call above.
await fetch('https://example.com/user')
})()
Any request handler overrides within the boundary are prepended to the initial list of request handlers, similar to how they are in the regular .use()
usage.
When a server boundary is nested within another server boundary, whichever request handler state the upper boundary has is treated as the initial state for the nested boundary.
const server = setupServer(
http.get('https://example.com/user', () => {
return HttpResponse.json({ name: 'John' })
})
)
// This server boundary has the following request handlers:
// - (initial) GET /user -> 200 OK
// - (override) POST /login -> 500 Internal Server Error
server.boundary(() => {
server.use(
http.post('https://example.com/login', () => {
return new HttpResponse(null, { status: 500 })
})
)
// This server boundary has the following request handlers:
// - (initial) GET /user -> 200 OK
// - (initial) POST /login -> 500 Internal Server Error
// - (override) DELETE /post -> 404 Not Found
server.boundary(() => {
server.use(
http.delete('https://example.com/post', () => {
return new HttpResponse(null, { status: 404 })
})
)
// Resetting the request handlers will remove any
// request handler overrides added in *this* boundary.
// The resulting request handlers will be:
// - (initial) GET /user -> 200 OK
// - (initial) POST /login -> 500 Internal Server Error
server.resetHandlers()
})()
})()