Undici ships with a set of built-in interceptors that can be composed via
dispatcher.compose() to add cross-cutting behaviour such as automatic
retries, response decompression, redirect following, DNS caching, and more.
import { Agent, interceptors } from 'undici'
const { retry, redirect, decompress, dump, responseError, dns, cache, deduplicate } = interceptors
const agent = new Agent().compose([
retry({ maxRetries: 3 }),
redirect({ maxRedirections: 5 }),
decompress()
])
const response = await agent.request({ origin: 'https://example.com', path: '/', method: 'GET' })You can also apply interceptors to a single Client or Pool:
import { Client, interceptors } from 'undici'
const client = new Client('https://example.com').compose(
interceptors.retry({ maxRetries: 2 })
)Custom interceptors use the same shape as
dispatcher.compose():
an interceptor takes a dispatch function and returns another dispatch-like
function with the same (options, handler) signature.
When an interceptor wraps the handler, forward the callbacks that it does not
handle itself. The complete handler callback list is documented under
dispatcher.dispatch(options, handler).
import { Agent } from 'undici'
const timingInterceptor = dispatch => {
return (options, handler) => {
const started = performance.now()
return dispatch(options, {
...handler,
onResponseStart (controller, statusCode, headers, statusMessage) {
const duration = Math.round(performance.now() - started)
const method = options.method ?? 'GET'
const origin = options.origin ?? ''
console.log(`${method} ${origin}${options.path} -> ${statusCode} in ${duration}ms`)
return handler.onResponseStart?.(
controller,
statusCode,
headers,
statusMessage
)
},
onResponseError (controller, error) {
const duration = Math.round(performance.now() - started)
console.error(`request failed after ${duration}ms`, error)
return handler.onResponseError?.(controller, error)
}
})
}
}
const dispatcher = new Agent().compose(timingInterceptor)
const { body } = await dispatcher.request({
origin: 'https://example.com',
path: '/',
method: 'GET'
})
await body.dump()
await dispatcher.close()interceptors.dump(opts?): voidReads and discards the response body up to a configurable size limit. Useful for keeping a connection alive after an error response without reading the body yourself.
Parameters
Per-request override: set dumpMaxSize on the dispatch options to override
the global maxSize for a specific request.
Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.dump({ maxSize: 128 * 1024 }) // discard up to 128 KiB
)interceptors.retry(opts?): voidAutomatically retries failed requests using the same options accepted by
RetryHandler.
Parameters
opts.retryOptions
.
See
RetryOptions
for the full list of
accepted fields.Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.retry({
maxRetries: 5,
minTimeout: 200,
maxTimeout: 5000,
timeoutFactor: 2,
statusCodes: [429, 502, 503, 504]
})
)interceptors.redirect(opts?): voidFollows HTTP redirects (3xx responses) automatically.
Parameters
<Object><number>0
disables redirect following entirely.
Default:
undefined
(inherits from the per-request
maxRedirections
option).<boolean>true
, throws an error once the
redirect limit is reached instead of returning the final redirect response.
Default:
false
.<string>[]
.<string>Authorization
on cross-origin hops.
Default:
[]
.Per-request override: any of the four options above can also be set directly on the dispatch options to override the interceptor defaults for a specific request.
Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.redirect({
maxRedirections: 10,
throwOnMaxRedirect: true,
stripHeadersOnCrossOriginRedirect: ['authorization', 'cookie']
})
)interceptors.decompress(opts?): voidAutomatically decompresses response bodies encoded with gzip, x-gzip,
br (Brotli), deflate, compress, x-compress, or zstd.
Experimental: This interceptor is experimental and subject to change. A one-time
ExperimentalWarningis emitted on first use.
Parameters
Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.decompress({
skipStatusCodes: [204, 304],
skipErrorResponses: false // decompress error bodies too
})
)interceptors.responseError(opts?): voidConverts 4xx/5xx responses into thrown ResponseError instances, making it
easy to handle HTTP errors with a standard try/catch block.
The error body is automatically decoded for application/json and
text/plain responses. For JSON responses the body is parsed and exposed as
error.body.
Parameters
<Object>Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors, errors } from 'undici'
const agent = new Agent().compose(interceptors.responseError())
try {
await agent.request({ origin: 'https://example.com', path: '/not-found', method: 'GET' })
} catch (err) {
if (err instanceof errors.ResponseError) {
console.error(err.status, err.body)
}
}interceptors.dns(opts?): voidCaches DNS lookups so that repeated requests to the same origin reuse the resolved IP address instead of performing a fresh lookup every time. Supports dual-stack (IPv4 + IPv6) and custom lookup/storage implementations.
Parameters
<Object><number>0
(use the TTL from the DNS record).<number>Infinity
.<boolean>true
, both IPv4 (
A
) and IPv6 (
AAAA
)
records are looked up and the interceptor picks between them based on
affinity
.
Default:
true
.<4>
|
<6>
|
<null>
Preferred IP family when
dualStack
is
enabled.
null
lets the interceptor alternate between families.
Default:
null
.<Function>node:dns
's
lookup
callback form:
(origin, options, callback) => void
.<Function>(origin, records, affinity)
to choose which resolved address to use.get
,
set
,
delete
,
full
, and
size
.DNSStorage interface
interface DNSStorage {
size: number
get(origin: string): DNSInterceptorOriginRecords | null
set(origin: string, records: DNSInterceptorOriginRecords | null, options: { ttl: number }): void
delete(origin: string): void
full(): boolean
}Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.dns({
maxTTL: 60_000, // cache for at most 60 seconds
dualStack: true,
affinity: 4 // prefer IPv4
})
)interceptors.cache(opts?): voidCaches HTTP responses according to RFC 9111 (HTTP Caching). See
CacheStore for information on providing a custom
backing store.
Parameters
CacheStore
documentation for accepted fields.Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors, cacheStores } from 'undici'
const agent = new Agent().compose(
interceptors.cache({ store: new cacheStores.MemoryCacheStore() })
)interceptors.deduplicate(opts?): voidDeduplicates concurrent identical requests so that only one is sent over the
wire. All waiting callers receive the same response once the in-flight request
completes. Only safe HTTP methods (e.g. GET, HEAD) may be deduplicated.
Parameters
<Object><string>GET
,
HEAD
,
OPTIONS
,
TRACE
).
Default:
['GET']
.<string>[]
.<string>x-request-id
that vary
per request but should not prevent deduplication. Matching is
case-insensitive.
Default:
[]
.<number>AbortError
to prevent unbounded memory growth.
Default:
5_242_880
(5 MiB).Returns: <Dispatcher.DispatcherComposeInterceptor>
Example
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.deduplicate({
methods: ['GET', 'HEAD'],
excludeHeaderNames: ['x-request-id', 'x-trace-id'],
maxBufferSize: 2 * 1024 * 1024
})
)Interceptors are applied in the order they appear in the compose() call.
The first interceptor in the array wraps the outermost layer.
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose([
interceptors.dns({ maxTTL: 30_000 }),
interceptors.retry({ maxRetries: 3 }),
interceptors.redirect({ maxRedirections: 5 }),
interceptors.decompress(),
interceptors.responseError()
])In the example above the request flow is:
- dns — resolves and caches the target IP
- retry — retries the dispatch on transient failures
- redirect — follows any 3xx redirects
- decompress — decompresses the response body
- responseError — converts 4xx/5xx into thrown errors