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.

SnapshotAgent extends MockAgent to provide automatic record-and-replay testing. Instead of writing interceptors by hand, you run your tests once in record mode against a real server — the agent captures every request and response to a JSON snapshot file. Subsequent test runs use playback mode to replay those recorded responses without touching the network, giving you deterministic integration tests that behave exactly like a live call did.
SnapshotAgent is experimental and subject to change. Node.js will emit an ExperimentalWarning the first time it is instantiated in a process.

How it works

1

Record

Run your tests with mode: 'record'. The agent makes real HTTP requests and saves each request/response pair to a snapshot file.
2

Commit the snapshot file

Add the generated JSON file to version control. The file acts as a fixture for all future test runs.
3

Replay

Run tests with mode: 'playback' (the default in CI). The agent loads the snapshot file and serves recorded responses without making any network calls.
4

Update when the API changes

Re-run with mode: 'update' to refresh existing snapshots while keeping any not yet recorded.

Constructor

Basic SnapshotAgent instantiation
import { SnapshotAgent } from 'undici'

const agent = new SnapshotAgent({
  mode: 'playback',
  snapshotPath: './test/snapshots/api.json'
})
options
object
All MockAgentOptions are supported in addition to the following.

Modes

Record mode

Makes real HTTP requests and writes each response to the snapshot file. Call agent.saveSnapshots() when recording is complete.
Record mode
import { SnapshotAgent, setGlobalDispatcher } from 'undici'

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

// Real HTTP request — recorded automatically
const response = await fetch('https://api.example.com/users')
const users = await response.json()

// Write snapshots to disk
await agent.saveSnapshots()

Playback mode

Loads the snapshot file and serves recorded responses without making real requests. In 'playback' mode, snapshotPath is required. A request that does not match any snapshot throws UndiciError.
Playback mode
import { SnapshotAgent, setGlobalDispatcher } from 'undici'

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

// Served from snapshot — no network call
const response = await fetch('https://api.example.com/users')

Update mode

Uses existing snapshots when a match is found. For requests with no recorded snapshot, a real network request is made and its response is saved. snapshotPath is required.
Update mode
import { SnapshotAgent, setGlobalDispatcher } from 'undici'

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

// Uses snapshot if exists, otherwise records a new one
const response = await fetch('https://api.example.com/new-endpoint')
await agent.saveSnapshots()

Instance methods

agent.saveSnapshots(filePath?)

Saves all recorded snapshots to disk.
filePath
string
Path to save to. Defaults to the snapshotPath supplied at construction.
returns
Promise<void>

agent.loadSnapshots(filePath?)

Loads snapshots from a file. In playback and update modes, this is called automatically at construction. Call it manually if you need to load from an additional file.
filePath
string
Path to load from. Defaults to the snapshotPath supplied at construction.
returns
Promise<void>

agent.getRecorder()

Returns the underlying SnapshotRecorder instance for advanced introspection.
returns
SnapshotRecorder
Inspecting recorded count
const recorder = agent.getRecorder()
console.log(`Recorded ${recorder.size()} interactions`)

agent.getMode()

Returns the current operating mode.
returns
'record' | 'playback' | 'update'

agent.clearSnapshots()

Clears all in-memory snapshots without touching the snapshot file.

agent.replaceSnapshots(snapshotData)

Replaces the entire in-memory snapshot store with the given data. Useful for filtering or transforming snapshots before saving.

agent.close()

Saves any pending snapshots (if autoFlush is enabled), closes the underlying real agent, and calls super.close(). Always await this in cleanup hooks.

Snapshot file format

Snapshots are stored as a JSON array. The response body is base64-encoded.
Snapshot file structure
[
  {
    "hash": "dGVzdC1oYXNo...",
    "snapshot": {
      "request": {
        "method": "GET",
        "url": "https://api.example.com/users",
        "headers": {},
        "body": null
      },
      "response": {
        "statusCode": 200,
        "headers": {
          "content-type": "application/json"
        },
        "body": "W3siaWQiOjF9XQ==",
        "trailers": {}
      },
      "timestamp": "2024-01-01T00:00:00.000Z"
    }
  }
]
Multiple identical requests are recorded as separate sequential entries. In playback mode they are replayed in order; the last response repeats for any additional calls beyond what was recorded.

