What this usually means
MSW's Service Worker or server-side interceptor isn't running when the request fires. The most common causes: the worker wasn't started (or was started too late), the handler URL pattern doesn't match the actual request URL (including query params, hash, or trailing slash), or the test framework resets handlers between tests without re-registering them. In browser environments, the worker registration can fail silently due to scope restrictions or HTTPS requirements. In Node.js (for tests), the setupServer or setupWorker call might be missing from the test's lifecycle hooks, or the handlers array might be empty at the time of the request.
The first ten minutes — establish facts before touching code.
- 1Check the browser console for '[MSW] Mocking enabled' – if absent, worker failed to register.
- 2Run `npx msw init public/` (or your public dir) to ensure the mockServiceWorker.js file exists and is served.
- 3In Node.js tests, add a `beforeAll(() => server.listen())` and `afterEach(() => server.resetHandlers())` – missing these is the #1 cause.
- 4Log the request URL inside the handler: `console.log(request.url)` and compare it with the actual request URL from the network tab.
- 5Verify the handler method: a POST handler won't catch a GET request even if the URL matches exactly.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchBrowser: DevTools → Console → filter by 'MSW'
- searchBrowser: DevTools → Application → Service Workers → check status and scope
- searchNetwork tab: the request that leaked shows real server response; find its URL and match against handlers
- searchNode.js tests: the test runner's output – MSW prints warnings about unmatched requests
- searchHandler file: the exact URL string(s) you passed to `http.get()`, `http.post()`, etc.
- searchWorker registration code: `worker.start()` – is it called before any fetch?
- searchCI logs: any errors from MSW during worker installation (especially in headless browsers)
Practical causes, not theory. These are the things you will actually find.
- warningWorker not started: `worker.start()` missing or called after the first fetch
- warningURL mismatch: handler path has trailing slash but request doesn't, or vice versa; query params not accounted for
- warningHTTP method mismatch: handler uses `http.post` but request is GET
- warningIncorrect worker scope: MSW worker scope must be set to or above the mocked endpoint path
- warningTest lifecycle: missing `beforeAll`/`afterEach` for server.listen/resetHandlers in vitest/jest
- warningMultiple MSW instances: two different workers registered (e.g., one from a library) causing conflict
- warningCDN/service worker cache: stale mockServiceWorker.js from a previous deployment
Concrete fix directions. Pick the one that matches your root cause.
- buildEnsure worker.start() is called once, before any fetch, ideally in an app entry point or test setup file.
- buildNormalize URLs: use `http.all('https://example.com/api/*', handler)` to catch any method and subpath, or explicitly handle trailing slashes.
- buildAdd a wildcard handler as default to catch all unhandled requests for debugging: `http.all('*', ({ request }) => console.log('Unhandled:', request.url))`.
- buildIn test suites, always call `server.resetHandlers()` after each test and `server.close()` after all tests.
- buildRe-register the service worker: go to DevTools → Application → Service Workers → Unregister, then reload the page.
A fix you cannot prove is a guess. Close the loop.
- verifiedOpen DevTools Console and confirm '[MSW] Mocking enabled.' appears.
- verifiedMake a test request and see '[MSW] <method> <url>' logged with a green checkmark.
- verifiedIn tests, assert that the mock response is returned: `expect(response.status).toBe(200)`.
- verifiedCheck the Network tab: the request should show a status of '(from service worker)' or no request at all if intercepted by MSW.
- verifiedRun `npx msw ls` (if using MSW 2.x) to list registered handlers and verify the URL pattern.
Things that make this bug worse or harder to find.
- warningDon't put `worker.start()` inside a component's `useEffect` without a guard – it runs on every render.
- warningDon't ignore the 'captured a request without a matching handler' warning – it's the single most important clue.
- warningDon't assume the handler URL matches – copy-paste the exact URL from the network tab into the handler.
- warningDon't forget that `http.get('/api/user', resolver)` will NOT match `/api/user?foo=bar` unless you use a wildcard or match query params explicitly.
- warningDon't use relative URLs in handlers when running in Node.js – always use absolute URLs or a base URL.
The Phantom 404: MSW silent fail in a React component test
Timeline
- 09:00Developer pushes a new feature with a mocked API call using MSW.
- 09:15CI pipeline fails: integration tests show real API responses instead of mocks.
- 09:30Developer checks console – no MSW warnings. Tests run in jsdom (simulated browser).
- 09:45Network tab (via vitest's debug mode) shows requests hitting the real endpoint.
- 10:00Discovers that the test file uses `setupServer` but forgot to call `server.listen()` in `beforeAll`.
- 10:05Adds `beforeAll(() => server.listen())` and `afterEach(() => server.resetHandlers())`.
- 10:06Tests pass locally and in CI.
I was working on a new settings page that fetches user preferences from `/api/preferences`. I wrote the MSW handler in `src/mocks/handlers.ts` and the test in `Settings.test.tsx`. The test rendered the component and waited for the mock data to appear. But the data never showed – the component showed a loading spinner indefinitely.
I opened the dev tools in the headless browser (vitest's debug mode) and saw the network request to `/api/preferences` returned a real 404 from the staging server. MSW had not intercepted it. No warnings in console. I checked the test setup file – I had imported `setupServer` and defined handlers, but I never called `server.listen()`. In my previous project, I had used a global setup file that called it automatically, so I forgot. The fix was a single line in `beforeAll`.
After adding the lifecycle hooks, the test passed. But I also noticed that the handler URL had a trailing slash in the component (`/api/preferences/`) while the handler was `/api/preferences`. I added a wildcard to be safe. The lesson: always check the exact request URL and never assume the lifecycle hooks are in place.
Root cause
Missing `server.listen()` call in the test's `beforeAll` hook, causing MSW to never start intercepting in the Node.js test environment.
The fix
Added `beforeAll(() => server.listen())` and `afterEach(() => server.resetHandlers())` to the test file's setup.
The lesson
Always verify the test setup includes the required lifecycle hooks. Use a global setup file or a shared `setupTests.ts` to avoid repeating this in every test suite.
In browser environments, MSW installs a Service Worker. If the worker fails to register, MSW silently falls back to no mocking. Common reasons: the `mockServiceWorker.js` file is missing from the public directory, or the page is served over HTTP while the worker requires HTTPS (localhost is an exception).
To diagnose, open DevTools → Application → Service Workers. If no worker is listed, run `npx msw init public/` to copy the worker script. Also check the Console for any registration errors (e.g., 'Failed to register a ServiceWorker'). Ensure the worker script is served with the correct MIME type (`text/javascript`).
MSW uses a strict URL matching algorithm by default. `http.get('/api/user', handler)` matches `https://example.com/api/user` but NOT `https://example.com/api/user/` (trailing slash) or `https://example.com/api/user?foo=bar` (query params). Use wildcards: `http.get('/api/user*', handler)` to match any subpath or query. Or use a passthrough handler to see what MSW is seeing: `http.all('*', ({ request }) => console.log(request.url))`.
Also check the `params` object: if you use a path parameter like `/api/user/:id`, MSW extracts `id` from the URL. A mismatch in the parameter name or type (e.g., expecting string but getting number) can cause the handler to not match. In MSW 2.x, the `params` are strings unless you use a custom matcher.
When using MSW with Node.js test runners (Jest, Vitest, Mocha), you must explicitly start and stop the server. The typical pattern: `beforeAll(() => server.listen())`, `afterEach(() => server.resetHandlers())`, `afterAll(() => server.close())`. Missing any of these is the #1 cause of 'request not intercepted' in tests.
A common mistake is to put `server.listen()` inside a `beforeEach` instead of `beforeAll` – this works but is wasteful. Also, if you have multiple test files, each file needs its own setup unless you use a global setup file. In Vitest, you can define a `setupFiles` array in `vitest.config.ts` that points to a file calling `server.listen()` once for the entire suite.
Some libraries (like Storybook) include their own MSW instance. If your app also starts MSW, the two workers can conflict, causing one to unregister the other. Symptoms: intermittent interception, or MSW warnings about 'duplicate worker registration'.
Solution: either disable the library's worker (check its configuration) or unify them by using the same worker instance. In Storybook, you can use `initialize` with the same handlers. In the app, check if `window.__MSW_INSTANCE__` exists to avoid double registration.
Service workers are cached aggressively by browsers. If you update your handlers but the browser still uses the old `mockServiceWorker.js`, interception may fail. Solution: go to DevTools → Application → Service Workers → Unregister, then reload the page. For persistent issues, add a version query parameter to the worker URL: `worker.start({ serviceWorker: { url: '/mockServiceWorker.js?v=2' } })`.
In CI, headless browsers often don't have a persistent cache, but if you use a service worker cache-first strategy, the old script might be served. Always run `npx msw init <publicDir> --save` to ensure the latest worker script is deployed.
Frequently asked questions
Why does MSW work in development but not in production builds?
In production, static assets are often served from a CDN or a different origin, and the service worker scope might not cover the API endpoints. Also, production builds may tree-shake the MSW library if it's only imported in development. Ensure MSW is only enabled in development mode (e.g., using `if (process.env.NODE_ENV === 'development')`).
How do I match query parameters with MSW?
Use a wildcard in the URL pattern: `http.get('/api/user*', handler)`. To access query params inside the handler, use `request.url.searchParams.get('key')`. For exact query matching, you can use a custom predicate: `http.get('/api/user', ({ request }) => { if (request.url.searchParams.get('page') !== '1') return; ... })`.
What does '[MSW] Warning: captured a request without a matching request handler' mean?
It means MSW intercepted a request, but none of your registered handlers matched the URL/method. The request will pass through to the network. This is the most direct clue that your handler URL pattern doesn't match. Copy the URL from the warning and compare it with your handler patterns.
Can I use MSW with Cypress or Playwright?
Yes, but with care. In Cypress, you can use `cy.intercept()` for mocking; MSW can conflict. In Playwright, you can use MSW's `setupWorker` in the browser context, but you must start it after the page loads. For end-to-end tests, it's often simpler to use Playwright's built-in route interception.
Why does my handler work in one test file but not another?
Most likely because the failing test file doesn't have the lifecycle hooks (`server.listen()`, etc.). Check that every test file that needs MSW has the setup. Alternatively, use a global setup file (e.g., `setupTests.ts`) that runs before all tests and starts the server once.