Interceptors are middleware functions that wrap a dispatcher’sDocumentation 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.
dispatch method to add cross-cutting behaviour — caching, retrying, redirecting, decompressing, and more. undici ships with eight built-in interceptors accessible from undici.interceptors, and any number of interceptors can be layered together using the .compose() method available on every dispatcher.
How .compose() works
dispatcher.compose() accepts one or more interceptor functions and returns a new dispatcher that applies them in order. Each interceptor receives the dispatch function of the one below it in the stack, so the first argument to .compose() runs first (outermost), and the last runs closest to the actual network call.
Composing multiple interceptors
Array form
interceptors.cache(options)
The cache interceptor stores HTTP responses and serves them from the cache on subsequent matching requests, fully honouring RFC 9111 semantics (Cache-Control, ETag, Last-Modified, Vary, stale-while-revalidate, stale-if-error).
Only safe HTTP methods can be cached. Unsafe methods (
PUT, POST, PATCH, DELETE) automatically invalidate cached entries for the same origin.Basic cache interceptor
Options
The cache store to use. Must implement
get, createWriteStream, and delete. Built-in options are MemoryCacheStore and SqliteCacheStore.HTTP methods whose responses will be cached. Only safe methods are accepted (
GET, HEAD, OPTIONS, TRACE).When set to a number (TTL in seconds), responses that do not carry explicit
Cache-Control directives are cached for that duration. Implements the RFC 9111 heuristic caching allowance.Whether the cache behaves as a shared cache (e.g. a proxy) or a private cache (e.g. a browser). Affects how
Cache-Control: private responses are handled.Allowlist of origins to cache. Requests to origins not in the list bypass the cache entirely. Strings are matched case-insensitively;
RegExp instances are tested against the lowercase origin.HTTP caching semantics
The cache interceptor respects the following directives automatically:| Directive | Behaviour |
|---|---|
Cache-Control: no-store | Bypasses the cache completely for this request |
Cache-Control: no-cache | Forces revalidation before serving a cached response |
Cache-Control: max-age | Treats response as fresh for the given number of seconds |
Cache-Control: stale-while-revalidate | Serves the stale response immediately while revalidating in the background |
Cache-Control: stale-if-error | Serves a stale response when the upstream returns an error |
ETag / If-None-Match | Used in conditional GET revalidation requests |
Last-Modified / If-Modified-Since | Used in conditional GET revalidation requests |
Vary | Cached entries are keyed per request header combination |
MemoryCacheStore
An in-process, in-memory cache. Best for single-process applications or testing. Exported from undici.cacheStores.
Maximum total size in bytes of all stored responses combined (default 100 MB).
Maximum number of cached responses. Once reached, new entries are not stored.
Maximum size in bytes for a single response body (default 5 MB). Responses larger than this are not cached.
SqliteCacheStore
A persistent cache backed by Node.js’ built-in node:sqlite module. Survives process restarts and is safe to share across forked workers on the same machine.
File path for the SQLite database. Defaults to an in-memory database.
Maximum number of entries to persist. When the limit is reached, older entries are evicted.
Maximum size in bytes for a single response body. Responses larger than this value will not be stored.
Examples
- MemoryCacheStore
- SqliteCacheStore
In-memory cache
interceptors.retry(options)
The retry interceptor wraps every request in a RetryHandler, automatically re-dispatching failed requests with exponential backoff. It is transport-layer aware: it uses Range headers and ETag checks to resume partially received response bodies without re-downloading already-received bytes.
Requests with stateful bodies (streams,
AsyncIterable) are not retried because the body cannot be replayed once partially consumed.Options
Maximum number of retry attempts before the error is propagated to the caller.
Minimum wait time in milliseconds before the first retry.
Upper bound in milliseconds for the computed retry delay. Exponential growth is capped at this value.
Multiplier applied to the previous delay to produce the next one (
minTimeout * timeoutFactor ^ attempt).When
true, respects the server-sent Retry-After response header. Both date strings and relative seconds are supported. The computed wait is still capped at maxTimeout.HTTP status codes that trigger a retry. Responses with status codes not in this list are forwarded as-is to the handler.
HTTP methods eligible for retry. Non-idempotent methods such as
POST and PATCH are excluded by default.Node.js error codes and undici error codes that cause a retry.
When
true, the final error is thrown after all retries are exhausted. Set to false to receive the error response body instead of an exception — useful when you need to inspect the error payload returned by the server.Custom retry decision function. When provided, overrides the default exponential backoff logic. Call
callback(null) to schedule a retry, or callback(err) to abort and surface the error. See RetryHandler for the full signature.Exponential backoff behaviour
The delay before attemptn is calculated as:
Retry-After header, that value takes precedence (still capped at maxTimeout).
Example
Retry interceptor with custom options
interceptors.redirect(options)
The redirect interceptor automatically follows HTTP 3xx responses by re-dispatching the request to the Location URL. It maintains a redirect history, enforces an upper bound on the number of hops, and handles method changes mandated by the spec (e.g. POST → GET on 301/302/303).
Options
Maximum number of redirections to follow. When
null or 0, redirects are not followed and the 3xx response is forwarded to the handler. If maxRedirections is also set per-request in dispatchOptions, the per-request value takes precedence.When
true, exceeding maxRedirections throws an error instead of forwarding the final 3xx response.Redirect status code handling
| Status | Method change | Body |
|---|---|---|
| 301, 302 (POST only) | POST → GET | Body dropped |
| 303 (non-HEAD) | Any → GET | Body dropped |
| 307, 308 | No change | Body preserved |
| 300 | No change | Body ignored |
authorization, cookie, proxy-authorization) are automatically stripped when following cross-origin redirects.
Example
Redirect interceptor
interceptors.dns(options)
The DNS interceptor resolves hostnames to IP addresses and caches the results for maxTTL milliseconds. On each dispatch it substitutes the resolved IP in the origin, preserving the original Host header for correct TLS SNI and virtual hosting. It also implements Happy Eyeballs-style dual-stack fallback: on a connection error it retries with the other IP family before surfacing the error.
Options
Maximum time in milliseconds to cache a resolved DNS record. Individual records may have shorter TTLs. Default is 10 seconds.
Maximum number of hostnames to keep in the DNS cache simultaneously.
When
true, both IPv4 and IPv6 records are resolved and the interceptor alternates between them. When false, only the family specified by affinity is used.Preferred IP family. When
dualStack is enabled and affinity is set, the preferred family is always tried first.Custom DNS resolution function with the signature
(origin: URL, options, callback). Use this to integrate a custom resolver or a mock in tests.Custom IP selection function with the signature
(origin: URL, records, affinity). Allows implementing custom load-balancing strategies across multiple IPs.Custom DNS record storage implementing
{ get, set, full, delete }. Provide this to share a DNS cache across multiple interceptor instances.Example
DNS interceptor with short TTL
interceptors.dump(options)
The dump interceptor discards the response body up to maxSize bytes, then signals completion to the underlying connection. This releases the connection back to the pool without waiting for the caller to consume the body — useful when you only need headers (e.g. checking status codes) and want to keep connection reuse efficient.
Options
Maximum number of bytes to read and discard (default 1 MB). If the
Content-Length header reports a body larger than maxSize, the interceptor aborts immediately with a RequestAbortedError.maxSize can also be controlled per-request via dispatchOptions.dumpMaxSize, which takes precedence over the interceptor-level default.Example
Dump interceptor — headers-only pattern
interceptors.decompress(options)
The decompress interceptor transparently decompresses response bodies encoded with gzip, x-gzip, deflate, compress, x-compress, br (Brotli), or zstd. It removes the content-encoding and content-length headers from the forwarded response so downstream handlers see raw bytes.
Options
Status codes for which decompression is skipped.
204 No Content and 304 Not Modified never carry a body, so decompression is irrelevant.When
true, responses with status >= 400 are forwarded as-is without decompression.Supported encodings
Content-Encoding value | Decompressor |
|---|---|
gzip, x-gzip | zlib.createGunzip() |
deflate, compress, x-compress | zlib.createInflate() |
br | zlib.createBrotliDecompress() |
zstd | zlib.createZstdDecompress() |
For security, the maximum number of chained
Content-Encoding values is capped at 5. Responses with more than 5 encodings are rejected with an error.Example
Decompress interceptor
interceptors.deduplicate(options)
The deduplicate interceptor coalesces concurrent identical requests into a single outgoing request. Additional callers that arrive while the first request is in flight are attached as waiting handlers; when the response arrives, all waiters receive the same response — headers, body chunks, and trailers — without any additional network round-trips.
Only safe HTTP methods can be deduplicated. The default is
['GET'].Options
HTTP methods eligible for deduplication. Must be safe methods (
GET, HEAD, OPTIONS, TRACE).Header names that, if present in an incoming request, cause it to bypass deduplication entirely. Header name matching is case-insensitive.
Header names excluded from the deduplication key. Requests that differ only in these headers are still treated as duplicates. Useful for tracing headers like
x-request-id.Maximum bytes buffered per waiting handler (default 5 MB). If a waiting handler is paused and this threshold is exceeded, it is aborted to prevent unbounded memory growth.
Example
Deduplicate interceptor
interceptors.responseError()
The responseError interceptor converts HTTP error responses (status 400 and above) into thrown ResponseError exceptions. This lets you use try/catch for error handling instead of checking statusCode on every response.
responseError interceptor
application/json and text/plain content types and parses them into err.body. For all other content types, err.body is an empty string.
Combining interceptors
Interceptors compose cleanly. The order matters: the first interceptor in.compose() is the outermost wrapper and runs first on every request. A common production setup layers retry around cache around decompression:
Production-ready interceptor stack