Use this file to discover all available pages before exploring further.
Testing code that uses undici requires attention to two practical concerns: by default undici keeps sockets open for a few seconds after each request to reduce connection overhead in production, which causes tests to hang; and real HTTP requests make tests slow, non-deterministic, and fragile. This guide covers both concerns — adjusting timeouts for tests, writing unit tests with MockAgent, and using SnapshotAgent for integration record-and-replay — along with patterns for guarding against silent reconnection bugs.
Undici’s production defaults hold connections open for several seconds. In a test suite this means the process won’t exit until all keep-alive timers fire. Set short timeouts to fix this:
Short keep-alive timeouts for tests
import { request, setGlobalDispatcher, Agent } from 'undici'const agent = new Agent({ keepAliveTimeout: 10, // milliseconds keepAliveMaxTimeout: 10 // milliseconds})setGlobalDispatcher(agent)
Apply this once in a global test setup file (e.g. test/setup.js) so every test in the suite benefits automatically.
Wrapping agent setup in beforeEach / afterEach (or their node:test equivalents before / after) ensures a clean slate for every test and prevents interceptors from leaking between tests.
Register as many interceptors as needed on one or more pools. Each interceptor is consumed by one matching request by default — call .persist() to reuse it indefinitely or .times(n) for a specific count.
Multiple interceptors
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' }).reply(200, 'foo')mockPool.intercept({ path: '/hello', method: 'GET' }).reply(200, 'hello')// Health check endpoint called many timesmockPool.intercept({ path: '/health', method: 'GET' }).reply(200, 'ok').persist()const result1 = await request('http://localhost:3000/foo')const result2 = await request('http://localhost:3000/hello')// /health can be called any number of times
SnapshotAgent extends MockAgent with automatic record-and-replay. Run your tests once in record mode against a real API to capture responses, then switch to playback mode so the tests run offline against the saved snapshots.
Record mode
Playback mode
Recording real API responses
import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'// Run this once with SNAPSHOT_MODE=recordconst agent = new SnapshotAgent({ mode: 'record', snapshotPath: './test/snapshots/api-test.json'})setGlobalDispatcher(agent)// Real HTTP call — gets recordedconst response = await fetch('https://api.example.com/users')const users = await response.json()await agent.saveSnapshots()
Replaying recorded responses
import { SnapshotAgent, setGlobalDispatcher } from 'undici'const agent = new SnapshotAgent({ mode: 'playback', snapshotPath: './test/snapshots/api-test.json'})setGlobalDispatcher(agent)// Uses saved snapshot — no network I/Oconst response = await fetch('https://api.example.com/users')const users = await response.json()
import { test } from 'node:test'import { strict as assert } from 'node:assert'import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'test('user API returns an array of users', async (t) => { const originalDispatcher = getGlobalDispatcher() const agent = new SnapshotAgent({ mode: process.env.SNAPSHOT_MODE || 'playback', snapshotPath: './test/snapshots/user-api.json' }) setGlobalDispatcher(agent) // Restore original dispatcher after the test t.after(() => setGlobalDispatcher(originalDispatcher)) const response = await fetch('https://jsonplaceholder.typicode.com/users') const users = await response.json() assert.ok(Array.isArray(users), 'response should be an array') assert.ok(users.length > 0, 'array should be non-empty') assert.ok('id' in users[0], 'each user should have an id')})
Undici’s Client automatically reconnects after a socket error. In tests this can mask bugs — a request silently disconnects, reconnects, and succeeds when it should have failed. Add a disconnect guard to catch these cases:
Disconnect guard for Client tests
const { Client } = require('undici')const { test, after } = require('node:test')const { tspl } = require('@matteo.collina/tspl')test('example with disconnect guard', async (t) => { t = tspl(t, { plan: 1 }) const client = new Client('http://localhost:3000') after(() => client.close()) client.on('disconnect', () => { // close() and destroy() also emit 'disconnect' — ignore expected ones if (!client.closed && !client.destroyed) { t.fail('unexpected disconnect') } }) // ... test logic ...})
Skip the disconnect guard for tests that intentionally trigger disconnects: signal aborts, server-side socket destruction, body stream destruction, timeout errors, successful protocol upgrades, and HTTP parser errors from malformed responses.