Fix: MSW (Mock Service Worker) Not Working — Handlers Not Intercepting, Browser Not Mocking, or v2 Migration Errors
Quick Answer
How to fix Mock Service Worker issues — browser vs Node setup, handler registration, worker start timing, passthrough requests, and common MSW v2 API changes from v1.
The Problem
MSW is set up but requests still go to the real server:
// handlers.ts
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([{ id: 1, name: 'Alice' }]);
}),
];
// tests — real fetch still fires despite MSW setup
const response = await fetch('/api/users');
// Returns real server data, not the mockOr the browser worker doesn’t start:
// src/mswWorker.js
export const worker = setupWorker(...handlers);
// worker.start() called but network tab shows real requestsOr MSW v1 code breaks after upgrading to v2:
// v1 syntax — no longer valid in v2
import { rest } from 'msw';
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.json([{ id: 1 }]));
});
// TypeError: rest is not a function (or not exported)Or some requests are mocked but others bypass MSW:
[MSW] Warning: intercepted a request without a matching request handler
GET https://cdn.example.com/api/config — bypassedWhy This Happens
MSW intercepts at different layers depending on the environment:
- Browser — MSW registers a Service Worker that intercepts network requests. If
worker.start()isn’t awaited before your app makes requests, those early requests miss the interceptor. - Node.js (tests) — MSW uses
@mswjs/interceptorsto patch the globalfetch,XMLHttpRequest, and Node’shttpmodule. Ifserver.listen()isn’t called before tests run, no interception happens. - v1 → v2 breaking changes — MSW v2 replaced the
restandgraphqlnamespaces withhttpandgraphql, changed the handler signature from(req, res, ctx)to({ request, params, cookies }), and changed the response format to standardResponse/HttpResponse. - Unmatched requests — by default in v2, unmatched requests pass through to the real network. In v1, unmatched requests also passed through unless configured otherwise.
Fix 1: Set Up MSW Correctly for Node.js Tests
For Jest, Vitest, or other Node.js test runners:
// src/mocks/handlers.ts
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([
{ id: 1, name: 'Alice', email: '[email protected]' },
{ id: 2, name: 'Bob', email: '[email protected]' },
]);
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json();
return HttpResponse.json({ id: 3, ...body }, { status: 201 });
}),
http.get('/api/users/:id', ({ params }) => {
const { id } = params;
if (id === '999') {
return HttpResponse.json({ error: 'Not found' }, { status: 404 });
}
return HttpResponse.json({ id, name: 'Alice' });
}),
];
// src/mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);// src/setupTests.ts (or vitest.setup.ts / jest.setup.ts)
import { server } from './mocks/server';
// Start server before all tests
beforeAll(() => server.listen({ onUnhandledRequest: 'warn' }));
// Reset handlers after each test (undo any runtime additions)
afterEach(() => server.resetHandlers());
// Clean up after all tests
afterAll(() => server.close());// jest.config.ts / vitest.config.ts
export default {
setupFilesAfterFramework: ['./src/setupTests.ts'],
// OR for Vitest:
test: {
setupFiles: ['./src/setupTests.ts'],
},
};Fix 2: Set Up MSW for Browser Development
For development mode interception in the browser:
# 1. Generate the service worker file
npx msw init public/ --save
# Creates public/mockServiceWorker.js
# Adds "msw" to package.json under "browser" or saves config
# 2. Add mockServiceWorker.js to your static files
# Ensure it's served at the root URL: http://localhost:3000/mockServiceWorker.js// src/mocks/browser.ts
import { setupWorker } from 'msw/browser';
import { handlers } from './handlers';
export const worker = setupWorker(...handlers);
// src/main.tsx (or index.tsx) — start worker before rendering
async function enableMocking() {
if (process.env.NODE_ENV !== 'development') return;
const { worker } = await import('./mocks/browser');
// Start the worker — MUST be awaited
return worker.start({
onUnhandledRequest: 'warn', // Warn for requests without a handler
serviceWorker: {
url: '/mockServiceWorker.js', // Path to the service worker
},
});
}
enableMocking().then(() => {
ReactDOM.createRoot(document.getElementById('root')!).render(<App />);
});Vite — ensure service worker is served correctly:
// vite.config.ts
export default defineConfig({
// If using a base URL, configure the service worker path
server: {
// The worker file must be at the root
},
});Note: The Service Worker must be served from the same origin as your app. If your app runs on
http://localhost:3000, the worker must be athttp://localhost:3000/mockServiceWorker.js.
Fix 3: Migrate from MSW v1 to v2
MSW v2 changed the entire handler API:
// v1 — OLD syntax
import { rest } from 'msw';
export const handlers = [
rest.get('/api/users', (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json([{ id: 1, name: 'Alice' }])
);
}),
rest.post('/api/users', async (req, res, ctx) => {
const body = await req.json();
return res(
ctx.status(201),
ctx.json({ id: 2, ...body })
);
}),
rest.get('/api/users/:id', (req, res, ctx) => {
const { id } = req.params;
return res(ctx.json({ id, name: 'Alice' }));
}),
];
// v2 — NEW syntax
import { http, HttpResponse } from 'msw';
export const handlers = [
http.get('/api/users', () => {
return HttpResponse.json([{ id: 1, name: 'Alice' }]);
// Status 200 is the default
}),
http.post('/api/users', async ({ request }) => {
const body = await request.json(); // Standard Request API
return HttpResponse.json({ id: 2, ...body }, { status: 201 });
}),
http.get('/api/users/:id', ({ params }) => {
const { id } = params; // params is an object
return HttpResponse.json({ id, name: 'Alice' });
}),
];v2 response formats:
import { http, HttpResponse } from 'msw';
// JSON response
http.get('/api/data', () => HttpResponse.json({ key: 'value' }))
// Text response
http.get('/api/text', () => HttpResponse.text('Hello world'))
// Error response
http.get('/api/fail', () => HttpResponse.json(
{ error: 'Not found' },
{ status: 404 }
))
// With custom headers
http.get('/api/file', () => new HttpResponse(blob, {
headers: { 'Content-Type': 'application/pdf' }
}))
// Network error (connection refused)
http.get('/api/down', () => HttpResponse.networkError('Service unavailable'))
// Delay response
http.get('/api/slow', async () => {
await new Promise(r => setTimeout(r, 2000));
return HttpResponse.json({ data: 'slow response' });
})Fix 4: Override Handlers Per Test
Add or replace handlers for specific tests without affecting other tests:
import { http, HttpResponse } from 'msw';
import { server } from '../mocks/server';
describe('UserList', () => {
test('shows users', async () => {
// Default handler from handlers.ts applies
render(<UserList />);
await screen.findByText('Alice');
});
test('shows error state', async () => {
// Override for this test only
server.use(
http.get('/api/users', () => {
return HttpResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
})
);
render(<UserList />);
await screen.findByText('Failed to load users');
// server.resetHandlers() in afterEach restores default handlers
});
test('shows empty state', async () => {
server.use(
http.get('/api/users', () => {
return HttpResponse.json([]); // Empty array
})
);
render(<UserList />);
await screen.findByText('No users found');
});
});One-time handlers (respond once then fall through):
server.use(
http.get('/api/users', () => {
return HttpResponse.json([{ id: 1 }]);
}, { once: true }) // Removes itself after first match
);Fix 5: Handle GraphQL, Cookies, and Headers
import { http, graphql, HttpResponse } from 'msw';
// GraphQL handlers
const graphqlHandlers = [
graphql.query('GetUser', ({ variables }) => {
const { id } = variables;
return HttpResponse.json({
data: { user: { id, name: 'Alice', email: '[email protected]' } },
});
}),
graphql.mutation('CreateUser', ({ variables }) => {
return HttpResponse.json({
data: { createUser: { id: '2', ...variables.input } },
});
}),
// With errors
graphql.query('GetProtectedData', () => {
return HttpResponse.json({
errors: [{ message: 'Unauthorized', extensions: { code: 'UNAUTHENTICATED' } }],
});
}),
];
// Access request headers
http.get('/api/protected', ({ request }) => {
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return HttpResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
return HttpResponse.json({ data: 'secret' });
})
// Access cookies
http.get('/api/session', ({ cookies }) => {
const sessionId = cookies.session_id;
if (!sessionId) {
return HttpResponse.json({ error: 'No session' }, { status: 401 });
}
return HttpResponse.json({ userId: '1' });
})
// Set response cookies
http.post('/api/login', () => {
return HttpResponse.json({ success: true }, {
headers: {
'Set-Cookie': 'session_id=abc123; HttpOnly; Path=/',
},
});
})Fix 6: Passthrough and Unhandled Request Behavior
// server.ts — configure unhandled request behavior
export const server = setupServer(...handlers);
server.listen({
onUnhandledRequest: 'warn', // Log warning (default)
// onUnhandledRequest: 'error', // Fail the test
// onUnhandledRequest: 'bypass', // Silently pass through
// onUnhandledRequest: (request) => {
// // Custom handling
// if (request.url.includes('analytics')) return; // Ignore analytics
// console.warn('Unhandled:', request.method, request.url);
// },
});
// Passthrough specific requests
import { passthrough } from 'msw';
export const handlers = [
// Match all API calls
http.get('/api/*', ({ request }) => {
// Pass through requests to external CDN
if (request.url.includes('cdn.example.com')) {
return passthrough();
}
// Handle locally
return HttpResponse.json([]);
}),
// Explicit passthrough handler
http.get('https://cdn.example.com/*', () => passthrough()),
];Still Not Working?
Service Worker not registering in browser — open DevTools → Application → Service Workers. If the worker isn’t listed, check: (1) mockServiceWorker.js exists at the correct URL, (2) you’re on HTTPS or localhost (Service Workers require a secure context), (3) worker.start() is awaited before your app renders. Check the browser console for registration errors.
MSW intercepts in tests but the response is wrong — verify the handler URL matches exactly. http.get('/api/users', ...) matches relative URL /api/users. If your fetch uses a full URL (fetch('http://localhost:3000/api/users')), the handler must also use the full URL or a URL with a wildcard: http.get('http://localhost:3000/api/users', ...). Use '**/api/users' for glob matching in Playwright but not in MSW — MSW uses exact URL matching or new URL() patterns.
server.resetHandlers() not working — resetHandlers() removes handlers added via server.use() at runtime, restoring to the initial handlers passed to setupServer(). If you’re modifying the original handlers array directly, resetHandlers() won’t help. Always add test-specific handlers via server.use(), never mutate the original array.
For related testing issues, see Fix: Jest Mock Not Working and Fix: Playwright Not Working.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: CodeMirror Not Working — Editor Not Rendering, Extensions Not Loading, or React State Out of Sync
How to fix CodeMirror 6 issues — basic setup, language and theme extensions, React integration, vim mode, collaborative editing, custom keybindings, and read-only mode.
Fix: GSAP Not Working — Animations Not Playing, ScrollTrigger Not Firing, or React Cleanup Issues
How to fix GSAP animation issues — timeline and tween basics, ScrollTrigger setup, React useGSAP hook, cleanup and context, SplitText, stagger animations, and Next.js integration.
Fix: i18next Not Working — Translations Missing, Language Not Switching, or Namespace Errors
How to fix i18next issues — react-i18next setup, translation file loading, namespace configuration, language detection, interpolation, pluralization, and Next.js integration.
Fix: ky Not Working — Requests Failing, Hooks Not Firing, or Retry Not Working
How to fix ky HTTP client issues — instance creation, hooks (beforeRequest, afterResponse), retry configuration, timeout handling, JSON parsing, error handling, and migration from fetch or axios.