Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nodejs/undici/llms.txt

Use this file to discover all available pages before exploring further.

Testing code that makes HTTP requests usually requires either running a real server or intercepting requests at the network level. Undici’s built-in mocking utilities let you intercept calls and return deterministic responses without any network activity, keeping your tests fast, isolated, and reproducible. The MockAgent class is the entry point — it wraps a real Agent and routes matching requests to configured interceptors instead.

Why mock HTTP requests?

No real servers needed

Tests run without spinning up external services or a local development server.

Deterministic responses

Every test gets the exact response you specify — no flaky failures from network conditions.

Error simulation

Easily test how your code handles 4xx/5xx responses or network errors.

Speed

No I/O means your test suite runs significantly faster.

Creating a MockAgent

Basic MockAgent setup
import { MockAgent, setGlobalDispatcher } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)
Once set as the global dispatcher, all request() and fetch() calls will be handled by the mock agent. Requests that don’t match any interceptor will fall through to the real network by default — use disableNetConnect() to prevent that.

Options

OptionTypeDefaultDescription
connectionsnumberSet to 1 to get a MockClient from .get(), otherwise returns MockPool
ignoreTrailingSlashbooleanfalseIgnore trailing slashes when matching paths
acceptNonStandardSearchParametersbooleanfalseAccept multi-value params like param[]=1&param[]=2
enableCallHistorybooleanfalseRecord all requests for later assertion

Intercepting requests

Use mockAgent.get(origin) to get a MockPool (or MockClient) for a specific origin, then call .intercept() to define what to match and .reply() to specify the response.
Basic mocked request
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')

const { statusCode, body } = await request('http://localhost:3000/foo')

console.log('response received', statusCode) // response received 200

for await (const data of body) {
  console.log('data', data.toString('utf8')) // data foo
}

Intercept options

All fields in the intercept() options object support strings, regular expressions, or predicate functions:
OptionTypeDescription
pathstring | RegExp | FunctionPath to match (including query string when using RegExp/Function)
methodstring | RegExp | FunctionHTTP method to match. Default: 'GET'
bodystring | RegExp | FunctionRequest body to match
headersRecord<string, string | RegExp | Function>Request headers to match (all must pass)
queryRecord<string, any>Query parameters (only when path is a string)

Matching by method, query, and headers

Matching method, path with query, headers, and body
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')

mockPool.intercept({
  path: '/foo?hello=there&see=ya',
  method: 'POST',
  body: 'form1=data1&form2=data2'
}).reply(200, { foo: 'bar' }, {
  headers: { 'content-type': 'application/json' },
  trailers: { 'Content-MD5': 'test' }
})

const { statusCode, headers, trailers, body } = await request(
  'http://localhost:3000/foo?hello=there&see=ya',
  { method: 'POST', body: 'form1=data1&form2=data2' }
)

console.log('response received', statusCode) // response received 200
console.log('headers', headers)              // { 'content-type': 'application/json' }
for await (const data of body) {
  console.log('data', data.toString('utf8')) // '{"foo":"bar"}'
}
console.log('trailers', trailers)            // { 'content-md5': 'test' }

Matching with regular expressions and functions

RegExp and function matchers
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')

mockPool.intercept({
  path: '/foo',
  method: /^GET$/,
  body: (value) => value === 'form=data',
  headers: {
    'User-Agent': 'undici',
    Host: /^example\.com$/
  }
}).reply(200, 'foo')

Dynamic reply from request data

Pass a callback to reply() to read incoming request data and build the response dynamically:
Dynamic reply based on request
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')

mockPool.intercept({
  path: '/bank-transfer',
  method: 'POST',
  headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' }
}).reply(200, (opts) => {
  // opts includes method, headers, body, origin, path
  return { message: 'transaction processed' }
})

Reply options

The MockInterceptor returned by .intercept() exposes several chainable methods:
MethodDescription
.reply(statusCode, data, responseOptions?)Respond with status code, body, headers, and trailers
.replyWithError(error)Throw an error for the matching request
.defaultReplyHeaders(headers)Add default headers to subsequent replies
.defaultReplyTrailers(trailers)Add default trailers to subsequent replies
.replyContentLength()Auto-calculate and include content-length
.persist()Return this response indefinitely
.times(n)Return this response exactly n times
.delay(ms)Delay the response by ms milliseconds
Persistent and timed interceptors
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')

// Responds to every matching request forever
mockPool.intercept({ path: '/health', method: 'GET' })
  .reply(200, { status: 'ok' })
  .persist()

