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 })
)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