mrkeyoor.com_
Thu 06 Aug 07:44 UTC
npmTestingupdated 06 Aug 2026

@mswjs/interceptors

@mswjs/interceptors is the network interception engine underneath Mock Service Worker. Instead of monkey-patching http.request or a specific client like axios, it hooks Node at the TCP and TLS socket handle level and stubs TCPWrap and TLSWrap until you decide whether to claim a connection or let it through. Higher-level interceptors sit on top of that socket layer and parse the packets back into requests, so a single HttpRequestInterceptor sees traffic from node:http, the global fetch, direct undici calls, axios, got, and node-fetch alike. Every intercepted request is handed to you as a standard Fetch API Request, and you reply with a standard Response through a controller object. There are also narrower interceptors for http.ClientRequest, XMLHttpRequest, fetch, raw sockets, and WebSocket connections, plus a BatchInterceptor to run several at once.

Verdict

This is infrastructure for tool authors, and it is very good at that job: socket-level interception means axios, got, undici, and native fetch all get caught by one listener. If you are writing tests rather than writing a testing library, install msw instead and let it depend on this for you.

API stability2/5Still on 0.x at 0.42.3, and the 0.x contract means minor releases can break you. The interceptor names and event shapes have churned (HttpRequestInterceptor and SocketInterceptor are recent additions), and a separate backport tag exists on 0.17 for stragglers.
Docs4/5One long README, but an unusually honest and complete one: it explains the interception algorithm layer by layer, documents every interceptor and event, and states up front when you should not use the library. There is no hosted docs site, changelog notes, or searchable API reference.
Maintenance5/5Pushed 25 July 2026 with 0.42.3 published 24 July 2026, 23 open issues (31 counting PRs), and active development by the Mock Service Worker author, who has an obvious incentive to keep it healthy.
Ecosystem4/5Around 22M weekly downloads, almost all of it arriving as a transitive dependency of msw rather than direct installs. Few projects depend on it directly, so community examples beyond the README are thin.

Use it if

  • You are building your own mocking, recording, or proxying tool and want the interception algorithm handed to you instead of writing another http.request patch
  • You need to observe or mock traffic from clients you do not control, including direct undici usage and native fetch, which client-specific patches miss because the request never touches http.ClientRequest
  • You want the intercepted request and your mocked reply expressed as Fetch API Request and Response objects rather than a library-specific request shape
  • You need WebSocket interception with both sides available: the client connection and the original server connection, so you can forward, drop, or rewrite individual messages
Skip it if

Setup reality

npm install @mswjs/interceptors is small and dependency-light (debug, outvariant, rettime, is-node-process, @open-draft/until) with no native build, but Node 22 or newer is required by the engines field. Nothing is intercepted until you call interceptor.apply(), and nothing is cleaned up until you call interceptor.dispose(), so in a test suite you own that lifecycle by hand: apply in beforeAll, removeAllListeners between tests, dispose in afterAll, or the patched globals leak into the next file. Imports are per-interceptor subpaths (@mswjs/interceptors/fetch, /ClientRequest, /XMLHttpRequest, /WebSocket, /http, /net), and some of them resolve to different /node or /web builds depending on the environment, which trips up bundlers with unusual condition handling. The biggest behavioral surprise is timing: a request must be answered within the same tick as the listener, so setTimeout does not work; make the listener async and await instead. Unhandled exceptions in your listener are quietly turned into 500 responses unless you rethrow them from an unhandledException listener.

Patterns

Catch every HTTP client with one interceptorintercept-all-http

import { HttpRequestInterceptor } from '@mswjs/interceptors/http'

const interceptor = new HttpRequestInterceptor()
interceptor.apply()

interceptor.on('request', ({ request, requestId }) => {
  console.log(request.method, request.url)
})

Because this hooks the socket layer, it sees node:http, global fetch, direct undici, axios, and got without any per-client adapters. The tradeoff is that initiator is just a net.Socket unless you also apply the matching client-level interceptor.

Reply with a mocked responsemock-a-response

import { FetchInterceptor } from '@mswjs/interceptors/fetch'

const interceptor = new FetchInterceptor()
interceptor.apply()

interceptor.on('request', ({ request, controller }) => {
  if (new URL(request.url).pathname === '/api/user') {
    controller.respondWith(
      Response.json({ id: 1, name: 'Ada' }, { status: 200 })
    )
  }
})

Not calling respondWith lets the request hit the real network. A request can only be answered once, and there is no matching layer, so the URL check is your job.

Add latency without breaking the tick ruledelay-a-response

import { setTimeout } from 'node:timers/promises'