// Responds exactly twice, then falls through
mockPool.intercept({ path: '/limited', method: 'GET' })
  .reply(200, 'limited')
  .times(2)

Activating globally vs per-request

import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

const mockPool = mockAgent.get('http://localhost:3000')
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')

// All requests use the mock automatically
const { statusCode } = await request('http://localhost:3000/foo')

Preventing real network calls

Call mockAgent.disableNetConnect() to ensure that any request not matched by an interceptor throws a MockNotMatchedError instead of silently hitting the network:
Disabling real network connections
import { MockAgent, request } from 'undici'

const mockAgent = new MockAgent()
mockAgent.disableNetConnect()

// This would throw MockNotMatchedError because no interceptor matches
await request('http://example.com')
To allow some hosts through while blocking others, use enableNetConnect() with a matcher:
Selectively allowing real requests
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

mockAgent.enableNetConnect('example-1.com')           // string: exact host
mockAgent.enableNetConnect(/\.internal\.example\.com$/) // RegExp

await request('http://example-1.com') // real request
await request('http://blocked.com')   // throws

Asserting all interceptors were called

At the end of a test, verify that every interceptor you set up was actually triggered:
Asserting no pending interceptors
import { MockAgent } from 'undici'

const agent = new MockAgent()
agent.disableNetConnect()

agent
  .get('https://example.com')
  .intercept({ method: 'GET', path: '/' })
  .reply(200)

// Run your code under test here...

// Throws if any interceptors were never invoked
agent.assertNoPendingInterceptors()
The error message includes a formatted table showing every pending interceptor with its method, origin, path, status code, and invocation count.

MockPool and MockClient

By default mockAgent.get(origin) returns a MockPool (multiple connections). Pass { connections: 1 } to the MockAgent constructor to get a MockClient instead — useful when you need to test connection-specific behavior.
MockClient for single-connection testing
import { MockAgent, request } from 'undici'

const mockAgent = new MockAgent({ connections: 1 })

const mockClient = mockAgent.get('http://localhost:3000')
mockClient.intercept({ path: '/foo' }).reply(200, 'foo')

const { statusCode } = await request('http://localhost:3000/foo', {
  dispatcher: mockClient
})
console.log('response received', statusCode) // response received 200
You can also dispatch requests directly through the pool or client:
Dispatching through MockPool directly
import { MockAgent } from 'undici'

const mockAgent = new MockAgent()
const mockPool = mockAgent.get('http://localhost:3000')

mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo')

const { statusCode, body } = await mockPool.request({
  origin: 'http://localhost:3000',
  path: '/foo',
  method: 'GET'
})

SnapshotAgent for record-and-replay testing

SnapshotAgent extends MockAgent with automatic snapshot recording. In record mode it makes real requests and saves the responses; in playback mode it replays those saved responses without touching the network.
Recording snapshots
import { SnapshotAgent, setGlobalDispatcher } from 'undici'

const agent = new SnapshotAgent({
  mode: 'record',
  snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)

// Makes real HTTP requests and records them
const response = await fetch('https://api.example.com/users')
const users = await response.json()

await agent.saveSnapshots()
Replaying snapshots
import { SnapshotAgent, setGlobalDispatcher } from 'undici'

const agent = new SnapshotAgent({
  mode: 'playback',
  snapshotPath: './test/snapshots/api-calls.json'
})
setGlobalDispatcher(agent)

// Returns the previously recorded response — no real request made
const response = await fetch('https://api.example.com/users')
Snapshot files can contain sensitive data such as authorization tokens. Always add them to .gitignore when recording against real APIs, and use the excludeHeaders option to strip sensitive headers before saving.

Call history

Enable call history to inspect every request the mock agent received, regardless of whether it was intercepted:
Inspecting call history
import { MockAgent, setGlobalDispatcher, request } from 'undici'

const mockAgent = new MockAgent({ enableCallHistory: true })
setGlobalDispatcher(mockAgent)

await request('http://example.com', { query: { item: 1 } })

const history = mockAgent.getCallHistory()
const firstCall = history?.firstCall()
console.log(firstCall?.fullUrl)     // 'http://example.com/?item=1'
console.log(firstCall?.method)      // 'GET'
console.log(firstCall?.searchParams) // { item: '1' }

// Clear history between tests
mockAgent.clearCallHistory()

Cleaning up

Closing the MockAgent
import { MockAgent, setGlobalDispatcher } from 'undici'

const mockAgent = new MockAgent()
setGlobalDispatcher(mockAgent)

// Closes all mock pools/clients and clears call history
await mockAgent.close()

Build docs developers (and LLMs) love