Files
SnowLuma/.pnpm-store/v11/files/5e/0e9f68408fe296d86034eab700823db87d3d5f8e36ae4672e69718a513f5bc89c921788771f387a247cc9f095312c09709b90934703869e63f20d00b624b33
TanTan 2a69b23b47
Dev Build / cleanup (push) Canceled after 0s
Dev Build / typecheck (push) Canceled after 0s
Dev Build / build (linux-arm64, ubuntu-24.04-arm, linux-arm64) (push) Canceled after 0s
Dev Build / build (linux-x64, ubuntu-latest, linux-x64) (push) Canceled after 0s
Dev Build / build (win-x64, windows-latest, win32-x64) (push) Canceled after 0s
Dev Build / dispatch-dev-docker (push) Skipped
docs: 仓库链接指向本镜像仓库
2026-11-13 09:27:07 +08:00

427 lines
13 KiB
Plaintext

# Interceptors
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.
## Usage
```js
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`:
```js
import { Client, interceptors } from 'undici'
const client = new Client('https://example.com').compose(
interceptors.retry({ maxRetries: 2 })
)
```
---
## Custom interceptors
Custom interceptors use the same shape as
[`dispatcher.compose()`](./Dispatcher.md#dispatchercomposeinterceptors-interceptor):
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)`](./Dispatcher.md#dispatcherdispatchoptions-handler).
```js
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])`
Reads 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**
* `opts` {Object} (optional)
* `maxSize` {number} Maximum number of bytes to read and discard. Responses
whose declared or received body size exceeds this value are aborted.
**Default:** `1_048_576` (1 MiB).
Per-request override: set `dumpMaxSize` on the dispatch options to override
the global `maxSize` for a specific request.
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
**Example**
```js
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.dump({ maxSize: 128 * 1024 }) // discard up to 128 KiB
)
```
---
## `interceptors.retry([opts])`
Automatically retries failed requests using the same options accepted by
[`RetryHandler`](./RetryHandler.md).
**Parameters**
* `opts` {RetryHandler.RetryOptions} (optional) Global retry options applied
to every request. Individual requests can override via `opts.retryOptions`.
See [`RetryOptions`](./RetryHandler.md#retryoptions) for the full list of
accepted fields.
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
**Example**
```js
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])`
Follows HTTP redirects (3xx responses) automatically.
**Parameters**
* `opts` {Object} (optional)
* `maxRedirections` {number} Maximum number of redirects to follow. Passing
`0` disables redirect following entirely. **Default:** `undefined`
(inherits from the per-request `maxRedirections` option).
* `throwOnMaxRedirect` {boolean} When `true`, throws an error when a
redirect would be followed but the redirect limit has been reached, instead
of returning that redirect response. A non-redirect response after exactly
`maxRedirections` redirects is returned normally. **Default:** `false`.
* `stripHeadersOnRedirect` {string[]} List of header names to remove from
the request when following any redirect. **Default:** `[]`.
* `stripHeadersOnCrossOriginRedirect` {string[]} List of header names to
remove from the request when following a cross-origin redirect (i.e. the
redirect target has a different origin). Useful for stripping
`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**
```js
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.redirect({
maxRedirections: 10,
throwOnMaxRedirect: true,
stripHeadersOnCrossOriginRedirect: ['authorization', 'cookie']
})
)
```
---
## `interceptors.decompress([opts])`
Automatically decompresses response bodies encoded with `gzip`, `x-gzip`,
`br` (Brotli), `deflate`, or `zstd`. Any other coding, including `compress`
and `x-compress`, is left untouched, matching `fetch()`. As required by
[RFC 9659](https://www.rfc-editor.org/rfc/rfc9659.html), a `zstd` frame that
requires a window size larger than 8 MB fails to decode.
> **Experimental:** This interceptor is experimental and subject to change.
> A one-time `ExperimentalWarning` is emitted on first use.
**Parameters**
* `opts` {Object} (optional)
* `skipStatusCodes` {number[]} Status codes for which decompression is
skipped. **Default:** `[204, 304]`.
* `skipErrorResponses` {boolean} When `true`, responses with a status code
>= 400 are not decompressed. **Default:** `true`.
* `maxSize` {number} Maximum decompressed response size in bytes for each
decompression stage. The request fails with a
`ResponseExceededMaxSizeError` if a stage exceeds this limit. Set to `0` to
disable the limit. **Default:** `0`.
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
**Example**
```js
import { Agent, interceptors } from 'undici'
const agent = new Agent().compose(
interceptors.decompress({
skipStatusCodes: [204, 304],
skipErrorResponses: false, // decompress error bodies too
maxSize: 16 * 1024 * 1024 // limit decoded bodies to 16 MiB
})
)
```
---
## `interceptors.responseError([opts])`
Converts 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**
* `opts` {Object} (optional)
* `maxSize` {number} Maximum number of response body bytes to include in the
error. If the body exceeds this limit, the resulting `ResponseError`
contains the retained prefix with `bodyTruncated` set to `true`. Set to `0`
to disable the limit. **Default:** `1048576` (1 MiB).
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
**Example**
```js
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])`
Caches 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**
* `opts` {Object} (optional)
* `maxTTL` {number} Maximum number of milliseconds a DNS record is cached,
regardless of the TTL returned by the resolver. **Default:** `0`
(use the TTL from the DNS record).
* `maxItems` {number} Maximum number of origins to cache simultaneously.
Oldest entries are evicted when the limit is reached. **Default:**
`Infinity`.
* `dualStack` {boolean} When `true`, both IPv4 (`A`) and IPv6 (`AAAA`)
records are looked up and the interceptor picks between them based on
`affinity`. **Default:** `true`.
* `affinity` {4 | 6 | null} Preferred IP family when `dualStack` is
enabled. `null` lets the interceptor alternate between families.
**Default:** `null`.
* `lookup` {Function} (optional) Custom DNS resolution function with the
same signature as `node:dns`'s `lookup` callback form:
`(origin, options, callback) => void`.
* `pick` {Function} (optional) Custom record-selection function called with
`(origin, records, affinity)` to choose which resolved address to use.
* `storage` {DNSStorage} (optional) Custom storage backend. Must implement
`get`, `set`, `delete`, `full`, and `size`.
**`DNSStorage` interface**
```ts
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**
```js
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])`
Caches HTTP responses according to RFC 9111 (HTTP Caching). See
[`CacheStore`](./CacheStore.md) for information on providing a custom
backing store.
**Parameters**
* `opts` {CacheHandler.CacheOptions} (optional) See the
[`CacheStore`](./CacheStore.md) documentation for accepted fields.
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
**Example**
```js
import { Agent, interceptors, cacheStores } from 'undici'
const agent = new Agent().compose(
interceptors.cache({ store: new cacheStores.MemoryCacheStore() })
)
```
---
## `interceptors.deduplicate([opts])`
Deduplicates 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**
* `opts` {Object} (optional)
* `methods` {string[]} HTTP methods to deduplicate. Must be safe HTTP
methods (`GET`, `HEAD`, `OPTIONS`, `TRACE`). **Default:** `['GET']`.
* `skipHeaderNames` {string[]} Header names whose presence in a request
causes it to bypass deduplication entirely. Matching is
case-insensitive. **Default:** `[]`.
* `excludeHeaderNames` {string[]} Header names to exclude from the
deduplication key. Requests that differ only in these headers are still
deduplicated together. Useful for headers like `x-request-id` that vary
per request but should not prevent deduplication. Matching is
case-insensitive. **Default:** `[]`.
* `maxBufferSize` {number} Maximum number of bytes buffered per paused
waiting handler. If a waiting handler exceeds this threshold it is failed
with an `AbortError` to prevent unbounded memory growth. **Default:**
`5_242_880` (5 MiB).
**Returns:** {Dispatcher.DispatcherComposeInterceptor}
**Example**
```js
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
})
)
```
---
## Composing multiple interceptors
Interceptors are applied in the order they appear in the `compose()` call.
The first interceptor in the array wraps the outermost layer.
```js
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:
1. **dns** — resolves and caches the target IP
2. **retry** — retries the dispatch on transient failures
3. **redirect** — follows any 3xx redirects
4. **decompress** — decompresses the response body
5. **responseError** — converts 4xx/5xx into thrown errors