interceptor.on('request', async ({ controller }) => {
  await setTimeout(500)
  controller.respondWith(new Response(null, { status: 503 }))
})

Responses must be produced in the same tick as the listener, so a callback-style setTimeout silently misses the window. An async listener that awaits works because the interceptor waits on the returned promise.

Inspect the request body safelyread-request-body

interceptor.on('request', async ({ request, controller }) => {
  const body = await request.clone().json()
  if (body.action === 'delete') {
    controller.respondWith(new Response(null, { status: 403 }))
  }
})

Clone before reading. Reading the body off the original request consumes the stream and the real request then fails or sends nothing.

Add a header to outgoing requestsmodify-request-headers

interceptor.on('request', ({ request }) => {
  request.headers.set('x-trace-id', crypto.randomUUID())
})

Headers are the only mutable part of an intercepted request. Method, URL, and body are readonly by design, so this is not a substitute for a rewriting proxy.

Simulate a network failureerror-a-request

interceptor.on('request', ({ controller }) => {
  // generic network error
  controller.respondWith(Response.error())
})

// or with a specific reason
interceptor.on('request', ({ controller }) => {
  controller.errorWith(new Error('ECONNRESET simulated'))
})

errorWith carries your message, but clients differ: http.ClientRequest surfaces it, while fetch throws a generic TypeError with the original error tucked into cause.

Stop exceptions turning into 500 responseshandle-listener-exceptions

interceptor.on('unhandledException', ({ error, request }) => {
  console.error('mock handler blew up for', request.url)
  throw error
})

By default a thrown error in the request listener becomes a 500 response, which hides bugs in your own mock code. Rethrowing inside unhandledException makes it a real failure again.

Apply the Node preset in one gobatch-interceptors

import { BatchInterceptor } from '@mswjs/interceptors'
import nodeInterceptors from '@mswjs/interceptors/presets/node'

const interceptor = new BatchInterceptor({
  name: 'test-interceptor',
  interceptors: nodeInterceptors,
})

interceptor.on('request', ({ request }) => console.log(request.url))
interceptor.apply()

The node preset is ClientRequestInterceptor plus XMLHttpRequestInterceptor plus FetchInterceptor; a browser preset drops the first. Applying client-level interceptors alongside HttpRequestInterceptor is also what makes the initiator property useful.

Wire apply and dispose into a test suitetest-lifecycle

import { beforeAll, afterEach, afterAll } from 'vitest'
import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest'

const interceptor = new ClientRequestInterceptor()

beforeAll(() => interceptor.apply())
afterEach(() => interceptor.removeAllListeners())
afterAll(() => interceptor.dispose())

Nothing is automatic here. Skip dispose and the patched globals survive into other test files; skip removeAllListeners and listeners from a previous test keep answering requests.

Log responses and tell mocks from real trafficobserve-responses

interceptor.on('response', ({ response, responseType, request }) => {
  console.log(responseType, response.status, request.url)
  // responseType is 'mock' or 'original'
})

This is the hook for recording fixtures or asserting that a request actually reached the network. responseType is the only reliable way to tell whether your handler answered it.

Intercept a WebSocket connectionintercept-websocket

import { WebSocketInterceptor } from '@mswjs/interceptors/WebSocket'

const interceptor = new WebSocketInterceptor()
interceptor.apply()

interceptor.on('connection', ({ client, server }) => {
  client.addEventListener('message', (event) => {
    if (event.data === 'ping') {
      event.preventDefault()
      client.send('pong')
    }
  })
})

The intercepted connection is not opened at all until you call server.connect(). Once connected, client and server messages are forwarded both ways by default and preventDefault is how you stop one.

Claim a raw TCP connectionsocket-level-claim

import { SocketInterceptor } from '@mswjs/interceptors/net'

const interceptor = new SocketInterceptor()
interceptor.apply()

interceptor.on('connection', ({ socket, connectionOptions, controller }) => {
  if (connectionOptions.host === 'db.internal') {
    controller.claim()
    socket.on('data', (chunk) => socket.write(chunk))
  } else {
    controller.passthrough()
  }
})

The connection stays pending until you call claim() or passthrough(), so forgetting the else branch hangs unrelated traffic. The exposed socket is mirrored: it emits data when the client writes, and writing to it delivers data to the client.

Alternatives

PackageRegistryPick it when
mswnpmYou want the thing built on top of this: request handlers, URL matching, and one set of mocks shared by browser and Node tests
nocknpmYou are mocking Node HTTP in tests and prefer a chainable scope API with strict assertions about which requests were made
undicinpmAll your traffic goes through fetch or undici and MockAgent covers you without patching anything global
fetch-mocknpmYou only need to fake the global fetch and want matching plus call inspection in one small package