Advanced configuration

Body normalization

Strip volatile fields from the request body before matching so that timestamps or generated IDs do not prevent snapshots from matching:
Normalizing request body for matching
const agent = new SnapshotAgent({
  mode: 'playback',
  snapshotPath: './snapshots.json',
  normalizeBody: (body) => {
    if (!body) return ''
    const parsed = JSON.parse(String(body))
    delete parsed.timestamp
    delete parsed.requestId
    return JSON.stringify(parsed)
  }
})

Header filtering

Controlling header matching and storage
const agent = new SnapshotAgent({
  mode: 'record',
  snapshotPath: './snapshots.json',
  matchHeaders: ['content-type', 'accept'],        // only these headers participate in matching
  ignoreHeaders: ['user-agent', 'date'],            // ignored during matching, still stored
  excludeHeaders: ['authorization', 'x-api-key']   // stripped from snapshots entirely
})

Excluding URLs

Bypassing specific URLs
const agent = new SnapshotAgent({
  mode: 'record',
  snapshotPath: './snapshots.json',
  excludeUrls: [
    'https://analytics.example.com',
    /\/api\/v\d+\/health/,
    'telemetry'
  ]
})

Conditional recording

Recording only specific requests
const agent = new SnapshotAgent({
  mode: 'record',
  snapshotPath: './snapshots.json',
  shouldRecord: (requestOpts) => {
    const url = new URL(requestOpts.path, requestOpts.origin)
    return requestOpts.method === 'GET' && url.pathname.startsWith('/api/v1/')
  }
})

Security considerations

Snapshot files can contain sensitive data including authentication tokens, cookies, and personal information. Handle them carefully.
Always exclude sensitive headers from snapshots:
Excluding sensitive headers
const agent = new SnapshotAgent({
  mode: 'record',
  snapshotPath: './snapshots.json',
  excludeHeaders: [
    'authorization',
    'x-api-key',
    'cookie',
    'set-cookie',
    'x-auth-token'
  ],
  shouldRecord: (requestOpts) => {
    const url = new URL(requestOpts.path, requestOpts.origin)
    // Never record authentication endpoints
    return !url.pathname.includes('/auth/') && !url.pathname.includes('/login')
  }
})
Add environment-specific snapshot files to .gitignore if they contain real credentials or personal data. Commit only sanitised snapshot files.

Test integration example

Environment-controlled record/playback in node:test
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'

// SNAPSHOT_MODE=record npm test  → record against the live API
// npm test                       → replay from snapshot
const mode = process.env.SNAPSHOT_MODE ?? 'playback'

test('GET /users returns an array', async (t) => {
  const original = getGlobalDispatcher()

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

  t.after(async () => {
    if (mode === 'record') {
      await agent.saveSnapshots()
    }
    await agent.close()
    setGlobalDispatcher(original)
  })

  const response = await fetch('https://jsonplaceholder.typicode.com/users')
  const users = await response.json()

  assert.equal(response.status, 200)
  assert.ok(Array.isArray(users))
  assert.ok(users.length > 0)
})
Use a test helper that creates a SnapshotAgent per test suite with a descriptive snapshot file name. This keeps snapshot files small and makes failures easy to diagnose.

Comparison with manual MockAgent

MockAgent (manual intercepts)SnapshotAgent
Setup effortHigh — write every interceptor by handLow — record once, reuse automatically
AccuracyDepends on how well you reproduce real responsesExact replica of real responses
MaintenanceUpdate interceptors when API changesRe-run in record or update mode
Offline capableYesYes (playback mode)
Best forUnit tests, controlled error scenariosIntegration tests needing realistic data

Build docs developers (and LLMs) love