Prerequisites
Environment check
Node.js version
v24.16.0 (also reproduces on Node 22; the same versions on macOS do not reproduce — see below)
Reproduction repository
https://github.com/Joseamica/msw-two-servers-repro
Reproduction steps
The issue is deterministic on Linux and absent on macOS, so the repro runs in Docker:
git clone https://github.com/Joseamica/msw-two-servers-repro
cd msw-two-servers-repro
docker run --rm --cpus=2 -v "$PWD":/src:ro node:24-bookworm bash -c \
'cp -r /src /app && cd /app && rm -rf node_modules && npm install && npx vitest run'
The single test file starts two setupServer() instances that are in listen() at the same time — a "global" one (simulating a server started from a Vitest setupFiles) and a per-file one — then sends a PATCH with a JSON body (via fetch and via axios/XHR) to a handler whose resolver calls await request.json().
Current behavior
Every intercepted request is evaluated twice against the request handlers: executeHandlers runs 2× with the same requestId and the same Request instance. On the second pass the body is already consumed, so the resolver's await request.json() throws, and msw prints (once per request with a body):
TypeError: Body is unusable: Body has already been read
at consumeBody (node:internal/deps/undici/undici:6979:31)
at FetchRequest.json (node:internal/deps/undici/undici:6930:18)
at /app/repro.test.js:19:32
at file:///app/node_modules/msw/lib/core/handlers/RequestHandler.mjs:176:26
at HttpHandler.run (file:///app/node_modules/msw/lib/core/handlers/RequestHandler.mjs:142:35)
at processTicksAndRejections (node:internal/process/task_queues:104:5)
at executeHandlers (file:///app/node_modules/msw/lib/core/utils/executeHandlers.mjs:11:14)
at until (file:///app/node_modules/until-async/lib/index.js:14:17)
at InterceptorHttpNetworkFrame.resolve (file:///app/node_modules/msw/lib/core/experimental/frames/http-frame.mjs:81:41)
at Emitter.<anonymous> (file:///app/node_modules/msw/lib/core/experimental/define-network.mjs:80:11)
[MSW] Encountered an unhandled exception during the handler lookup for "PATCH http://localhost/api/items/1". Please see the original error above.
The tests still pass (the first evaluation produces the mocked response) — the failure mode is deterministic stderr noise that floods CI logs.
Instrumentation evidence. Logging (requestId, method, url, request.bodyUsed) at the top of lib/core/utils/executeHandlers.mjs shows each requestId evaluated exactly twice, the second time with the body already consumed:
EXEC 8441ed5bc8e25 PATCH http://localhost/api/items/1 bodyUsed=false
EXEC 8441ed5bc8e25 PATCH http://localhost/api/items/1 bodyUsed=true <- duplicate pass
EXEC b81dd5ce91449 PATCH http://localhost/api/items/2 bodyUsed=false
EXEC b81dd5ce91449 PATCH http://localhost/api/items/2 bodyUsed=true <- duplicate pass
Commenting out the global server's listen() (so only one server is listening) → each requestId appears once and there are zero errors. In the real-world project where we found this, the same instrumentation showed 64 executions for 32 requests with two servers listening, and exactly 32 with one.
Platform determinism. On Linux (Docker node:24-bookworm and node:22-bookworm, --cpus=2) the double evaluation happens on every request — in our project that was a deterministic 22 errors across 22 test files on every CI run. On macOS with the same Node/msw/Vitest versions the count is always 0. Whatever suppresses the duplicate lookup appears to be a race that Linux always loses.
Version matrix (all reproduce identically on Linux): msw 2.14.6 (experimental frames / defineNetwork) and 2.13.6 (pre-frames); @mswjs/interceptors 0.41.9; Node 22 and 24; Vitest 4.1.7 with the jsdom environment; both fetch and axios (XHR adapter) as clients.
Expected behavior
A request should be evaluated against a server's handlers exactly once. Two setupServer() instances listening simultaneously shouldn't cause the same requestId to be resolved twice on one frame — and in any case, a duplicate lookup pass shouldn't re-run a resolver with an already-consumed Request body.
Two servers listening at once is admittedly an odd setup (ours was accidental: a global server in Vitest setupFiles plus per-file servers). Workaround that fully eliminates the noise: ensure only one server listens per test file — we replaced the global listen() in the setup file with an explicit installGlobalServer() helper that each test file opts into, so a file uses either the global server or its own, never both. Still filing this because the double evaluation looks like a genuine bug at the frame/emitter level, and the platform-dependent behavior makes it nasty to debug: quiet and green on macOS dev machines while Linux CI fills with TypeError: Body is unusable.
Prerequisites
Environment check
mswversion (2.14.6)Node.js version
v24.16.0 (also reproduces on Node 22; the same versions on macOS do not reproduce — see below)
Reproduction repository
https://github.com/Joseamica/msw-two-servers-repro
Reproduction steps
The issue is deterministic on Linux and absent on macOS, so the repro runs in Docker:
The single test file starts two
setupServer()instances that are inlisten()at the same time — a "global" one (simulating a server started from a VitestsetupFiles) and a per-file one — then sends aPATCHwith a JSON body (viafetchand via axios/XHR) to a handler whose resolver callsawait request.json().Current behavior
Every intercepted request is evaluated twice against the request handlers:
executeHandlersruns 2× with the samerequestIdand the sameRequestinstance. On the second pass the body is already consumed, so the resolver'sawait request.json()throws, and msw prints (once per request with a body):The tests still pass (the first evaluation produces the mocked response) — the failure mode is deterministic stderr noise that floods CI logs.
Instrumentation evidence. Logging
(requestId, method, url, request.bodyUsed)at the top oflib/core/utils/executeHandlers.mjsshows eachrequestIdevaluated exactly twice, the second time with the body already consumed:Commenting out the global server's
listen()(so only one server is listening) → eachrequestIdappears once and there are zero errors. In the real-world project where we found this, the same instrumentation showed 64 executions for 32 requests with two servers listening, and exactly 32 with one.Platform determinism. On Linux (Docker
node:24-bookwormandnode:22-bookworm,--cpus=2) the double evaluation happens on every request — in our project that was a deterministic 22 errors across 22 test files on every CI run. On macOS with the same Node/msw/Vitest versions the count is always 0. Whatever suppresses the duplicate lookup appears to be a race that Linux always loses.Version matrix (all reproduce identically on Linux): msw 2.14.6 (experimental frames /
defineNetwork) and 2.13.6 (pre-frames);@mswjs/interceptors0.41.9; Node 22 and 24; Vitest 4.1.7 with thejsdomenvironment; bothfetchand axios (XHR adapter) as clients.Expected behavior
A request should be evaluated against a server's handlers exactly once. Two
setupServer()instances listening simultaneously shouldn't cause the samerequestIdto be resolved twice on one frame — and in any case, a duplicate lookup pass shouldn't re-run a resolver with an already-consumedRequestbody.Two servers listening at once is admittedly an odd setup (ours was accidental: a global server in Vitest
setupFilesplus per-file servers). Workaround that fully eliminates the noise: ensure only one server listens per test file — we replaced the globallisten()in the setup file with an explicitinstallGlobalServer()helper that each test file opts into, so a file uses either the global server or its own, never both. Still filing this because the double evaluation looks like a genuine bug at the frame/emitter level, and the platform-dependent behavior makes it nasty to debug: quiet and green on macOS dev machines while Linux CI fills withTypeError: Body is unusable.