Debugging runbook

Debug common issues with MSW.

Below you can find the most common issues that developers experience when integrating Mock Service Worker into their applications. Please read through this page before opening an issue on GitHub as there’s a decent chance there is an answer to your issue below.

Before you begin

Check Node.js version

Check the version of Node.js your project is using:

node -v

If it’s lower than Node.js v18, upgrade to the latest Node.js version. We do not look into issues happening on unsupported versions of Node.js.

Check MSW version

First, check what version of the msw package you have installed:

npm ls msw

Then, check the latest publish version:

npm view msw version

If these two differ, upgrade the msw version in your project and see if the issue persists.

Debugging runbook

You’ve checked the environment and MSW versions but the issue still persists. It’s time to do some debugging. Below, you can find a step-by-step debugging runbook to follow when experiencing any unexpected behavior with MSW.

Step 1: Verify setup

First, verify that MSW is correctly set up. Take the worker/server instance and add a new request:start life-cycle event listener to it.

server.events.on('request:start', ({ request }) => {
  console.log('Outgoing:', request.method, request.url)
})

You can learn more about the Life-cycle events API.

With this listener in place, you should see the console message on every outgoing request that MSW intercepts. The console message should look like this:

Outgoing: GET https://api.example.com/some/request
Outgoing: POST http://localhost/post/abc-123

You must see the request’s method and the absolute request URL. If the request method is different, adjust your request handler to reflect it. If you see a relative request URL, your request client or your testing environment are not configured correctly. You must configure the base URL option of your request client to produce absolute request URLs, and your test environment to have document.baseURI set.

Otherwise, verify that the problematic request is printed. If it is, continue to the next step.

If the problematic request is the only request your application makes, try adding a dummy fetch call anywhere after MSW setup to confirm this step. For example:

fetch('https://example.com')

If there’s no message printed for the problematic request (or any requests), MSW is likely not being set up correctly and cannot intercept the requests. Please refer to the Integration instructions and make sure you are setting up the library as illustrated there.

Step 2: Verify handler

Go to the request handler you’ve created for the problematic request and add a console statement in its resolver function.

// src/mocks/handlers.js
import { http } from 'msw'
 
export const handlers = [
  http.get('/some/request', ({ request }) => {
    console.log('Handler', request.method, request.url)
 
    // The rest of the response resolver here.
  }),
]

You should see this console message when the problematic request happens on the page/tests. If you do, continue to the next step.

If there’s no message, MSW is able to intercept the request but cannot match it against this handler. This likely means your request handler’s predicate doesn’t match the actual request URL. Verify that the predicate is correct. Some of the common issues include:

  • Using an environment variable in the path, which is not set in tests/CI (e.g. http.get(BASE_URL + '/path')). Inspect any dynamic segments of the request path and make sure they have expected values;
  • Typos in the request path. Carefully examine the request printed in the previous step of this runbook and find any typos/mistakes in it.

If unsure, please read through the documentation on intercepting requests with MSW:

Intercepting requests

Learn about request interception and how to capture REST and GraphQL requests.

Step 3: Verify response

If the request handler is invoked but the request still doesn’t get the mocked response, the next place to check is the mocked response itself. In the request handler, jump to the mock response(s) you define.

// src/mocks/handlers.js
import { http, HttpResponse } from 'msw'
 
export const handlers = [
  http.get('/some/request', ({ request }) => {
    console.log('Handler', request.method, request.url)
 
    return HttpResponse.json({ mocked: true })
  }),
]

Verify that you are constructing a valid response. You can assign the response to a variable and print it out to inspect. You can also do an early return of a dummy mocked response and see if it’s received by your application.

If unsure, please read about mocking responses with MSW:

Mocking responses

Learn about response resolvers and the different ways to respond to a request.

If you haven’t uncovered any issues with the mocked response, proceed to the next step.

Step 4: Verify application

If none of the previous steps have proven fruitful, it’s likely the issue is in the request/response handling logic of your application. Go to the source code that performs the request and handles the response, and verify they are correct. Please follow the guidelines of your request framework carefully to make sure you are performing requests as intended.

If the problem persists, open a new issue on GitHub and provide a minimal reproduction repository. Issues without the reproduction repository where the problem can be reliably reproduced will be automatically closed.

Common issues

ReferenceError: fetch is not defined

This error indicates that the global fetch function is not defined in the current process. This may happen for two reasons:

  1. You are using an older version of Node.js (< v17);
  2. Your environment suppresses that global function.

Upgrading Node.js

First, check what version of Node.js you are using by running this command in the same terminal you received the fetch error:

node -v

The newer Node.js versions ship with the global Fetch API, which includes the global fetch function.

Fixing environment

Some tools, like Jest, meddle with your Node.js environment, forcefully removing present globals. If you are using such tools, make sure you add those globals back in their configurations.

Here’s an example of how to configure Jest to work with the global Fetch API in Node.js.

This issue is caused by your environment not having the Node.js globals for one reason or another. This commonly happens in Jest because it intentionally robs you of Node.js globals and fails to re-add them in their entirely. As the result, you have to explicitly add them yourself.

Create a jest.polyfills.js file next to your jest.config.js with the following content:

// jest.polyfills.js
/**
 * @note The block below contains polyfills for Node.js globals
 * required for Jest to function when running JSDOM tests.
 * These HAVE to be require's and HAVE to be in this exact
 * order, since "undici" depends on the "TextEncoder" global API.
 *
 * Consider migrating to a more modern test runner if
 * you don't want to deal with this.
 */
 
const { TextDecoder, TextEncoder } = require('node:util')
 
Object.defineProperties(globalThis, {
  TextDecoder: { value: TextDecoder },
  TextEncoder: { value: TextEncoder },
})
 
const { Blob, File } = require('node:buffer')
const { fetch, Headers, FormData, Request, Response } = require('undici')
 
Object.defineProperties(globalThis, {
  fetch: { value: fetch, writable: true },
  Blob: { value: Blob },
  File: { value: File },
  Headers: { value: Headers },
  FormData: { value: FormData },
  Request: { value: Request },
  Response: { value: Response },
})

Make sure to install undici. It’s the official fetch implementation in Node.js.

Then, set the setupFiles option in jest.config.js to point to the newly created jest.polyfills.js:

// jest.config.js
module.exports = {
  setupFiles: ['./jest.polyfills.js'],
}

If you find this setup cumbersome, consider migrating to a modern testing framework, like Vitest, which has none of the Node.js globals issues and provides native ESM support out of the box.


Mock responses don’t arrive at tests

HTTP requests have asynchronous nature. When testing code that depends on the resolution of those requests, like a UI element that renders once the response is received, you need to account for that asynchronicity. This often means using the right tools of your testing framework to properly await UI elements.

// test/suite.test.ts
import { render, screen } from '@testing-library/react'
import { Welcome } from '../components/Welcome'
 
it('renders the welcome text', async () => {
  render(<Welcome />)
 
  // Make sure to use "findBy*" methods that will attempt
  // to look up an element multiple times before throwing.
  // You can also use "waitFor" as an alternative.
  await screen.findByText('Hello, Jack')
})

Do not introduce arbitrary setTimeout/sleep functions because they subject your tests to race conditions! The only reliable way to await asynchronous code is to await the state that derives from it (i.e. that a certain UI element has appeared in the DOM).


Receiving stale mock responses

Modern request libraries, like SWR, React Query, or Apollo, often introduce cache to guarantee great user experience and optimal runtime performance. Note that caching is not automatically disabled while testing, which may lead to your tests receiving stale/wrong data across different test suites.

Please refer to your request library’s documentation on how to correctly disable cache in tests.

For example, here’s how to disable cache using SWR:

// test/suite.test.ts
import { cache } from 'swr'
 
beforeEach(() => {
  // Reset the cache before each new test so there are
  // no stale responses when requesting same endpoints.
  cache.clear()
})

Requests are not resolving when using jest.useFakeTimers

When using fake timers in Jest, all timer APIs are mocked, including queueMicrotask. The queueMicrotask API is used internally by the global fetch in Node.js to parse request/response bodies. Because of this, when using jest.useFakeTimers() with the default configuration, body reading methods like await request.text() and await request.json() will not resolve properly.

For example, here’s how to prevent Jest from faking the queueMicrotask calls when using jest.useFakeTimers():

jest.useFakeTimers({
  // Explicitly tell Jest not to affect the "queueMicrotask" calls.
  doNotFake: ['queueMicrotask'],
})

Please refer to Jest’s documentation on fake timers.


RTK Query requests are not intercepted

A common mistake when using RTK Query is not setting the baseUrl in the baseQuery configuration. Without this, the requests will have relative URLs, which is a no-op in Node.js (see this issue for more details).

createApi({
  baseQuery: fetchBaseQuery({
    baseUrl: new URL('/your/api/endpoint', location.origin).href,
  }),
})