mirror of
https://github.com/nodejs/node.git
synced 2026-10-28 10:15:07 +08:00
deps: update undici to 8.7.0
PR-URL: https://github.com/nodejs/node/pull/64282 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Richard Lau <richard.lau@ibm.com>
This commit is contained in:
1 parent
8a84e6be90
commit
0c138da549
83 files changed
+10695
-4868
No files matched your search
Vendored
+153
-1
@@ -10,6 +10,7 @@
|
||||
* [Building for externally shared node builtins](#external-builds)
|
||||
* [Benchmarks](#benchmarks)
|
||||
* [Documentation](#documentation)
|
||||
* [Reproduction](#reproduction)
|
||||
* [Developer's Certificate of Origin 1.1](#developers-certificate-of-origin)
|
||||
* [Moderation Policy](#moderation-policy)
|
||||
|
||||
@@ -58,7 +59,7 @@ npm i
|
||||
> This requires [docker](https://www.docker.com/) installed on your machine.
|
||||
|
||||
```bash
|
||||
npm run build-wasm
|
||||
npm run build:wasm
|
||||
```
|
||||
|
||||
#### Copy the sources to `undici`
|
||||
@@ -198,6 +199,157 @@ cd docs && npm i && npm run serve
|
||||
|
||||
The documentation will be available at `http://localhost:3000`.
|
||||
|
||||
<a id="reproduction"></a>
|
||||
### Reproduction
|
||||
|
||||
When reporting a bug, a high-quality reproduction helps maintainers diagnose and fix the issue quickly. Follow the guidelines below to create a minimal, standalone reproduction script.
|
||||
|
||||
#### What Makes a Good Reproduction
|
||||
|
||||
- **Standalone**: The script must be self-contained and run without external dependencies beyond `undici` and Node.js built-in modules.
|
||||
- **Minimal**: Strip everything unrelated to the bug — no production configs, no frameworks, no unnecessary middleware.
|
||||
- **Reproducible**: Run the script and confirm the bug occurs before submitting.
|
||||
- **Specific**: Include the exact `undici` API call that triggers the issue (e.g., `Client`, `Pool`, `Agent`, `fetch`, `request`, `stream`, `pipeline`).
|
||||
- **Run with Node's test runner**: Use `node --test` (Node 20+) or `node test/your-repro.js`.
|
||||
|
||||
#### Standalone Server Pattern
|
||||
|
||||
Use `createServer` from `node:http` (or `node:https` for TLS-related bugs) to run a local server inside the same script. This avoids external service dependencies and keeps the reproduction fully self-contained.
|
||||
|
||||
```javascript
|
||||
'use strict'
|
||||
|
||||
const { test } = require('node:test')
|
||||
const { createServer } = require('node:http')
|
||||
const { once } = require('node:events')
|
||||
const { Client } = require('undici')
|
||||
|
||||
// https://github.com/nodejs/undici/issues/XXXX
|
||||
|
||||
test('short description of the bug', { timeout: 60000 }, async (t) => {
|
||||
t.plan(1)
|
||||
|
||||
// 1. Start a local HTTP server that reproduces the scenario
|
||||
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
|
||||
// Simulate the bug-triggering response
|
||||
res.statusCode = 200
|
||||
res.setHeader('content-length', 100)
|
||||
res.end('hello'.repeat(20))
|
||||
})
|
||||
t.after(() => { server.closeAllConnections?.(); server.close() })
|
||||
|
||||
server.listen(0)
|
||||
await once(server, 'listening')
|
||||
|
||||
// 2. Create the undici client pointing to the local server
|
||||
const client = new Client(`http://localhost:${server.address().port}`)
|
||||
t.after(() => client.close())
|
||||
|
||||
// 3. Perform the request that triggers the bug
|
||||
const { body } = await client.request({ path: '/', method: 'GET' })
|
||||
|
||||
let responseBody = ''
|
||||
for await (const chunk of body) {
|
||||
responseBody += chunk
|
||||
}
|
||||
|
||||
t.assert.strictEqual(responseBody, 'hello'.repeat(20))
|
||||
})
|
||||
```
|
||||
|
||||
Save the script as `test/repro-XXXX.js` and run it:
|
||||
|
||||
```bash
|
||||
node --test test/repro-XXXX.js
|
||||
```
|
||||
|
||||
#### Fetch-Based Reproduction
|
||||
|
||||
For bugs involving `fetch`, use the same standalone server pattern:
|
||||
|
||||
```javascript
|
||||
'use strict'
|
||||
|
||||
const { test } = require('node:test')
|
||||
const { createServer } = require('node:http')
|
||||
const { once } = require('node:events')
|
||||
const { fetch, Agent } = require('undici')
|
||||
|
||||
// https://github.com/nodejs/undici/issues/XXXX
|
||||
|
||||
test('short description of the fetch bug', { timeout: 60000 }, async (t) => {
|
||||
t.plan(1)
|
||||
|
||||
const server = createServer({ joinDuplicateHeaders: true }, async (req, res) => {
|
||||
res.statusCode = 200
|
||||
res.setHeader('content-type', 'application/json')
|
||||
res.end(JSON.stringify({ ok: true }))
|
||||
})
|
||||
t.after(() => server.close())
|
||||
|
||||
server.listen(0)
|
||||
await once(server, 'listening')
|
||||
|
||||
const agent = new Agent({ keepAliveTimeout: 1 })
|
||||
t.after(() => agent.close())
|
||||
|
||||
const response = await fetch(`http://localhost:${server.address().port}/`, {
|
||||
dispatcher: agent
|
||||
})
|
||||
|
||||
await response.body.cancel()
|
||||
|
||||
t.assert.strictEqual(response.status, 200)
|
||||
})
|
||||
```
|
||||
|
||||
#### Using Pool or Agent
|
||||
|
||||
For bugs involving connection pooling or agent behavior:
|
||||
|
||||
```javascript
|
||||
'use strict'
|
||||
|
||||
const { test } = require('node:test')
|
||||
const { createServer } = require('node:http')
|
||||
const { once } = require('node:events')
|
||||
const { Pool } = require('undici')
|
||||
|
||||
// https://github.com/nodejs/undici/issues/XXXX
|
||||
|
||||
test('Pool bug reproduction', { timeout: 60000 }, async (t) => {
|
||||
t.plan(1)
|
||||
|
||||
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
|
||||
res.end('ok')
|
||||
})
|
||||
t.after(() => server.close())
|
||||
|
||||
server.listen(0)
|
||||
await once(server, 'listening')
|
||||
|
||||
const pool = new Pool(`http://localhost:${server.address().port}`)
|
||||
t.after(() => pool.close())
|
||||
|
||||
const data = await pool.request({ path: '/', method: 'GET' })
|
||||
await data.body.dump()
|
||||
|
||||
t.assert.strictEqual(data.statusCode, 200)
|
||||
})
|
||||
```
|
||||
|
||||
#### Adding the Reproduction to the Repository
|
||||
|
||||
If you are submitting a bug fix via a pull request, include a test file `test/issue-XXXX.js` that reproduces the bug and passes with the fix.
|
||||
|
||||
#### Checklist for Submitting a Bug Report
|
||||
|
||||
1. [ ] Write a standalone reproduction script.
|
||||
2. [ ] Run it locally to confirm the bug is reproducible.
|
||||
3. [ ] Attach the script (or a gist link) in the bug report.
|
||||
4. [ ] Include the exact undici version and Node.js version.
|
||||
5. [ ] Describe the expected vs. actual behavior.
|
||||
|
||||
<a id="developers-certificate-of-origin"></a>
|
||||
## Developer's Certificate of Origin 1.1
|
||||
|
||||
|
||||
Vendored
+3
-2
@@ -717,8 +717,9 @@ Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
|
||||
#### Pipelining
|
||||
|
||||
Undici will only use pipelining if configured with a `pipelining` factor
|
||||
greater than `1`. Also it is important to pass `blocking: false` to the
|
||||
request options to properly pipeline requests.
|
||||
greater than `1`. Only enable pipelining when the remote server is trusted.
|
||||
Also it is important to pass `blocking: false` to the request options to
|
||||
properly pipeline requests.
|
||||
|
||||
Undici always assumes that connections are persistent and will immediately
|
||||
pipeline requests, without checking whether the connection is persistent.
|
||||
|
||||
Vendored
+12
@@ -153,6 +153,18 @@ lead to a loss of confidentiality, integrity, or availability.
|
||||
calling `body.formData()` on untrusted responses is considered an application
|
||||
responsibility, not a vulnerability in undici.
|
||||
|
||||
#### HTTP/1.1 keep-alive with untrusted servers
|
||||
|
||||
* HTTP/1.1 responses on a reused connection are ordered, but they do not carry a
|
||||
request identifier. Once a request has been written on a reused connection,
|
||||
a well-formed response sent by the server is the response to that next request
|
||||
from the client's point of view. Undici avoids immediate reuse of
|
||||
non-pipelined HTTP/1.1 connections when callers are already waiting to send
|
||||
more work, but applications that require per-request connection isolation for
|
||||
untrusted or user-controlled servers should disable reuse by using
|
||||
`pipelining: 0` or `maxRequestsPerClient: 1`, or use a protocol with response
|
||||
correlation such as HTTP/2.
|
||||
|
||||
#### Application Misconfiguration
|
||||
|
||||
* Issues arising from incorrect or insecure use of undici APIs (such as
|
||||
|
||||
+247
-43
@@ -1,87 +1,291 @@
|
||||
# Agent
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v3.2.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/dispatcher/agent.js -->
|
||||
|
||||
Agent allows dispatching requests against multiple different origins.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Requests are not guaranteed to be dispatched in order of invocation.
|
||||
An `Agent` dispatches requests against multiple different origins, creating and
|
||||
reusing a per-origin [`Pool`][] (or [`Client`][]) on demand. It is the default
|
||||
dispatcher used by [`request`][], [`stream`][], and [`fetch`][] when no explicit
|
||||
dispatcher is supplied.
|
||||
|
||||
## `new undici.Agent([options])`
|
||||
Requests are not guaranteed to be dispatched in their order of invocation.
|
||||
|
||||
Arguments:
|
||||
```mjs
|
||||
import { Agent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
* **options** `AgentOptions` (optional)
|
||||
const agent = new Agent({ connections: 10 })
|
||||
setGlobalDispatcher(agent)
|
||||
```
|
||||
|
||||
Returns: `Agent`
|
||||
## Class: `Agent`
|
||||
|
||||
### Parameter: `AgentOptions`
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
|
||||
* **maxOrigins** `number` (optional) - Default: `Infinity` - Limits the total number of origins that can receive requests at a time, throwing an `MaxOriginsReachedError` error when attempting to dispatch when the max is reached. If `Infinity`, no limit is enforced.
|
||||
`Agent` keeps an internal map of origins to dispatchers. The first request to a
|
||||
given origin lazily creates a dispatcher through the configured `factory`, and
|
||||
subsequent requests to the same origin reuse it. Idle dispatchers are closed
|
||||
automatically once they have no open connections and are no longer busy.
|
||||
|
||||
### `new Agent([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4365
|
||||
description: Added the `maxOrigins` option to cap the number of origins.
|
||||
-->
|
||||
|
||||
* `options` {AgentOptions} (optional) Extends {PoolOptions}.
|
||||
* `factory` {Function} Builds the dispatcher used for a given origin. When the
|
||||
resolved options contain `connections: 1`, the default factory creates a
|
||||
{Client}; otherwise it creates a {Pool}. **Default:** `(origin, opts) => new Pool(origin, opts)`.
|
||||
* `origin` {URL|string} The origin to create a dispatcher for.
|
||||
* `opts` {Object} The resolved options for the new dispatcher.
|
||||
* Returns: {Dispatcher}
|
||||
* `maxOrigins` {number} Limits the total number of distinct origins that may
|
||||
receive requests at the same time. A {MaxOriginsReachedError} is thrown when
|
||||
a request targets a new origin once the limit is reached. When set to
|
||||
`Infinity`, no limit is enforced. Must be a number greater than `0`.
|
||||
**Default:** `Infinity`.
|
||||
|
||||
`Agent` inherits all {PoolOptions} (and therefore all {ClientOptions}). The
|
||||
per-origin {Pool} it creates uses the default unlimited `connections`, so
|
||||
concurrent requests to the same origin are spread across separate {Client}
|
||||
instances on separate sockets.
|
||||
|
||||
> [!NOTE]
|
||||
> Like `Pool`, `Agent` inherits all [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions). `allowH2` defaults to `true` and `maxConcurrentStreams` to `100`. The per-origin `Pool` it creates uses the default unlimited `connections`, so concurrent requests to the same origin land on separate `Client` instances and separate TCP/TLS sockets — HTTP/2 multiplexing on a shared session does not apply unless `connections` is set to a small value. See [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions).
|
||||
> Because each concurrent request to an origin may use a different {Client},
|
||||
> HTTP/2 multiplexing on a shared session does not apply unless `connections` is
|
||||
> set to a small value (for example `connections: 1`). See {PoolOptions} and
|
||||
> {ClientOptions} for the full set of inherited options such as `allowH2`
|
||||
> (default `true`) and `maxConcurrentStreams` (default `100`).
|
||||
|
||||
## Instance Properties
|
||||
### `agent.closed`
|
||||
|
||||
### `Agent.closed`
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
|
||||
* Type: {boolean}
|
||||
|
||||
### `Agent.destroyed`
|
||||
`true` after [`agent.close()`][] has been called.
|
||||
|
||||
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
|
||||
### `agent.destroyed`
|
||||
|
||||
## Instance Methods
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
### `Agent.close([callback])`
|
||||
* Type: {boolean}
|
||||
|
||||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
|
||||
`true` after [`agent.destroy()`][] has been called, or after [`agent.close()`][]
|
||||
has been called and the shutdown has completed.
|
||||
|
||||
### `Agent.destroy([error, callback])`
|
||||
### `agent.stats`
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
### `Agent.dispatch(options, handler: AgentDispatchOptions)`
|
||||
* Type: {Record<string, ClientStats|PoolStats>}
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
Aggregate statistics for the agent, keyed by origin. Each value is the
|
||||
[`ClientStats`][] or [`PoolStats`][] object reported by the dispatcher currently
|
||||
serving that origin. Origins whose dispatcher does not expose `stats` are
|
||||
omitted.
|
||||
|
||||
#### Parameter: `AgentDispatchOptions`
|
||||
```mjs
|
||||
import { Agent } from 'undici'
|
||||
|
||||
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions)
|
||||
const agent = new Agent()
|
||||
await agent.request({ origin: 'http://localhost:3000', path: '/', method: 'GET' })
|
||||
console.log(agent.stats)
|
||||
// { 'http://localhost:3000': PoolStats { ... } }
|
||||
```
|
||||
|
||||
* **origin** `string | URL`
|
||||
### `agent.dispatch(options, handler)`
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
### `Agent.connect(options[, callback])`
|
||||
* `options` {AgentDispatchOptions} Extends {DispatchOptions}.
|
||||
* `origin` {string|URL} The origin to dispatch the request against. Required.
|
||||
* `handler` {DispatchHandler}
|
||||
* Returns: {boolean} `false` if the dispatcher is busy and the caller should
|
||||
wait for the [`'drain'`][] event before dispatching again.
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
|
||||
Routes the request to the dispatcher for `options.origin`, creating one through
|
||||
the `factory` if needed. Throws an {InvalidArgumentError} when `options.origin`
|
||||
is not a non-empty string or {URL}, and a {MaxOriginsReachedError} when a new
|
||||
origin would exceed `maxOrigins`. Implements [`Dispatcher.dispatch()`][].
|
||||
|
||||
### `Agent.dispatch(options, handler)`
|
||||
### `agent.close([callback])`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
### `Agent.pipeline(options, handler)`
|
||||
* `callback` {Function} (optional) Invoked once every per-origin dispatcher has
|
||||
closed. When omitted, a {Promise} is returned.
|
||||
* Returns: {Promise|void}
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
|
||||
Gracefully closes the agent and every dispatcher it owns, waiting for in-flight
|
||||
requests to complete. Implements [`Dispatcher.close()`][].
|
||||
|
||||
### `Agent.request(options[, callback])`
|
||||
### `agent.destroy([error[, callback]])`
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
### `Agent.stream(options, factory[, callback])`
|
||||
* `error` {Error} (optional) The error to abort pending requests with.
|
||||
* `callback` {Function} (optional) Invoked once every per-origin dispatcher has
|
||||
been destroyed. When omitted, a {Promise} is returned.
|
||||
* Returns: {Promise|void}
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
|
||||
Forcefully destroys the agent and every dispatcher it owns, aborting all pending
|
||||
requests. Implements [`Dispatcher.destroy()`][].
|
||||
|
||||
### `Agent.upgrade(options[, callback])`
|
||||
### `agent.connect(options[, callback])`
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
### `Agent.stats()`
|
||||
See [`Dispatcher.connect()`][]. The request is routed to the dispatcher for
|
||||
`options.origin`.
|
||||
|
||||
Returns an object of stats by origin in the format of `Record<string, TClientStats | TPoolStats>`
|
||||
### `agent.pipeline(options, handler)`
|
||||
|
||||
See [`PoolStats`](/docs/docs/api/PoolStats.md) and [`ClientStats`](/docs/docs/api/ClientStats.md).
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.pipeline()`][]. The request is routed to the dispatcher for
|
||||
`options.origin`.
|
||||
|
||||
### `agent.request(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.request()`][]. The request is routed to the dispatcher for
|
||||
`options.origin`.
|
||||
|
||||
```mjs
|
||||
import { Agent } from 'undici'
|
||||
|
||||
const agent = new Agent()
|
||||
const { statusCode, body } = await agent.request({
|
||||
origin: 'http://localhost:3000',
|
||||
path: '/',
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
console.log('response received', statusCode)
|
||||
console.log('data', await body.text())
|
||||
```
|
||||
|
||||
### `agent.stream(options, factory[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.stream()`][]. The request is routed to the dispatcher for
|
||||
`options.origin`.
|
||||
|
||||
### `agent.upgrade(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v3.2.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.upgrade()`][]. The request is routed to the dispatcher for
|
||||
`options.origin`.
|
||||
|
||||
### Event: `'connect'`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.6.0
|
||||
-->
|
||||
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
|
||||
`Agent` followed by the per-origin dispatcher chain.
|
||||
|
||||
Emitted when a socket has been created for an origin and is ready to receive
|
||||
requests. Forwarded from the underlying dispatcher's [`'connect'`][] event.
|
||||
|
||||
### Event: `'disconnect'`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.6.0
|
||||
-->
|
||||
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
|
||||
`Agent`.
|
||||
* `error` {Error}
|
||||
|
||||
Emitted when a socket for an origin has disconnected. Forwarded from the
|
||||
underlying dispatcher's [`'disconnect'`][] event.
|
||||
|
||||
### Event: `'connectionError'`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.6.0
|
||||
-->
|
||||
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
|
||||
`Agent`.
|
||||
* `error` {Error}
|
||||
|
||||
Emitted when a connection attempt for an origin fails. Forwarded from the
|
||||
underlying dispatcher's [`'connectionError'`][] event.
|
||||
|
||||
### Event: `'drain'`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.6.0
|
||||
-->
|
||||
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>} The dispatchers involved, starting with this
|
||||
`Agent`.
|
||||
|
||||
Emitted when an origin's dispatcher is no longer busy and can accept more
|
||||
requests. Forwarded from the underlying dispatcher's [`'drain'`][] event.
|
||||
|
||||
[`'connect'`]: Dispatcher.md#event-connect
|
||||
[`'connectionError'`]: Dispatcher.md#event-connectionerror
|
||||
[`'disconnect'`]: Dispatcher.md#event-disconnect
|
||||
[`'drain'`]: Dispatcher.md#event-drain
|
||||
[`Client`]: Client.md#class-client
|
||||
[`ClientStats`]: ClientStats.md
|
||||
[`Dispatcher.close()`]: Dispatcher.md#dispatcherclosecallback-promise
|
||||
[`Dispatcher.connect()`]: Dispatcher.md#dispatcherconnectoptions-callback
|
||||
[`Dispatcher.destroy()`]: Dispatcher.md#dispatcherdestroyerror-callback-promise
|
||||
[`Dispatcher.dispatch()`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`Dispatcher.pipeline()`]: Dispatcher.md#dispatcherpipelineoptions-handler
|
||||
[`Dispatcher.request()`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`Dispatcher.stream()`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
|
||||
[`Dispatcher.upgrade()`]: Dispatcher.md#dispatcherupgradeoptions-callback
|
||||
[`Pool`]: Pool.md#class-pool
|
||||
[`PoolStats`]: PoolStats.md
|
||||
[`agent.close()`]: #agentclosecallback
|
||||
[`agent.destroy()`]: #agentdestroyerror-callback
|
||||
[`fetch`]: Fetch.md
|
||||
[`request`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`stream`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
|
||||
+246
-48
@@ -1,99 +1,297 @@
|
||||
# Class: BalancedPool
|
||||
# BalancedPool
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v4.8.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/dispatcher/balanced-pool.js -->
|
||||
|
||||
A pool of [Pool](/docs/docs/api/Pool.md) instances connected to multiple upstreams.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Requests are not guaranteed to be dispatched in order of invocation.
|
||||
A `BalancedPool` distributes requests across a set of {Pool} instances, each
|
||||
connected to a different upstream. It uses a weighted round-robin algorithm:
|
||||
every upstream starts with the same weight, and the weight is adjusted up on
|
||||
successful connections and down on connection errors, so that healthy upstreams
|
||||
receive a larger share of traffic.
|
||||
|
||||
## `new BalancedPool(upstreams [, options])`
|
||||
Requests are not guaranteed to be dispatched in the order they are issued.
|
||||
|
||||
Arguments:
|
||||
```mjs
|
||||
import { BalancedPool } from 'undici'
|
||||
|
||||
* **upstreams** `URL | string | string[]` - It should only include the **protocol, hostname, and port**.
|
||||
* **options** `BalancedPoolOptions` (optional)
|
||||
const pool = new BalancedPool([
|
||||
'http://localhost:3000',
|
||||
'http://localhost:3001',
|
||||
'http://localhost:3002',
|
||||
])
|
||||
|
||||
### Parameter: `BalancedPoolOptions`
|
||||
const { statusCode, body } = await pool.request({
|
||||
path: '/',
|
||||
method: 'GET',
|
||||
})
|
||||
console.log('response received', statusCode)
|
||||
for await (const chunk of body) {
|
||||
console.log('data', chunk.toString())
|
||||
}
|
||||
```
|
||||
|
||||
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)
|
||||
## Class: `BalancedPool`
|
||||
|
||||
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Pool(origin, opts)`
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Introduced the `BalancedPool` class.
|
||||
-->
|
||||
|
||||
The `PoolOptions` are passed to each of the `Pool` instances being created.
|
||||
## Instance Properties
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
### `BalancedPool.upstreams`
|
||||
A pool of {Pool} instances connected to multiple upstreams.
|
||||
|
||||
Returns an array of upstreams that were previously added.
|
||||
### `new BalancedPool(upstreams[, options])`
|
||||
|
||||
### `BalancedPool.closed`
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.15.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/1237
|
||||
description: Added the `factory` option.
|
||||
-->
|
||||
|
||||
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
|
||||
* `upstreams` {URL|string|URL[]|string[]} One or more upstream origins. Each
|
||||
value should only include the **protocol, hostname, and port**. **Default:**
|
||||
`[]`.
|
||||
* `options` {BalancedPoolOptions} (optional)
|
||||
|
||||
### `BalancedPool.destroyed`
|
||||
The options passed to the constructor are forwarded to every {Pool} instance
|
||||
that is created for an upstream.
|
||||
|
||||
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
|
||||
#### Parameter: `BalancedPoolOptions`
|
||||
|
||||
### `BalancedPool.stats`
|
||||
Extends: {PoolOptions}
|
||||
|
||||
Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool.
|
||||
In addition to all {PoolOptions}, the following options are supported:
|
||||
|
||||
## Instance Methods
|
||||
* `factory` {Function} A function used to create the {Pool} instance for each
|
||||
upstream. **Default:** `(origin, opts) => new Pool(origin, opts)`.
|
||||
* `origin` {URL|string} The upstream origin.
|
||||
* `opts` {Object} The options object derived from the constructor options.
|
||||
* Returns: {Dispatcher}
|
||||
* `maxWeightPerServer` {number} The maximum weight any single upstream can
|
||||
reach. Successful connections increase an upstream's weight up to this value.
|
||||
**Default:** `100`.
|
||||
* `errorPenalty` {number} The amount by which an upstream's weight is decreased
|
||||
on a connection error and increased on a successful connection. **Default:**
|
||||
`15`.
|
||||
|
||||
### `BalancedPool.addUpstream(upstream)`
|
||||
### `balancedPool.addUpstream(upstream)`
|
||||
|
||||
Add an upstream.
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
Arguments:
|
||||
* `upstream` {URL|string} The upstream origin to add. It should only include the
|
||||
**protocol, hostname, and port**.
|
||||
* Returns: {BalancedPool} The `BalancedPool` instance, for chaining.
|
||||
|
||||
* **upstream** `string` - It should only include the **protocol, hostname, and port**.
|
||||
Adds an upstream. If an open, non-destroyed upstream with the same origin is
|
||||
already present, the call is a no-op.
|
||||
|
||||
### `BalancedPool.removeUpstream(upstream)`
|
||||
### `balancedPool.removeUpstream(upstream)`
|
||||
|
||||
Removes an upstream that was previously added.
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
### `BalancedPool.close([callback])`
|
||||
* `upstream` {URL|string} The upstream origin to remove. It should only include
|
||||
the **protocol, hostname, and port**.
|
||||
* Returns: {BalancedPool} The `BalancedPool` instance, for chaining.
|
||||
|
||||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
|
||||
Removes an upstream that was previously added. If no matching upstream is found,
|
||||
the call is a no-op.
|
||||
|
||||
### `BalancedPool.destroy([error, callback])`
|
||||
### `balancedPool.getUpstream(upstream)`
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
### `BalancedPool.connect(options[, callback])`
|
||||
* `upstream` {URL|string} The upstream origin to look up. It should only include
|
||||
the **protocol, hostname, and port**.
|
||||
* Returns: {Pool|undefined} The {Pool} instance managing the given upstream, or
|
||||
`undefined` if it is not present.
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
|
||||
Returns the {Pool} instance for the given upstream origin, if it is open and not
|
||||
destroyed.
|
||||
|
||||
### `BalancedPool.dispatch(options, handlers)`
|
||||
### `balancedPool.upstreams`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
### `BalancedPool.pipeline(options, handler)`
|
||||
* Type: {string[]} The origins of the upstreams that are currently open and not
|
||||
destroyed.
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
|
||||
### `balancedPool.closed`
|
||||
|
||||
### `BalancedPool.request(options[, callback])`
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Introduced the `BalancedPool` class.
|
||||
-->
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
* Type: {boolean} `true` after `balancedPool.close()` has been called.
|
||||
|
||||
### `BalancedPool.stream(options, factory[, callback])`
|
||||
Implements [`client.closed`][].
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
|
||||
### `balancedPool.destroyed`
|
||||
|
||||
### `BalancedPool.upgrade(options[, callback])`
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Introduced the `BalancedPool` class.
|
||||
-->
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
||||
* Type: {boolean} `true` after `balancedPool.destroy()` has been called, or after
|
||||
`balancedPool.close()` has been called and the shutdown has completed.
|
||||
|
||||
## Instance Events
|
||||
Implements [`client.destroyed`][].
|
||||
|
||||
### `balancedPool.stats`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
* Type: {PoolStats}
|
||||
|
||||
Returns a {PoolStats} instance aggregating the statistics of the underlying
|
||||
pools.
|
||||
|
||||
### `balancedPool.close([callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.close([callback])`][].
|
||||
|
||||
### `balancedPool.destroy([error[, callback]])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`][].
|
||||
|
||||
### `balancedPool.connect(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`][].
|
||||
|
||||
### `balancedPool.dispatch(options, handlers)`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Introduced the `BalancedPool` class.
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handler)`][].
|
||||
|
||||
### `balancedPool.pipeline(options, handler)`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`][].
|
||||
|
||||
### `balancedPool.request(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.request(options[, callback])`][].
|
||||
|
||||
### `balancedPool.stream(options, factory[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`][].
|
||||
|
||||
### `balancedPool.upgrade(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`][].
|
||||
|
||||
### Event: `'connect'`
|
||||
|
||||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Introduced the `BalancedPool` class.
|
||||
- version: v5.8.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/1069
|
||||
description: Adopted weighted round-robin selection across upstreams.
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'connect'`][].
|
||||
|
||||
### Event: `'disconnect'`
|
||||
|
||||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Introduced the `BalancedPool` class.
|
||||
- version: v5.8.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/1069
|
||||
description: Adopted weighted round-robin selection across upstreams.
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'disconnect'`][].
|
||||
|
||||
### Event: `'drain'`
|
||||
|
||||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
|
||||
<!-- YAML
|
||||
added: v4.8.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Introduced the `BalancedPool` class.
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'drain'`][].
|
||||
|
||||
[Dispatcher Event: `'connect'`]: Dispatcher.md#event-connect
|
||||
[Dispatcher Event: `'disconnect'`]: Dispatcher.md#event-disconnect
|
||||
[Dispatcher Event: `'drain'`]: Dispatcher.md#event-drain
|
||||
[`Dispatcher.close([callback])`]: Dispatcher.md#dispatcherclosecallback-promise
|
||||
[`Dispatcher.connect(options[, callback])`]: Dispatcher.md#dispatcherconnectoptions-callback
|
||||
[`Dispatcher.destroy([error, callback])`]: Dispatcher.md#dispatcherdestroyerror-callback-promise
|
||||
[`Dispatcher.dispatch(options, handler)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`Dispatcher.pipeline(options, handler)`]: Dispatcher.md#dispatcherpipelineoptions-handler
|
||||
[`Dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`Dispatcher.stream(options, factory[, callback])`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
|
||||
[`Dispatcher.upgrade(options[, callback])`]: Dispatcher.md#dispatcherupgradeoptions-callback
|
||||
[`client.closed`]: Client.md#clientclosed
|
||||
[`client.destroyed`]: Client.md#clientdestroyed
|
||||
+172
-14
@@ -1,30 +1,188 @@
|
||||
# CacheStorage
|
||||
|
||||
Undici exposes a W3C spec-compliant implementation of [CacheStorage](https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage) and [Cache](https://developer.mozilla.org/en-US/docs/Web/API/Cache).
|
||||
<!--introduced_in=v5.22.1-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/web/cache/cachestorage.js -->
|
||||
|
||||
## Opening a Cache
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Undici exports a top-level CacheStorage instance. You can open a new Cache, or duplicate a Cache with an existing name, by using `CacheStorage.prototype.open`. If you open a Cache with the same name as an already-existing Cache, its list of cached Responses will be shared between both instances.
|
||||
undici provides a spec-compliant implementation of the [W3C Service Worker
|
||||
`CacheStorage`][] and [`Cache`][] interfaces. A `CacheStorage` is a named
|
||||
collection of [`Cache`][] objects, each of which stores `Request`/`Response`
|
||||
pairs.
|
||||
|
||||
The class itself is not exported; instead, undici exposes a single shared
|
||||
`CacheStorage` instance named `caches`. Import it from `'undici'`:
|
||||
|
||||
```mjs
|
||||
import { caches } from 'undici'
|
||||
|
||||
const cache_1 = await caches.open('v1')
|
||||
const cache_2 = await caches.open('v1')
|
||||
|
||||
// Although .open() creates a new instance,
|
||||
assert(cache_1 !== cache_2)
|
||||
// The same Response is matched in both.
|
||||
assert.deepStrictEqual(await cache_1.match('/req'), await cache_2.match('/req'))
|
||||
const cache = await caches.open('v1')
|
||||
```
|
||||
|
||||
## Deleting a Cache
|
||||
```cjs
|
||||
const { caches } = require('undici')
|
||||
|
||||
If a Cache is deleted, the cached Responses/Requests can still be used.
|
||||
caches.open('v1').then((cache) => {
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
## `caches`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.22.1
|
||||
-->
|
||||
|
||||
* Type: {CacheStorage}
|
||||
|
||||
The shared [`CacheStorage`](#class-cachestorage) instance exported by undici.
|
||||
All caches opened through `caches` live for the lifetime of the process.
|
||||
|
||||
```mjs
|
||||
const response = await cache_1.match('/req')
|
||||
import { caches } from 'undici'
|
||||
|
||||
await caches.open('v1')
|
||||
console.log(await caches.keys()) // ['v1']
|
||||
```
|
||||
|
||||
## Class: `CacheStorage`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.22.1
|
||||
-->
|
||||
|
||||
Represents the storage for [`Cache`][] objects. Each `CacheStorage` keeps a
|
||||
mapping of cache names to their backing list of cached responses.
|
||||
|
||||
The constructor is not public: attempting to instantiate `CacheStorage`
|
||||
directly throws a `TypeError`. Use the exported [`caches`](#caches) instance
|
||||
instead.
|
||||
|
||||
### `caches.match(request[, options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.22.1
|
||||
-->
|
||||
|
||||
* `request` {Request|string} The request to match against. A string is treated
|
||||
as a URL.
|
||||
* `options` {Object} (optional)
|
||||
* `ignoreSearch` {boolean} When `true`, ignores the query string of the URL
|
||||
when matching. **Default:** `false`.
|
||||
* `ignoreMethod` {boolean} When `true`, prevents matching operations from
|
||||
validating the `Request` `http` method. **Default:** `false`.
|
||||
* `ignoreVary` {boolean} When `true`, ignores the `Vary` header when matching.
|
||||
**Default:** `false`.
|
||||
* `cacheName` {string} When set, restricts the search to the cache with this
|
||||
name. **Default:** `undefined`.
|
||||
* Returns: {Promise} Fulfills with the first matching {Response}, or
|
||||
`undefined` if no response matched.
|
||||
|
||||
Searches the caches for a response matching `request`. When `options.cacheName`
|
||||
is provided, only that cache is searched; otherwise every cache is searched in
|
||||
insertion order and the first match is returned.
|
||||
|
||||
```mjs
|
||||
import { caches } from 'undici'
|
||||
|
||||
const response = await caches.match('https://example.com/data', {
|
||||
cacheName: 'v1'
|
||||
})
|
||||
```
|
||||
|
||||
### `caches.has(cacheName)`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.22.1
|
||||
-->
|
||||
|
||||
* `cacheName` {string} The name of the cache to look up.
|
||||
* Returns: {Promise} Fulfills with `true` if a cache named `cacheName` exists,
|
||||
otherwise `false`.
|
||||
|
||||
Checks whether a cache with the given name exists in the storage.
|
||||
|
||||
```mjs
|
||||
import { caches } from 'undici'
|
||||
|
||||
await caches.open('v1')
|
||||
console.log(await caches.has('v1')) // true
|
||||
console.log(await caches.has('v2')) // false
|
||||
```
|
||||
|
||||
### `caches.open(cacheName)`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.22.1
|
||||
-->
|
||||
|
||||
* `cacheName` {string} The name of the cache to open.
|
||||
* Returns: {Promise} Fulfills with the {Cache} named `cacheName`.
|
||||
|
||||
Opens the cache named `cacheName`, creating it if it does not already exist.
|
||||
|
||||
Each call returns a new [`Cache`][] object, but caches opened with the same name
|
||||
share their underlying list of cached responses. As a result, a response stored
|
||||
through one instance is visible through another instance with the same name.
|
||||
|
||||
```mjs
|
||||
import { caches } from 'undici'
|
||||
import assert from 'node:assert'
|
||||
|
||||
const cache1 = await caches.open('v1')
|
||||
const cache2 = await caches.open('v1')
|
||||
|
||||
// open() returns distinct objects,
|
||||
assert(cache1 !== cache2)
|
||||
// but they share the same cached responses.
|
||||
assert.deepStrictEqual(await cache1.match('/req'), await cache2.match('/req'))
|
||||
```
|
||||
|
||||
### `caches.delete(cacheName)`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.22.1
|
||||
-->
|
||||
|
||||
* `cacheName` {string} The name of the cache to delete.
|
||||
* Returns: {Promise} Fulfills with `true` if the cache existed and was deleted,
|
||||
otherwise `false`.
|
||||
|
||||
Removes the cache named `cacheName` from the storage. Any [`Cache`][] objects
|
||||
already obtained for that name, along with the `Request`/`Response` pairs they
|
||||
returned, remain usable after deletion.
|
||||
|
||||
```mjs
|
||||
import { caches } from 'undici'
|
||||
|
||||
const cache = await caches.open('v1')
|
||||
const response = await cache.match('/req')
|
||||
|
||||
await caches.delete('v1')
|
||||
|
||||
await response.text() // the Response's body
|
||||
// The already-retrieved response is still usable.
|
||||
await response.text()
|
||||
```
|
||||
|
||||
### `caches.keys()`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.22.1
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Fulfills with a {string[]} of cache names in insertion
|
||||
order.
|
||||
|
||||
Returns the names of all caches currently held by the storage.
|
||||
|
||||
```mjs
|
||||
import { caches } from 'undici'
|
||||
|
||||
await caches.open('v1')
|
||||
await caches.open('v2')
|
||||
console.log(await caches.keys()) // ['v1', 'v2']
|
||||
```
|
||||
|
||||
[W3C Service Worker `CacheStorage`]: https://w3c.github.io/ServiceWorker/#cachestorage-interface
|
||||
[`Cache`]: https://w3c.github.io/ServiceWorker/#cache-interface
|
||||
+334
-138
@@ -1,164 +1,360 @@
|
||||
# Cache Store
|
||||
|
||||
A Cache Store is responsible for storing and retrieving cached responses.
|
||||
It is also responsible for deciding which specific response to use based off of
|
||||
a response's `Vary` header (if present). It is expected to be compliant with
|
||||
[RFC-9111](https://www.rfc-editor.org/rfc/rfc9111.html).
|
||||
<!--introduced_in=v7.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/cache/memory-cache-store.js -->
|
||||
|
||||
## Pre-built Cache Stores
|
||||
> Stability: 2 - Stable
|
||||
|
||||
### `MemoryCacheStore`
|
||||
A cache store is the storage backend used by the cache interceptor to persist
|
||||
and retrieve cached responses. A store decides which stored response to serve
|
||||
for a request by comparing the request against the response's `Vary` header, and
|
||||
is expected to be compliant with [RFC 9111][].
|
||||
|
||||
The `MemoryCacheStore` stores the responses in-memory.
|
||||
undici ships two stores. `MemoryCacheStore` keeps responses in memory, while
|
||||
`SqliteCacheStore` persists them to a SQLite database. Both are available under
|
||||
the `cacheStores` export:
|
||||
|
||||
**Options**
|
||||
```mjs
|
||||
import { cacheStores } from 'undici'
|
||||
|
||||
- `maxSize` - The maximum total size in bytes of all stored responses. Default `104857600` (100MB).
|
||||
- `maxCount` - The maximum amount of responses to store. Default `1024`.
|
||||
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `5242880` (5MB).
|
||||
|
||||
### Getters
|
||||
|
||||
#### `MemoryCacheStore.size`
|
||||
|
||||
Returns the current total size in bytes of all stored responses.
|
||||
|
||||
### Methods
|
||||
|
||||
#### `MemoryCacheStore.isFull()`
|
||||
|
||||
Returns a boolean indicating whether the cache has reached its maximum size or count.
|
||||
|
||||
### Events
|
||||
|
||||
#### `'maxSizeExceeded'`
|
||||
|
||||
Emitted when the cache exceeds its maximum size or count limits. The event payload contains `size`, `maxSize`, `count`, and `maxCount` properties.
|
||||
|
||||
|
||||
### `SqliteCacheStore`
|
||||
|
||||
The `SqliteCacheStore` stores the responses in a SQLite database.
|
||||
Under the hood, it uses Node.js' [`node:sqlite`](https://nodejs.org/api/sqlite.html) api.
|
||||
The `SqliteCacheStore` is only exposed if the `node:sqlite` api is present.
|
||||
|
||||
**Options**
|
||||
|
||||
- `location` - The location of the SQLite database to use. Default `:memory:`.
|
||||
- `maxCount` - The maximum number of entries to store in the database. Default `Infinity`.
|
||||
- `maxEntrySize` - The maximum size in bytes that a response's body can be. If a response's body is greater than or equal to this, the response will not be cached. Default `Infinity`.
|
||||
|
||||
## Defining a Custom Cache Store
|
||||
|
||||
The store must implement the following functions:
|
||||
|
||||
### Getter: `isFull`
|
||||
|
||||
Optional. This tells the cache interceptor if the store is full or not. If this is true,
|
||||
the cache interceptor will not attempt to cache the response.
|
||||
|
||||
### Function: `get`
|
||||
|
||||
Parameters:
|
||||
|
||||
* **req** `Dispatcher.RequestOptions` - Incoming request
|
||||
|
||||
Returns: `GetResult | Promise<GetResult | undefined> | undefined` - If the request is cached, the cached response is returned. If the request's method is anything other than HEAD, the response is also returned.
|
||||
If the request isn't cached, `undefined` is returned.
|
||||
|
||||
The `get` method may return a `Promise` for async cache stores (e.g. Redis-backed or remote stores). The cache interceptor handles both synchronous and asynchronous return values, including in revalidation paths (304 Not Modified handling and stale-while-revalidate background revalidation).
|
||||
|
||||
Response properties:
|
||||
|
||||
* **response** `CacheValue` - The cached response data.
|
||||
* **body** `Readable | Iterable<Buffer> | undefined` - The response's body. This can be an array of `Buffer` chunks (with a `.values()` method) or a `Readable` stream. Both formats are supported in all code paths, including 304 revalidation.
|
||||
|
||||
### Function: `createWriteStream`
|
||||
|
||||
Parameters:
|
||||
|
||||
* **req** `Dispatcher.RequestOptions` - Incoming request
|
||||
* **value** `CacheValue` - Response to store
|
||||
|
||||
Returns: `Writable | undefined` - If the store is full, return `undefined`. Otherwise, return a writable so that the cache interceptor can stream the body and trailers to the store.
|
||||
|
||||
## `CacheValue`
|
||||
|
||||
This is an interface containing the majority of a response's data (minus the body).
|
||||
|
||||
### Property `statusCode`
|
||||
|
||||
`number` - The response's HTTP status code.
|
||||
|
||||
### Property `statusMessage`
|
||||
|
||||
`string` - The response's HTTP status message.
|
||||
|
||||
### Property `rawHeaders`
|
||||
|
||||
`Buffer[]` - The response's headers.
|
||||
|
||||
### Property `vary`
|
||||
|
||||
`Record<string, string | string[] | null> | undefined` - The headers defined by the response's `Vary` header
|
||||
and their respective values for later comparison. Values are `null` when the
|
||||
header specified in `Vary` was not present in the original request. These `null`
|
||||
values are automatically filtered out during revalidation so they are not sent
|
||||
as request headers.
|
||||
|
||||
For example, for a response like
|
||||
const { MemoryCacheStore, SqliteCacheStore } = cacheStores
|
||||
```
|
||||
Vary: content-encoding, accepts
|
||||
|
||||
`SqliteCacheStore` requires the [`node:sqlite`][] API. The class is always
|
||||
exported, but constructing one throws if `node:sqlite` is not available in the
|
||||
running Node.js version.
|
||||
|
||||
A cache store implements three methods — `get(key)`, `createWriteStream(key,
|
||||
value)`, and `delete(key)` — that operate on a [`CacheKey`](#cachekey) and a
|
||||
[`CacheValue`](#cachevalue). Any object implementing this contract can be passed
|
||||
to the cache interceptor, so custom stores (for example, backed by Redis or a
|
||||
remote service) are supported as well.
|
||||
|
||||
## Class: `MemoryCacheStore`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* Extends: {EventEmitter}
|
||||
|
||||
Stores cached responses in memory. The store enforces upper bounds on the total
|
||||
number of responses, the total size of all responses, and the size of any single
|
||||
response. When a limit is exceeded, the store evicts approximately half of its
|
||||
entries and emits a [`'maxSizeExceeded'`](#event-maxsizeexceeded) event.
|
||||
|
||||
```mjs
|
||||
import { interceptors, cacheStores, Agent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const store = new cacheStores.MemoryCacheStore({ maxSize: 50 * 1024 * 1024 })
|
||||
|
||||
setGlobalDispatcher(
|
||||
new Agent().compose(interceptors.cache({ store }))
|
||||
)
|
||||
```
|
||||
|
||||
### `new MemoryCacheStore([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `options` {Object} (optional)
|
||||
* `maxCount` {number} The maximum number of responses to store. **Default:**
|
||||
`1024`.
|
||||
* `maxSize` {number} The maximum total size, in bytes, of all stored
|
||||
responses. **Default:** `104857600` (100 MiB).
|
||||
* `maxEntrySize` {number} The maximum size, in bytes, of a single response
|
||||
body. Responses whose body exceeds this value are not cached. **Default:**
|
||||
`5242880` (5 MiB).
|
||||
* `errorCallback` {Function} A callback invoked with any error raised by the
|
||||
store. (optional)
|
||||
* `err` {Error}
|
||||
|
||||
Each of `maxCount`, `maxSize`, and `maxEntrySize` must be a non-negative
|
||||
integer; a `TypeError` is thrown otherwise.
|
||||
|
||||
### `memoryCacheStore.size`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
The current total size, in bytes, of all stored response bodies.
|
||||
|
||||
### `memoryCacheStore.isFull()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* Returns: {boolean} `true` when the store has reached its `maxSize` or
|
||||
`maxCount` limit, `false` otherwise.
|
||||
|
||||
### `memoryCacheStore.get(key)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `key` {CacheKey} The request to look up. See [`CacheKey`](#cachekey).
|
||||
* Returns: {GetResult|undefined} The matching cached response, or `undefined`
|
||||
when there is no fresh entry for `key`. See [`GetResult`](#getresult).
|
||||
|
||||
Looks up a cached response for `key`. A stored entry matches only when its
|
||||
`method` equals `key.method`, its `deleteAt` time is in the future, and every
|
||||
header listed in its `vary` map matches the corresponding header in
|
||||
`key.headers`.
|
||||
|
||||
### `memoryCacheStore.createWriteStream(key, value)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `key` {CacheKey} The request the response is being cached for. See
|
||||
[`CacheKey`](#cachekey).
|
||||
* `value` {CacheValue} The response metadata to store. See
|
||||
[`CacheValue`](#cachevalue).
|
||||
* Returns: {Writable|undefined} A writable stream that the response body is
|
||||
written to, or `undefined` when the response cannot be cached.
|
||||
|
||||
Returns a {Writable} stream used to write the response body into the store. When
|
||||
the stream finishes, the entry is committed; if its accumulated size exceeds
|
||||
`maxEntrySize`, the stream is destroyed and nothing is stored. Writing a new
|
||||
entry whose `key` matches an existing one replaces that entry.
|
||||
|
||||
### `memoryCacheStore.delete(key)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `key` {CacheKey} The request whose cached responses should be removed. See
|
||||
[`CacheKey`](#cachekey).
|
||||
* Returns: {undefined}
|
||||
|
||||
Removes every cached response stored for `key.origin` and `key.path`. Throws a
|
||||
`TypeError` if `key` is not an object.
|
||||
|
||||
### Event: `'maxSizeExceeded'`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.10.0
|
||||
-->
|
||||
|
||||
* `data` {Object}
|
||||
* `size` {number} The current total size, in bytes, of all stored responses.
|
||||
* `maxSize` {number} The configured `maxSize` limit.
|
||||
* `count` {number} The current number of stored responses.
|
||||
* `maxCount` {number} The configured `maxCount` limit.
|
||||
|
||||
Emitted when the store exceeds its `maxSize` or `maxCount` limit, immediately
|
||||
before entries are evicted. The event fires only once per overflow; it is not
|
||||
emitted again until the store drops back below both limits.
|
||||
|
||||
## Class: `SqliteCacheStore`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
Stores cached responses in a SQLite database using the [`node:sqlite`][] API.
|
||||
The constructor throws when [`node:sqlite`][] is not available.
|
||||
|
||||
```mjs
|
||||
import { interceptors, cacheStores, Agent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const store = new cacheStores.SqliteCacheStore({ location: './cache.db' })
|
||||
|
||||
setGlobalDispatcher(
|
||||
new Agent().compose(interceptors.cache({ store }))
|
||||
)
|
||||
```
|
||||
|
||||
### `new SqliteCacheStore([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `options` {Object} (optional)
|
||||
* `location` {string} The location of the SQLite database. Use `':memory:'`
|
||||
for an in-memory database. **Default:** `':memory:'`.
|
||||
* `maxCount` {number} The maximum number of responses to store. **Default:**
|
||||
`Infinity`.
|
||||
* `maxEntrySize` {number} The maximum size, in bytes, of a single response
|
||||
body. Responses whose body exceeds this value are not cached. Must not
|
||||
exceed `2000000000` (2 GB). **Default:** `2000000000` (2 GB).
|
||||
|
||||
`maxCount` and `maxEntrySize` must be non-negative integers; a `TypeError` is
|
||||
thrown otherwise, or if `maxEntrySize` is greater than 2 GB.
|
||||
|
||||
### `sqliteCacheStore.close()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* Returns: {undefined}
|
||||
|
||||
Closes the connection to the underlying SQLite database.
|
||||
|
||||
### `sqliteCacheStore.size`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
The number of responses currently stored in the database.
|
||||
|
||||
### `sqliteCacheStore.get(key)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `key` {CacheKey} The request to look up. See [`CacheKey`](#cachekey).
|
||||
* Returns: {GetResult|undefined} The matching cached response, or `undefined`
|
||||
when there is no fresh entry for `key`. The returned `body` is a {Buffer}.
|
||||
See [`GetResult`](#getresult).
|
||||
|
||||
Looks up a cached response for `key`, comparing the request method and every
|
||||
header named in the stored `vary` map.
|
||||
|
||||
### `sqliteCacheStore.set(key, value)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `key` {CacheKey} The request the response is being cached for. See
|
||||
[`CacheKey`](#cachekey).
|
||||
* `value` {CacheValue} The response to store, with its `body` set to a {Buffer},
|
||||
an {Array} of {Buffer}, or `null`. See [`CacheValue`](#cachevalue).
|
||||
* Returns: {undefined}
|
||||
|
||||
Writes a response into the database directly, without going through a stream. If
|
||||
the body exceeds `maxEntrySize`, nothing is stored. When an entry already exists
|
||||
for `key` it is overwritten; otherwise a new row is inserted and old entries are
|
||||
pruned to honour `maxCount`. This method is used internally by
|
||||
[`sqliteCacheStore.createWriteStream()`](#sqlitecachestorecreatewritestreamkey-value).
|
||||
|
||||
### `sqliteCacheStore.createWriteStream(key, value)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `key` {CacheKey} The request the response is being cached for. See
|
||||
[`CacheKey`](#cachekey).
|
||||
* `value` {CacheValue} The response metadata to store. See
|
||||
[`CacheValue`](#cachevalue).
|
||||
* Returns: {Writable|undefined} A writable stream that the response body is
|
||||
written to, or `undefined` when the response cannot be cached.
|
||||
|
||||
Returns a {Writable} stream used to write the response body into the store. When
|
||||
the stream finishes, the buffered body is committed via
|
||||
[`sqliteCacheStore.set()`](#sqlitecachestoresetkey-value). If the body exceeds
|
||||
`maxEntrySize`, the stream is destroyed and nothing is stored.
|
||||
|
||||
### `sqliteCacheStore.delete(key)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `key` {CacheKey} The request whose cached responses should be removed. See
|
||||
[`CacheKey`](#cachekey).
|
||||
* Returns: {undefined}
|
||||
|
||||
Removes every cached response stored for the URL derived from `key.origin` and
|
||||
`key.path`. Throws a `TypeError` if `key` is not an object.
|
||||
|
||||
## Implementing a custom cache store
|
||||
|
||||
A cache store is any object that implements the `CacheStore` interface:
|
||||
|
||||
* `get(key)` — Look up a response. Returns a [`GetResult`](#getresult),
|
||||
`undefined`, or a {Promise} resolving to either.
|
||||
* `createWriteStream(key, value)` — Return a {Writable} to receive the response
|
||||
body, or `undefined` if the response cannot be stored.
|
||||
* `delete(key)` — Remove all responses for `key`. May return a {Promise}.
|
||||
|
||||
Returning a {Promise} from `get` or `delete` lets a store be backed by an
|
||||
asynchronous resource; the cache interceptor awaits these values, including on
|
||||
the revalidation and stale-while-revalidate paths.
|
||||
|
||||
### `CacheKey`
|
||||
|
||||
The request a response is being looked up or stored for.
|
||||
|
||||
* `origin` {string} The request origin.
|
||||
* `method` {string} The request method.
|
||||
* `path` {string} The request path.
|
||||
* `headers` {Record<string, string|string[]>} The request headers, used to
|
||||
satisfy `Vary`. (optional)
|
||||
|
||||
### `CacheValue`
|
||||
|
||||
The metadata of a cached response, excluding its body.
|
||||
|
||||
* `statusCode` {number} The response's HTTP status code.
|
||||
* `statusMessage` {string} The response's HTTP status message.
|
||||
* `headers` {Record<string, string|string[]>} The response headers.
|
||||
* `vary` {Record<string, string|string[]|null>} The header names listed in the
|
||||
response's `Vary` header mapped to the values they had in the original
|
||||
request. A value is `null` when that header was absent from the original
|
||||
request. (optional)
|
||||
* `etag` {string} The response's entity tag. (optional)
|
||||
* `cacheControlDirectives` {Object} The parsed `Cache-Control` directives of the
|
||||
response. (optional)
|
||||
* `cachedAt` {number} The time, in milliseconds, at which the response was
|
||||
cached.
|
||||
* `staleAt` {number} The time, in milliseconds, at which the response becomes
|
||||
stale.
|
||||
* `deleteAt` {number} The time, in milliseconds, at which the response must be
|
||||
evicted. A store must not return a response once this time has passed.
|
||||
|
||||
The `vary` map drives response selection. For a response such as:
|
||||
|
||||
```http
|
||||
Vary: content-encoding, accept
|
||||
content-encoding: utf8
|
||||
accepts: application/json
|
||||
accept: application/json
|
||||
```
|
||||
|
||||
This would be
|
||||
the recorded map is:
|
||||
|
||||
```js
|
||||
{
|
||||
'content-encoding': 'utf8',
|
||||
accepts: 'application/json'
|
||||
accept: 'application/json'
|
||||
}
|
||||
```
|
||||
|
||||
If the original request did not include the `accepts` header:
|
||||
If the original request did not include the `accept` header, its value is
|
||||
recorded as `null`:
|
||||
|
||||
```js
|
||||
{
|
||||
'content-encoding': 'utf8',
|
||||
accepts: null
|
||||
accept: null
|
||||
}
|
||||
```
|
||||
|
||||
### Property `cachedAt`
|
||||
### `GetResult`
|
||||
|
||||
`number` - Time in millis that this value was cached.
|
||||
The value returned by `get`. It contains every field of
|
||||
[`CacheValue`](#cachevalue) plus the response body.
|
||||
|
||||
### Property `staleAt`
|
||||
* `body` {Readable|Iterable|AsyncIterable|Buffer|string} The cached response
|
||||
body. (optional)
|
||||
|
||||
`number` - Time in millis that this value is considered stale.
|
||||
|
||||
### Property `deleteAt`
|
||||
|
||||
`number` - Time in millis that this value is to be deleted from the cache. This
|
||||
is either the same sa staleAt or the `max-stale` caching directive.
|
||||
|
||||
The store must not return a response after the time defined in this property.
|
||||
|
||||
## `CacheStoreReadable`
|
||||
|
||||
This extends Node's [`Readable`](https://nodejs.org/api/stream.html#class-streamreadable)
|
||||
and defines extra properties relevant to the cache interceptor.
|
||||
|
||||
### Getter: `value`
|
||||
|
||||
The response's [`CacheStoreValue`](/docs/docs/api/CacheStore.md#cachestorevalue)
|
||||
|
||||
## `CacheStoreWriteable`
|
||||
|
||||
This extends Node's [`Writable`](https://nodejs.org/api/stream.html#class-streamwritable)
|
||||
and defines extra properties relevant to the cache interceptor.
|
||||
|
||||
### Setter: `rawTrailers`
|
||||
|
||||
If the response has trailers, the cache interceptor will pass them to the cache
|
||||
interceptor through this method.
|
||||
[RFC 9111]: https://www.rfc-editor.org/rfc/rfc9111.html
|
||||
[`node:sqlite`]: https://nodejs.org/api/sqlite.html
|
||||
+349
-163
@@ -1,103 +1,168 @@
|
||||
# Class: Client
|
||||
# Client
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v1.0.0-->
|
||||
|
||||
A basic HTTP/1.1 client, mapped on top a single TCP/TLS connection. Pipelining is disabled by default.
|
||||
<!--type=module-->
|
||||
|
||||
Requests are not guaranteed to be dispatched in order of invocation.
|
||||
<!-- source_link=lib/dispatcher/client.js -->
|
||||
|
||||
## `new Client(url[, options])`
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Arguments:
|
||||
A basic HTTP/1.1 client mapped on top of a single TCP/TLS connection. When the
|
||||
server supports HTTP/2 and selects it during ALPN negotiation, the same
|
||||
connection is used for HTTP/2. Pipelining is disabled by default.
|
||||
|
||||
* **url** `URL | string` - Should only include the **protocol, hostname, and port**.
|
||||
* **options** `ClientOptions` (optional)
|
||||
`Client` extends [`Dispatcher`][] and is the lowest-level dispatcher in undici:
|
||||
it manages exactly one origin over one connection. For pooling across multiple
|
||||
connections use [`Pool`][], and for routing across multiple origins use
|
||||
[`Agent`][]. Requests are not guaranteed to be dispatched in the order in which
|
||||
they are invoked.
|
||||
|
||||
Returns: `Client`
|
||||
```mjs
|
||||
import { Client } from 'undici'
|
||||
|
||||
### Parameter: `ClientOptions`
|
||||
const client = new Client('http://localhost:3000')
|
||||
const { statusCode, body } = await client.request({ path: '/', method: 'GET' })
|
||||
```
|
||||
|
||||
* **bodyTimeout** `number | null` (optional) - Default: `300e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds. Please note the `timeout` will be reset if you keep writing data to the socket everytime.
|
||||
* **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
|
||||
* **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by *keep-alive* hints from the server. Defaults to 10 minutes.
|
||||
* **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by *keep-alive* hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
|
||||
* **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server *keep-alive* hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
|
||||
* **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
|
||||
* **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
||||
* **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options.
|
||||
* **maxFragments** `number` (optional) - Default: `131072` - Maximum number of fragments in a message. Set to 0 to disable the limit.
|
||||
* **maxPayloadSize** `number` (optional) - Default: `134217728` (128 MB) - Maximum allowed payload size in bytes for WebSocket messages. Applied to uncompressed messages, compressed frame payloads, and decompressed (permessage-deflate) messages. Set to 0 to disable the limit.
|
||||
* **pipelining** `number | null` (optional) - Default: `1` - The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Carefully consider your workload and environment before enabling concurrent requests as pipelining may reduce performance if used incorrectly. Pipelining is sensitive to network stack settings as well as head of line blocking caused by e.g. long running requests. Set to `0` to disable keep-alive connections. This option has no effect once HTTP/2 is negotiated — see `maxConcurrentStreams` for the h2 dispatch ceiling.
|
||||
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null` - Configures how undici establishes TCP/TLS connections. Accepts two forms:
|
||||
* **Object (`ConnectOptions`)**: Options passed directly to the internal [`buildConnector()`](/docs/docs/api/Connector.md). This is the simplest way to customize TLS or socket behavior (e.g., setting `rejectUnauthorized`, `ca`, `socketPath`). See [`ConnectOptions`](#parameter-connectoptions) for available fields.
|
||||
* **Function**: A custom connector with the signature `(options, callback)`, where `options` contains `{ hostname, host, protocol, port, servername, localAddress, httpSocket }` and `callback` follows `(error, socket)`. Useful when you need full control over socket creation, such as adding custom validation or proxy logic. When a function is provided, undici wraps it to automatically inject `socketPath` and `allowH2` into the `options` argument if those values are set on the client.
|
||||
* **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
||||
* **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
|
||||
* **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
|
||||
* **allowH2**: `boolean` - Default: `true`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
|
||||
* **useH2c**: `boolean` - Default: `false`. Enforces h2c for non-https connections.
|
||||
* **maxConcurrentStreams**: `number` - Default: `100`. The maximum number of concurrent HTTP/2 streams per session. When `allowH2` negotiates h2, this — not `pipelining` (which is HTTP/1.1 only, per [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2)) — is the ceiling the Client uses to dispatch in-flight requests on a shared session. The same value is advertised to the server as `peerMaxConcurrentStreams`, capping how many streams the server may push back. The initial value is replaced by the server's `SETTINGS_MAX_CONCURRENT_STREAMS` whenever the server sends one, so a user-supplied value acts as a pre-`SETTINGS` default rather than a hard cap.
|
||||
* **initialWindowSize**: `number` (optional) - Default: `262144` (256KB). Sets the HTTP/2 stream-level flow-control window size (SETTINGS_INITIAL_WINDOW_SIZE). Must be a positive integer greater than 0. This default is higher than Node.js core's default (65535 bytes) to improve throughput, Node's choice is very conservative for current high-bandwith networks. See [RFC 7540 Section 6.9.2](https://datatracker.ietf.org/doc/html/rfc7540#section-6.9.2) for more details.
|
||||
* **connectionWindowSize**: `number` (optional) - Default `524288` (512KB). Sets the HTTP/2 connection-level flow-control window size using `ClientHttp2Session.setLocalWindowSize()`. Must be a positive integer greater than 0. This provides better flow control for the entire connection across multiple streams. See [Node.js HTTP/2 documentation](https://nodejs.org/api/http2.html#clienthttp2sessionsetlocalwindowsize) for more details.
|
||||
* **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections. This will emit a `ping` event on the client with the duration of the ping in milliseconds.
|
||||
## Class: `Client`
|
||||
|
||||
> **Notes about HTTP/2**
|
||||
> - It only works under TLS connections. h2c is not supported.
|
||||
> - The server must support HTTP/2 and choose it as the protocol during the ALPN negotiation.
|
||||
> - The server must not have a bigger priority for HTTP/1.1 than HTTP/2.
|
||||
> - Pseudo headers are automatically attached to the request. If you try to set them, they will be overwritten.
|
||||
> - The `:path` header is automatically set to the request path.
|
||||
> - The `:method` header is automatically set to the request method.
|
||||
> - The `:scheme` header is automatically set to the request scheme.
|
||||
> - The `:authority` header is automatically set to the request `host[:port]`.
|
||||
> - `PUSH` frames are yet not supported.
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
#### Parameter: `ConnectOptions`
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
|
||||
Furthermore, the following options can be passed:
|
||||
### `new Client(url[, options])`
|
||||
|
||||
* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
|
||||
* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: 100.
|
||||
* **timeout** `number | null` (optional) - In milliseconds, Default `10e3`.
|
||||
* **servername** `string | null` (optional)
|
||||
* **keepAlive** `boolean | null` (optional) - Default: `true` - TCP keep-alive enabled
|
||||
* **keepAliveInitialDelay** `number | null` (optional) - Default: `60000` - TCP keep-alive interval for the socket in milliseconds
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
### Example - Basic Client instantiation
|
||||
* `url` {URL|string} The origin to connect to. Only the protocol, hostname, and
|
||||
port are used.
|
||||
* `options` {Object} (optional)
|
||||
* `bodyTimeout` {number|null} The timeout, in milliseconds, after which a
|
||||
request times out while receiving body data. Monitors the time between
|
||||
consecutive body chunks. Use `0` to disable it entirely. **Default:**
|
||||
`300e3`.
|
||||
* `headersTimeout` {number|null} The timeout, in milliseconds, the parser
|
||||
waits to receive the complete HTTP headers before the request times out. Use
|
||||
`0` to disable it entirely. **Default:** `300e3`.
|
||||
* `connectTimeout` {number|null} The timeout, in milliseconds, for
|
||||
establishing a socket connection. Use `0` to disable it entirely.
|
||||
**Default:** `10e3`.
|
||||
* `keepAliveTimeout` {number|null} The timeout, in milliseconds, after which a
|
||||
socket with no active requests times out. Monitors the time between activity
|
||||
on a connected socket. This value may be overridden by *keep-alive* hints
|
||||
from the server. **Default:** `4e3`.
|
||||
* `keepAliveMaxTimeout` {number|null} The maximum allowed `keepAliveTimeout`,
|
||||
in milliseconds, when overridden by *keep-alive* hints from the server.
|
||||
**Default:** `600e3`.
|
||||
* `keepAliveTimeoutThreshold` {number|null} A number of milliseconds
|
||||
subtracted from server *keep-alive* hints when overriding `keepAliveTimeout`,
|
||||
to account for timing inaccuracies caused by e.g. transport latency.
|
||||
**Default:** `2e3`.
|
||||
* `maxHeaderSize` {number|null} The maximum length of request headers, in
|
||||
bytes. **Default:** the value of Node.js' `--max-http-header-size` or
|
||||
`16384` (16 KiB).
|
||||
* `maxResponseSize` {number|null} The maximum length of a response body, in
|
||||
bytes. Set to `-1` to disable it. **Default:** `-1`.
|
||||
* `maxRequestsPerClient` {number|null} The maximum number of requests to send
|
||||
over a single connection before the socket is reset. Use `0` to disable this
|
||||
limit. **Default:** `null`.
|
||||
* `localAddress` {string|null} The local IP address the socket should connect
|
||||
from. **Default:** `null`.
|
||||
* `pipelining` {number|null} The number of concurrent requests sent over the
|
||||
single connection, per [RFC 7230, section 6.3.2][]. Only enable values
|
||||
greater than `1` when the remote server is trusted. Set to `0` to disable
|
||||
keep-alive connections. Has no effect once HTTP/2 is negotiated; see
|
||||
`maxConcurrentStreams` for the HTTP/2 dispatch ceiling. **Default:** `1`.
|
||||
* `connect` {Object|Function|null} Configures how connections are established.
|
||||
Either a `ConnectOptions` object passed to the built-in
|
||||
[`buildConnector`][] (every [`tls.connect()`][] option is accepted, plus the
|
||||
fields below), or a custom connector function with the signature
|
||||
`(options, callback)`. **Default:** `null`.
|
||||
* `socketPath` {string|null} An IPC endpoint, either a Unix domain socket or
|
||||
a Windows named pipe. **Default:** `null`.
|
||||
* `maxCachedSessions` {number|null} The maximum number of cached TLS
|
||||
sessions. Use `0` to disable TLS session caching. **Default:** `100`.
|
||||
* `timeout` {number|null} The connection timeout, in milliseconds.
|
||||
**Default:** `10e3`.
|
||||
* `servername` {string|null} The TLS server name used for SNI.
|
||||
* `keepAlive` {boolean|null} Whether TCP keep-alive is enabled on the socket.
|
||||
**Default:** `true`.
|
||||
* `keepAliveInitialDelay` {number|null} The TCP keep-alive interval for the
|
||||
socket, in milliseconds. **Default:** `60000`.
|
||||
* `strictContentLength` {boolean} Whether to throw when the request
|
||||
`content-length` header does not match the length of the request body.
|
||||
Disabling this exposes the application to HTTP request smuggling; only do so
|
||||
in controlled environments where the request source is fully trusted.
|
||||
**Default:** `true`.
|
||||
* `autoSelectFamily` {boolean} Enables a family autodetection algorithm that
|
||||
loosely implements section 5 of [RFC 8305][]. Ignored if unsupported by the
|
||||
current Node.js version. **Default:** `false`.
|
||||
* `autoSelectFamilyAttemptTimeout` {number} The time, in milliseconds, to wait
|
||||
for a connection attempt to finish before trying the next address when
|
||||
`autoSelectFamily` is enabled. **Default:** `250`.
|
||||
* `allowH2` {boolean} Enables HTTP/2 support when the server assigns it a
|
||||
higher priority through ALPN negotiation. **Default:** `true`.
|
||||
* `useH2c` {boolean} Enforces h2c (HTTP/2 cleartext) for non-HTTPS
|
||||
connections. **Default:** `false`.
|
||||
* `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2
|
||||
streams for a single session. Once h2 is negotiated this — not `pipelining`,
|
||||
which is HTTP/1.1 only — is the ceiling used to dispatch in-flight requests.
|
||||
It may be overridden by the server's `SETTINGS_MAX_CONCURRENT_STREAMS`
|
||||
frame. **Default:** `100`.
|
||||
* `initialWindowSize` {number} The HTTP/2 stream-level flow-control window
|
||||
size (`SETTINGS_INITIAL_WINDOW_SIZE`). Must be a positive integer.
|
||||
**Default:** `262144`.
|
||||
* `connectionWindowSize` {number} The HTTP/2 connection-level flow-control
|
||||
window size set via `ClientHttp2Session.setLocalWindowSize()`. Must be a
|
||||
positive integer. **Default:** `524288`.
|
||||
* `pingInterval` {number} The time interval, in milliseconds, between HTTP/2
|
||||
PING frames. Set to `0` to disable PING frames. Applies only to HTTP/2
|
||||
connections and emits a `ping` event on the client. **Default:** `60e3`.
|
||||
* `webSocket` {Object} (optional) WebSocket-specific configuration.
|
||||
* `maxFragments` {number} The maximum number of fragments in a message. Set
|
||||
to `0` to disable the limit. **Default:** `131072`.
|
||||
* `maxPayloadSize` {number} The maximum allowed payload size, in bytes, for
|
||||
WebSocket messages. Applied to uncompressed messages, compressed frame
|
||||
payloads, and decompressed (`permessage-deflate`) messages. Set to `0` to
|
||||
disable the limit. **Default:** `134217728`.
|
||||
* Returns: {Client}
|
||||
|
||||
This will instantiate the undici Client, but it will not connect to the origin until something is queued. Consider using `client.connect` to prematurely connect to the origin, or just call `client.request`.
|
||||
Instantiating a `Client` does not open a connection; the connection is
|
||||
established lazily once a request is queued. Call `client.connect()` to connect
|
||||
eagerly.
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
> Notes about HTTP/2:
|
||||
>
|
||||
> * Pseudo headers (`:path`, `:method`, `:scheme`, `:authority`) are attached
|
||||
> automatically and overwrite any user-provided values.
|
||||
> * The server must support HTTP/2 and select it during ALPN negotiation, and
|
||||
> must not give HTTP/1.1 a higher priority than HTTP/2.
|
||||
> * `PUSH` frames are not supported.
|
||||
|
||||
```mjs displayName="Basic instantiation"
|
||||
import { Client } from 'undici'
|
||||
|
||||
const client = new Client('http://localhost:3000')
|
||||
```
|
||||
|
||||
### Example - Connect with TLS options (object form)
|
||||
|
||||
Pass a `ConnectOptions` object to customize the TLS connection. The options are forwarded to the internal `buildConnector()`.
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
```mjs displayName="TLS options (object connector)"
|
||||
import { Client } from 'undici'
|
||||
import fs from 'node:fs'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
const client = new Client('https://localhost:3000', {
|
||||
connect: {
|
||||
rejectUnauthorized: false,
|
||||
ca: fs.readFileSync('./ca-cert.pem')
|
||||
ca: readFileSync('./ca-cert.pem')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Example - Connect via Unix domain socket
|
||||
|
||||
Use the `socketPath` option to connect through an IPC endpoint instead of a TCP connection.
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
```mjs displayName="Unix domain socket"
|
||||
import { Client } from 'undici'
|
||||
|
||||
const client = new Client('http://localhost:3000', {
|
||||
@@ -107,14 +172,20 @@ const client = new Client('http://localhost:3000', {
|
||||
})
|
||||
```
|
||||
|
||||
### Example - Custom connector (function form)
|
||||
```mjs displayName="Custom DNS lookup"
|
||||
import { Client } from 'undici'
|
||||
import dns from 'node:dns'
|
||||
|
||||
Pass a function for full control over socket creation. This allows you to perform additional checks on the socket, use a proxy, or implement custom connection logic.
|
||||
const client = new Client('https://example.com', {
|
||||
connect: {
|
||||
lookup (hostname, options, callback) {
|
||||
dns.lookup(hostname, { ...options, family: 4 }, callback)
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
> **Note:** When a function is provided, undici wraps it to automatically inject `socketPath` and `allowH2` into the first argument (`options`) when those values are set on the client.
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
```mjs displayName="Custom connector (function)"
|
||||
import { Client, buildConnector } from 'undici'
|
||||
|
||||
const connector = buildConnector({ rejectUnauthorized: false })
|
||||
@@ -123,9 +194,6 @@ const client = new Client('https://localhost:3000', {
|
||||
connector(opts, (err, socket) => {
|
||||
if (err) {
|
||||
cb(err)
|
||||
} else if (/* assertion */) {
|
||||
socket.destroy()
|
||||
cb(new Error('kaboom'))
|
||||
} else {
|
||||
cb(null, socket)
|
||||
}
|
||||
@@ -134,83 +202,180 @@ const client = new Client('https://localhost:3000', {
|
||||
})
|
||||
```
|
||||
|
||||
For more details on building custom connectors, see [Connector](/docs/docs/api/Connector.md).
|
||||
When a connector function is provided, undici wraps it to automatically inject
|
||||
`socketPath` and `allowH2` into the `options` argument when those values are set
|
||||
on the client. See [`Connector`][] for details on building custom connectors.
|
||||
|
||||
## Instance Methods
|
||||
### `client.close([callback])`
|
||||
|
||||
### `Client.close([callback])`
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
|
||||
Implements [`dispatcher.close([callback])`][]. Closes the client gracefully,
|
||||
waiting for enqueued requests to complete before resolving.
|
||||
|
||||
### `Client.destroy([error, callback])`
|
||||
### `client.destroy([error[, callback]])`
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided).
|
||||
Implements [`dispatcher.destroy([error[, callback]])`][]. Destroys the client
|
||||
abruptly, aborting all pending and running requests, and waits until the socket
|
||||
is closed before invoking `callback` (or resolving the returned promise).
|
||||
|
||||
### `Client.connect(options[, callback])`
|
||||
### `client.connect(options[, callback])`
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
### `Client.dispatch(options, handlers)`
|
||||
* `options` {Object} The connect options, identical to
|
||||
[`dispatcher.connect()`][] except that `origin` is omitted because the client
|
||||
is bound to a single origin.
|
||||
* `callback` {Function} (optional)
|
||||
* Returns: {Promise|void} A promise when `callback` is not provided.
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
Starts a connection to the client's origin and tunnels a connection through it,
|
||||
as defined by [`dispatcher.connect()`][].
|
||||
|
||||
### `Client.pipeline(options, handler)`
|
||||
### `client.dispatch(options, handlers)`
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
### `Client.request(options[, callback])`
|
||||
Implements [`dispatcher.dispatch(options, handler)`][]. This is the lowest-level
|
||||
API used by all other request methods; prefer `client.request()` for most use
|
||||
cases.
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
### `client.pipeline(options, handler)`
|
||||
|
||||
### `Client.stream(options, factory[, callback])`
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
|
||||
See [`dispatcher.pipeline(options, handler)`][]. Sends a request and returns a
|
||||
{Duplex} stream that the request body is written to and the response body is
|
||||
read from.
|
||||
|
||||
### `Client.upgrade(options[, callback])`
|
||||
### `client.request(options[, callback])`
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
## Instance Properties
|
||||
* `options` {Object}
|
||||
* `callback` {Function} (optional)
|
||||
* Returns: {Promise|void} A promise resolving to the response data when
|
||||
`callback` is not provided.
|
||||
|
||||
### `Client.closed`
|
||||
See [`dispatcher.request(options[, callback])`][]. Performs an HTTP request and
|
||||
returns its status code, headers, trailers, and a readable body stream.
|
||||
|
||||
* `boolean`
|
||||
```mjs
|
||||
import { Client } from 'undici'
|
||||
|
||||
const client = new Client('http://localhost:3000')
|
||||
const { statusCode, headers, body } = await client.request({
|
||||
path: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
console.log(statusCode)
|
||||
for await (const chunk of body) {
|
||||
console.log(chunk.toString())
|
||||
}
|
||||
```
|
||||
|
||||
### `client.stream(options, factory[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `options` {Object}
|
||||
* `factory` {Function} Returns a {Writable} the response body is piped into.
|
||||
* `callback` {Function} (optional)
|
||||
* Returns: {Promise|void} A promise when `callback` is not provided.
|
||||
|
||||
See [`dispatcher.stream(options, factory[, callback])`][]. A faster alternative
|
||||
to `client.request()` when the destination of the response body is known in
|
||||
advance.
|
||||
|
||||
### `client.upgrade(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `options` {Object}
|
||||
* `callback` {Function} (optional)
|
||||
* Returns: {Promise|void} A promise when `callback` is not provided.
|
||||
|
||||
See [`dispatcher.upgrade(options[, callback])`][]. Upgrades a connection to a
|
||||
different protocol, such as for WebSocket or HTTP `CONNECT`.
|
||||
|
||||
### `client.closed`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {boolean}
|
||||
|
||||
`true` after `client.close()` has been called.
|
||||
|
||||
### `Client.destroyed`
|
||||
### `client.destroyed`
|
||||
|
||||
* `boolean`
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
`true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed.
|
||||
* Type: {boolean}
|
||||
|
||||
### `Client.pipelining`
|
||||
`true` after `client.destroy()` has been called, or after `client.close()` has
|
||||
been called and the client shutdown has completed.
|
||||
|
||||
* `number`
|
||||
### `client.pipelining`
|
||||
|
||||
Property to get and set the pipelining factor.
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
## Instance Events
|
||||
* Type: {number}
|
||||
|
||||
The pipelining factor. This property can be read and written to adjust the
|
||||
number of concurrent requests sent over the connection. Only enable pipelining
|
||||
with trusted remote servers.
|
||||
|
||||
### `client.stats`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
* Type: {ClientStats}
|
||||
|
||||
Aggregate statistics for the client. See [`ClientStats`][].
|
||||
|
||||
### Event: `'connect'`
|
||||
|
||||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
Parameters:
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>}
|
||||
|
||||
* **origin** `URL`
|
||||
* **targets** `Array<Dispatcher>`
|
||||
Emitted when a socket has been created and connected. The client connects once
|
||||
`client.size > 0`. See [`Dispatcher` Event: `'connect'`][].
|
||||
|
||||
Emitted when a socket has been created and connected. The client will connect once `client.size > 0`.
|
||||
|
||||
#### Example - Client connect event
|
||||
|
||||
```js
|
||||
import { createServer } from 'http'
|
||||
```mjs
|
||||
import { createServer } from 'node:http'
|
||||
import { Client } from 'undici'
|
||||
import { once } from 'events'
|
||||
import { once } from 'node:events'
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.end('Hello, World!')
|
||||
@@ -221,43 +386,40 @@ await once(server, 'listening')
|
||||
const client = new Client(`http://localhost:${server.address().port}`)
|
||||
|
||||
client.on('connect', (origin) => {
|
||||
console.log(`Connected to ${origin}`) // should print before the request body statement
|
||||
console.log(`Connected to ${origin}`)
|
||||
})
|
||||
|
||||
try {
|
||||
const { body } = await client.request({
|
||||
path: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
body.setEncoding('utf-8')
|
||||
body.on('data', console.log)
|
||||
client.close()
|
||||
server.close()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
client.close()
|
||||
server.close()
|
||||
}
|
||||
const { body } = await client.request({ path: '/', method: 'GET' })
|
||||
body.setEncoding('utf8')
|
||||
body.on('data', console.log)
|
||||
|
||||
client.close()
|
||||
server.close()
|
||||
```
|
||||
|
||||
### Event: `'disconnect'`
|
||||
|
||||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v4.0.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/771
|
||||
description: The event is only emitted if the client had previously
|
||||
connected.
|
||||
-->
|
||||
|
||||
Parameters:
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>}
|
||||
* `error` {Error}
|
||||
|
||||
* **origin** `URL`
|
||||
* **targets** `Array<Dispatcher>`
|
||||
* **error** `Error`
|
||||
Emitted when a socket has disconnected. The `error` argument is the error that
|
||||
caused the disconnection. The client reconnects if or once `client.size > 0`.
|
||||
See [`Dispatcher` Event: `'disconnect'`][].
|
||||
|
||||
Emitted when socket has disconnected. The error argument of the event is the error which caused the socket to disconnect. The client will reconnect if or once `client.size > 0`.
|
||||
|
||||
#### Example - Client disconnect event
|
||||
|
||||
```js
|
||||
import { createServer } from 'http'
|
||||
```mjs
|
||||
import { createServer } from 'node:http'
|
||||
import { Client } from 'undici'
|
||||
import { once } from 'events'
|
||||
import { once } from 'node:events'
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.destroy()
|
||||
@@ -272,12 +434,10 @@ client.on('disconnect', (origin) => {
|
||||
})
|
||||
|
||||
try {
|
||||
await client.request({
|
||||
path: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
await client.request({ path: '/', method: 'GET' })
|
||||
} catch (error) {
|
||||
console.error(error.message)
|
||||
} finally {
|
||||
client.close()
|
||||
server.close()
|
||||
}
|
||||
@@ -285,16 +445,19 @@ try {
|
||||
|
||||
### Event: `'drain'`
|
||||
|
||||
Emitted when pipeline is no longer busy.
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
|
||||
* `origin` {URL}
|
||||
|
||||
#### Example - Client drain event
|
||||
Emitted when the pipeline is no longer busy. See
|
||||
[`Dispatcher` Event: `'drain'`][].
|
||||
|
||||
```js
|
||||
import { createServer } from 'http'
|
||||
```mjs
|
||||
import { createServer } from 'node:http'
|
||||
import { Client } from 'undici'
|
||||
import { once } from 'events'
|
||||
import { once } from 'node:events'
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.end('Hello, World!')
|
||||
@@ -310,17 +473,40 @@ client.on('drain', () => {
|
||||
server.close()
|
||||
})
|
||||
|
||||
const requests = [
|
||||
await Promise.all([
|
||||
client.request({ path: '/', method: 'GET' }),
|
||||
client.request({ path: '/', method: 'GET' }),
|
||||
client.request({ path: '/', method: 'GET' })
|
||||
]
|
||||
|
||||
await Promise.all(requests)
|
||||
|
||||
console.log('requests completed')
|
||||
])
|
||||
```
|
||||
|
||||
### Event: `'error'`
|
||||
|
||||
Invoked for user errors such as throwing in the `onResponseError` handler.
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `error` {Error}
|
||||
|
||||
Emitted for user errors, such as throwing inside an `onResponseError` handler.
|
||||
|
||||
[RFC 7230, section 6.3.2]: https://tools.ietf.org/html/rfc7230#section-6.3.2
|
||||
[RFC 8305]: https://tools.ietf.org/html/rfc8305#section-5
|
||||
[`Agent`]: Agent.md#class-agent
|
||||
[`ClientStats`]: ClientStats.md
|
||||
[`Connector`]: Connector.md
|
||||
[`Dispatcher` Event: `'connect'`]: Dispatcher.md#event-connect
|
||||
[`Dispatcher` Event: `'disconnect'`]: Dispatcher.md#event-disconnect
|
||||
[`Dispatcher` Event: `'drain'`]: Dispatcher.md#event-drain
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`Pool`]: Pool.md#class-pool
|
||||
[`buildConnector`]: Connector.md
|
||||
[`dispatcher.close([callback])`]: Dispatcher.md#dispatcherclosecallback
|
||||
[`dispatcher.connect()`]: Dispatcher.md#dispatcherconnectoptions-callback
|
||||
[`dispatcher.destroy([error[, callback]])`]: Dispatcher.md#dispatcherdestroyerror-callback
|
||||
[`dispatcher.dispatch(options, handler)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`dispatcher.pipeline(options, handler)`]: Dispatcher.md#dispatcherpipelineoptions-handler
|
||||
[`dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`dispatcher.stream(options, factory[, callback])`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
|
||||
[`dispatcher.upgrade(options[, callback])`]: Dispatcher.md#dispatcherupgradeoptions-callback
|
||||
[`tls.connect()`]: https://nodejs.org/api/tls.html#tlsconnectoptions-callback
|
||||
+80
-14
@@ -1,27 +1,93 @@
|
||||
# Class: ClientStats
|
||||
# ClientStats
|
||||
|
||||
Stats for a [Client](/docs/docs/api/Client.md).
|
||||
<!--introduced_in=v7.9.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/util/stats.js -->
|
||||
|
||||
## `new ClientStats(client)`
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Arguments:
|
||||
A `ClientStats` is a point-in-time snapshot of the connection and request
|
||||
state of a single [`Client`][]. Instances are not created directly; a fresh
|
||||
snapshot is produced each time the [`client.stats`][] getter is read.
|
||||
|
||||
* **client** `Client` - Client from which to return stats.
|
||||
```mjs
|
||||
import { Client } from 'undici'
|
||||
|
||||
## Instance Properties
|
||||
const client = new Client('http://localhost:3000')
|
||||
const stats = client.stats
|
||||
console.log(stats.connected, stats.running)
|
||||
```
|
||||
|
||||
### `ClientStats.connected`
|
||||
```cjs
|
||||
const { Client } = require('undici')
|
||||
|
||||
Boolean if socket as open connection by this client.
|
||||
const client = new Client('http://localhost:3000')
|
||||
const stats = client.stats
|
||||
console.log(stats.connected, stats.running)
|
||||
```
|
||||
|
||||
### `ClientStats.pending`
|
||||
## Class: `ClientStats`
|
||||
|
||||
Number of pending requests of this client.
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
### `ClientStats.running`
|
||||
Captures the connection and request counters of a [`Client`][] at the moment
|
||||
the snapshot is taken. The values are read from the client at construction time
|
||||
and do not update afterwards; read [`client.stats`][] again for current values.
|
||||
|
||||
Number of currently active requests across this client.
|
||||
### `new ClientStats(client)`
|
||||
|
||||
### `ClientStats.size`
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
Number of active, pending, or queued requests of this clients.
|
||||
* `client` {Client} The client to read the statistics from.
|
||||
|
||||
Creates a snapshot from the supplied `client`. This constructor is used
|
||||
internally by the [`client.stats`][] getter and is not normally invoked by
|
||||
application code.
|
||||
|
||||
### `clientStats.connected`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
* Type: {boolean}
|
||||
|
||||
`true` when the client currently has an open socket connection.
|
||||
|
||||
### `clientStats.pending`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of open socket connections of this client that do not have an active
|
||||
request.
|
||||
|
||||
### `clientStats.running`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of currently active requests of this client.
|
||||
|
||||
### `clientStats.size`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.9.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of active, pending, or queued requests of this client.
|
||||
|
||||
[`Client`]: Client.md#class-client
|
||||
[`client.stats`]: Client.md#clientstats
|
||||
+114
-32
@@ -1,49 +1,87 @@
|
||||
# Connector
|
||||
|
||||
Undici creates the underlying socket via the connector builder.
|
||||
Normally, this happens automatically and you don't need to care about this,
|
||||
but if you need to perform some additional check over the currently used socket,
|
||||
this is the right place.
|
||||
<!--introduced_in=v4.3.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/core/connect.js -->
|
||||
|
||||
If you want to create a custom connector, you must import the `buildConnector` utility.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
#### Parameter: `buildConnector.BuildOptions`
|
||||
undici opens the underlying socket for every request through a *connector*. By
|
||||
default this is handled internally, so most applications never interact with the
|
||||
connector directly. When a request needs additional control over the socket —
|
||||
for example to inspect a TLS certificate, pin a CA fingerprint, or tunnel the
|
||||
connection — a custom connector can be supplied through the `connect` option of
|
||||
a [`Dispatcher`][].
|
||||
|
||||
Every Tls option, see [here](https://nodejs.org/api/tls.html#tls_tls_connect_options_callback).
|
||||
Furthermore, the following options can be passed:
|
||||
Use the `buildConnector` helper to construct a connector with sensible defaults
|
||||
and then wrap it:
|
||||
|
||||
* **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
|
||||
* **preferH2** `boolean` (optional) - Default: `false` - Only effective together with `allowH2`. When `true`, ALPN is offered as `['h2', 'http/1.1']` (HTTP/2 first) instead of the default `['http/1.1', 'h2']`. Use this when the server selects the ALPN protocol by *client* preference (e.g. some load balancers) so that HTTP/2 is negotiated whenever the server supports it. If the server does not support HTTP/2, ALPN transparently falls back to `http/1.1`.
|
||||
* **maxCachedSessions** `number | null` (optional) - Default: `100` - Maximum number of TLS cached sessions. Use 0 to disable TLS session caching. Default: `100`.
|
||||
* **timeout** `number | null` (optional) - In milliseconds. Default `10e3`.
|
||||
* **servername** `string | null` (optional)
|
||||
```mjs
|
||||
import { buildConnector } from 'undici'
|
||||
|
||||
Once you call `buildConnector`, it will return a connector function, which takes the following parameters.
|
||||
const connector = buildConnector({ rejectUnauthorized: false })
|
||||
```
|
||||
|
||||
#### Parameter: `connector.Options`
|
||||
`buildConnector` accepts the same TLS options as
|
||||
[`tls.connect()`][tls.connect] in addition to the undici-specific options listed
|
||||
below, and returns a [`connector`](#connectoroptions-callback) function that
|
||||
undici invokes for each new connection.
|
||||
|
||||
* **hostname** `string` (required)
|
||||
* **host** `string` (optional)
|
||||
* **protocol** `string` (required)
|
||||
* **port** `string` (required)
|
||||
* **servername** `string` (optional)
|
||||
* **localAddress** `string | null` (optional) Local address the socket should connect from.
|
||||
* **httpSocket** `Socket` (optional) Establish secure connection on a given socket rather than creating a new socket. It can only be sent on TLS update.
|
||||
## `buildConnector([options])`
|
||||
|
||||
### Basic example
|
||||
<!-- YAML
|
||||
added: v5.23.4
|
||||
-->
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
* `options` {buildConnector.BuildOptions} (optional) Connection options. In
|
||||
addition to every [`tls.connect()`][tls.connect] option, the following fields
|
||||
are supported:
|
||||
* `allowH2` {boolean} Whether to offer HTTP/2 (`h2`) during TLS ALPN
|
||||
negotiation. **Default:** `true`.
|
||||
* `preferH2` {boolean} Only effective together with `allowH2`. When `true`,
|
||||
ALPN is offered as `['h2', 'http/1.1']` (HTTP/2 first) instead of the default
|
||||
`['http/1.1', 'h2']`. Use this when the server selects the ALPN protocol by
|
||||
*client* preference (for example, some load balancers) so that HTTP/2 is
|
||||
negotiated whenever the server supports it. If the server does not support
|
||||
HTTP/2, ALPN transparently falls back to `http/1.1`. **Default:** `false`.
|
||||
* `maxCachedSessions` {number|null} Maximum number of TLS sessions to cache for
|
||||
reuse. Use `0` to disable TLS session caching. Must be a non-negative
|
||||
integer. **Default:** `100`.
|
||||
* `socketPath` {string|null} An IPC endpoint, either a Unix domain socket or a
|
||||
Windows named pipe. **Default:** `null`.
|
||||
* `timeout` {number|null} The connection timeout in milliseconds. A
|
||||
[`ConnectTimeoutError`][] is raised when the socket fails to connect within
|
||||
this period. **Default:** `10e3`.
|
||||
* `port` {number} The port to connect to when one is not provided to the
|
||||
connector. Defaults to `443` for `https:` and `80` otherwise.
|
||||
* `keepAlive` {boolean|null} Whether to enable TCP keep-alive on the socket.
|
||||
**Default:** `true`.
|
||||
* `keepAliveInitialDelay` {number|null} The delay in milliseconds before the
|
||||
first TCP keep-alive probe is sent on an idle socket. **Default:** `60e3`.
|
||||
* `typeOfService` {number|null} The IP Type of Service (ToS) value to set on
|
||||
the socket. **Default:** `null`.
|
||||
* Returns: {buildConnector.connector} A
|
||||
[`connector`](#connectoroptions-callback) function bound to the supplied
|
||||
options.
|
||||
|
||||
Builds a connector function. `maxCachedSessions` must be a non-negative integer
|
||||
or an [`InvalidArgumentError`][] is thrown.
|
||||
|
||||
The returned function may be passed directly as the `connect` option of a
|
||||
[`Dispatcher`][], or wrapped to perform extra validation on each socket before it
|
||||
is handed back to undici.
|
||||
|
||||
```mjs
|
||||
import { Client, buildConnector } from 'undici'
|
||||
|
||||
const connector = buildConnector({ rejectUnauthorized: false })
|
||||
|
||||
const client = new Client('https://localhost:3000', {
|
||||
connect (opts, cb) {
|
||||
connector(opts, (err, socket) => {
|
||||
if (err) {
|
||||
cb(err)
|
||||
} else if (/* assertion */) {
|
||||
} else if (/* assertion */ false) {
|
||||
socket.destroy()
|
||||
cb(new Error('kaboom'))
|
||||
} else {
|
||||
@@ -54,15 +92,53 @@ const client = new Client('https://localhost:3000', {
|
||||
})
|
||||
```
|
||||
|
||||
## `connector(options, callback)`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.3.0
|
||||
-->
|
||||
|
||||
* `options` {Object} Per-connection options provided by undici.
|
||||
* `hostname` {string} The host to connect to.
|
||||
* `host` {string} (optional) The value of the request `Host` header, used to
|
||||
derive the TLS `servername` when one is not supplied.
|
||||
* `protocol` {string} The request protocol, for example `'https:'` or
|
||||
`'http:'`. A value of `'https:'` establishes a TLS connection.
|
||||
* `port` {string} The port to connect to. Defaults to `443` for `https:` and
|
||||
`80` otherwise.
|
||||
* `servername` {string} (optional) The TLS Server Name Indication (SNI) value.
|
||||
Derived from `host` when omitted.
|
||||
* `localAddress` {string|null} (optional) The local address the socket should
|
||||
connect from.
|
||||
* `httpSocket` {Socket} (optional) An existing socket on which to establish the
|
||||
secure connection rather than creating a new one. May only be supplied for a
|
||||
TLS upgrade (`protocol` of `'https:'`).
|
||||
* `callback` {Function} Called once the socket has connected or failed.
|
||||
* `err` {Error|null} The connection error, or `null` on success.
|
||||
* `socket` {Socket|TLSSocket|null} The connected socket, or `null` on failure.
|
||||
* Returns: {Socket|TLSSocket} The socket being established.
|
||||
|
||||
The function returned by [`buildConnector()`](#buildconnectoroptions). undici
|
||||
calls it for every new connection. It opens a [`net.Socket`][] for `http:`
|
||||
targets or a [`tls.TLSSocket`][] for `https:` targets, applies TCP keep-alive and
|
||||
the connect timeout, and invokes `callback` once the socket connects or errors.
|
||||
The socket is also returned synchronously.
|
||||
|
||||
When connecting over TLS, established sessions are cached (subject to
|
||||
`maxCachedSessions`) and reused for subsequent connections to the same
|
||||
`servername` or `hostname`.
|
||||
|
||||
### Example: validate the CA fingerprint
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
A custom connector is a convenient place to verify the server certificate before
|
||||
any request is sent over the socket.
|
||||
|
||||
```mjs
|
||||
import { Client, buildConnector } from 'undici'
|
||||
|
||||
const caFingerprint = 'FO:OB:AR'
|
||||
const connector = buildConnector({ rejectUnauthorized: false })
|
||||
|
||||
const client = new Client('https://localhost:3000', {
|
||||
connect (opts, cb) {
|
||||
connector(opts, (err, socket) => {
|
||||
@@ -97,20 +173,26 @@ client.request({
|
||||
function getIssuerCertificate (socket) {
|
||||
let certificate = socket.getPeerCertificate(true)
|
||||
while (certificate && Object.keys(certificate).length > 0) {
|
||||
// invalid certificate
|
||||
// Invalid certificate.
|
||||
if (certificate.issuerCertificate == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
// We have reached the root certificate.
|
||||
// In case of self-signed certificates, `issuerCertificate` may be a circular reference.
|
||||
// We have reached the root certificate. In case of self-signed
|
||||
// certificates, `issuerCertificate` may be a circular reference.
|
||||
if (certificate.fingerprint256 === certificate.issuerCertificate.fingerprint256) {
|
||||
break
|
||||
}
|
||||
|
||||
// continue the loop
|
||||
certificate = certificate.issuerCertificate
|
||||
}
|
||||
return certificate
|
||||
}
|
||||
```
|
||||
|
||||
[`ConnectTimeoutError`]: Errors.md#class-connecttimeouterror
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`InvalidArgumentError`]: Errors.md#class-invalidargumenterror
|
||||
[`net.Socket`]: https://nodejs.org/api/net.html#class-netsocket
|
||||
[`tls.TLSSocket`]: https://nodejs.org/api/tls.html#class-tlstlssocket
|
||||
[tls.connect]: https://nodejs.org/api/tls.html#tlsconnectoptions-callback
|
||||
+64
-24
@@ -1,19 +1,51 @@
|
||||
# MIME Type Parsing
|
||||
|
||||
## `MIMEType` interface
|
||||
<!--introduced_in=v4.13.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/web/fetch/data-url.js -->
|
||||
|
||||
* **type** `string`
|
||||
* **subtype** `string`
|
||||
* **parameters** `Map<string, string>`
|
||||
* **essence** `string`
|
||||
> Stability: 2 - Stable
|
||||
|
||||
undici exposes helpers for parsing and serializing MIME types according to the
|
||||
WHATWG [MIME Sniffing Standard][]. These are the same primitives undici uses
|
||||
internally to interpret `Content-Type` headers and `data:` URLs.
|
||||
|
||||
Import the functions from `'undici'`:
|
||||
|
||||
```mjs
|
||||
import { parseMIMEType, serializeAMimeType } from 'undici'
|
||||
```
|
||||
|
||||
A parsed MIME type is represented as a plain object with the following shape:
|
||||
|
||||
* `type` {string} The lowercased type, for example `'text'`.
|
||||
* `subtype` {string} The lowercased subtype, for example `'html'`.
|
||||
* `parameters` {Map<string, string>} The parameters, keyed by lowercased
|
||||
parameter name.
|
||||
* `essence` {string} The concatenation of `type`, `'/'`, and `subtype`, for
|
||||
example `'text/html'`.
|
||||
|
||||
## `parseMIMEType(input)`
|
||||
|
||||
Implements [parse a MIME type](https://mimesniff.spec.whatwg.org/#parse-a-mime-type).
|
||||
<!-- YAML
|
||||
added: v4.13.0
|
||||
-->
|
||||
|
||||
Parses a MIME type, returning its type, subtype, and any associated parameters. If the parser can't parse an input it returns the string literal `'failure'`.
|
||||
* `input` {string} The MIME type string to parse.
|
||||
* Returns: {Object|string} A parsed MIME type record, or the string literal
|
||||
`'failure'` when `input` cannot be parsed.
|
||||
* `type` {string} The lowercased type.
|
||||
* `subtype` {string} The lowercased subtype.
|
||||
* `parameters` {Map<string, string>} The parsed parameters, keyed by
|
||||
lowercased parameter name.
|
||||
* `essence` {string} The MIME type essence, `` `${type}/${subtype}` ``.
|
||||
|
||||
```js
|
||||
Implements the WHATWG [parse a MIME type][] algorithm. The `type` and `subtype`
|
||||
are lowercased, and only the first occurrence of a duplicate parameter name is
|
||||
retained. When `input` is empty, lacks a `/`, or otherwise violates the grammar,
|
||||
the string literal `'failure'` is returned instead of a record.
|
||||
|
||||
```mjs
|
||||
import { parseMIMEType } from 'undici'
|
||||
|
||||
parseMIMEType('text/html; charset=gbk')
|
||||
@@ -23,21 +55,31 @@ parseMIMEType('text/html; charset=gbk')
|
||||
// parameters: Map(1) { 'charset' => 'gbk' },
|
||||
// essence: 'text/html'
|
||||
// }
|
||||
|
||||
parseMIMEType('not a mime type')
|
||||
// 'failure'
|
||||
```
|
||||
|
||||
Arguments:
|
||||
## `serializeAMimeType(mimeType)`
|
||||
|
||||
* **input** `string`
|
||||
<!-- YAML
|
||||
added: v5.11.0
|
||||
-->
|
||||
|
||||
Returns: `MIMEType|'failure'`
|
||||
* `mimeType` {Object} A MIME type record, as returned by [`parseMIMEType()`][].
|
||||
* `type` {string} The type.
|
||||
* `subtype` {string} The subtype.
|
||||
* `parameters` {Map<string, string>} The parameters to serialize, in
|
||||
insertion order.
|
||||
* `essence` {string} The MIME type essence, `` `${type}/${subtype}` ``.
|
||||
* Returns: {string} The serialized MIME type.
|
||||
|
||||
## `serializeAMimeType(input)`
|
||||
Implements the WHATWG [serialize a MIME type][] algorithm. The essence is
|
||||
emitted followed by each parameter as `;name=value`. A parameter value that
|
||||
contains characters outside the HTTP token set is quoted, and any `"` or `\`
|
||||
characters within it are escaped.
|
||||
|
||||
Implements [serialize a MIME type](https://mimesniff.spec.whatwg.org/#serialize-a-mime-type).
|
||||
|
||||
Serializes a MIMEType object.
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { serializeAMimeType } from 'undici'
|
||||
|
||||
serializeAMimeType({
|
||||
@@ -46,12 +88,10 @@ serializeAMimeType({
|
||||
parameters: new Map([['charset', 'gbk']]),
|
||||
essence: 'text/html'
|
||||
})
|
||||
// text/html;charset=gbk
|
||||
|
||||
// 'text/html;charset=gbk'
|
||||
```
|
||||
|
||||
Arguments:
|
||||
|
||||
* **mimeType** `MIMEType`
|
||||
|
||||
Returns: `string`
|
||||
[MIME Sniffing Standard]: https://mimesniff.spec.whatwg.org/
|
||||
[`parseMIMEType()`]: #parsemimetypeinput
|
||||
[parse a MIME type]: https://mimesniff.spec.whatwg.org/#parse-a-mime-type
|
||||
[serialize a MIME type]: https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
|
||||
+120
-69
@@ -1,44 +1,58 @@
|
||||
# Cookie Handling
|
||||
# Cookies
|
||||
|
||||
## `Cookie` interface
|
||||
<!--introduced_in=v5.15.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/web/cookies/index.js -->
|
||||
|
||||
* **name** `string`
|
||||
* **value** `string`
|
||||
* **expires** `Date|number` (optional)
|
||||
* **maxAge** `number` (optional)
|
||||
* **domain** `string` (optional)
|
||||
* **path** `string` (optional)
|
||||
* **secure** `boolean` (optional)
|
||||
* **httpOnly** `boolean` (optional)
|
||||
* **sameSite** `'Strict'|'Lax'|'None'` (optional)
|
||||
* **unparsed** `string[]` (optional) Left over attributes that weren't parsed.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `deleteCookie(headers, name[, attributes])`
|
||||
A set of helpers for reading and writing HTTP cookies on a [`Headers`][]
|
||||
instance, following the [RFC 6265bis][] cookie parsing and serialization
|
||||
algorithm. They operate on the `Cookie` request header and the `Set-Cookie`
|
||||
response header.
|
||||
|
||||
Sets the expiry time of the cookie to the unix epoch, causing browsers to delete it when received.
|
||||
|
||||
```js
|
||||
import { deleteCookie, Headers } from 'undici'
|
||||
|
||||
const headers = new Headers()
|
||||
deleteCookie(headers, 'name')
|
||||
|
||||
console.log(headers.get('set-cookie')) // name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT
|
||||
```mjs
|
||||
import { getCookies, setCookie, deleteCookie, getSetCookies, parseCookie } from 'undici'
|
||||
```
|
||||
|
||||
Arguments:
|
||||
These functions do not manage a cookie jar or perform any network activity; they
|
||||
only read from and mutate the supplied `Headers` object.
|
||||
|
||||
* **headers** `Headers`
|
||||
* **name** `string`
|
||||
* **attributes** `{ path?: string, domain?: string }` (optional)
|
||||
## `Cookie`
|
||||
|
||||
Returns: `void`
|
||||
The `Cookie` object describes a single cookie and its attributes. It is the
|
||||
return shape of [`parseCookie()`][] and [`getSetCookies()`][], and the input
|
||||
shape accepted by [`setCookie()`][].
|
||||
|
||||
* `name` {string} The cookie name.
|
||||
* `value` {string} The cookie value.
|
||||
* `expires` {Date|number} The cookie expiry time, as a `Date` or a Unix
|
||||
timestamp in seconds. (optional)
|
||||
* `maxAge` {number} The number of seconds until the cookie expires. (optional)
|
||||
* `domain` {string} The hosts the cookie is sent to. (optional)
|
||||
* `path` {string} The URL path that must exist for the cookie to be sent.
|
||||
(optional)
|
||||
* `secure` {boolean} Whether the cookie is only sent over HTTPS. (optional)
|
||||
* `httpOnly` {boolean} Whether the cookie is inaccessible to client-side
|
||||
JavaScript. (optional)
|
||||
* `sameSite` {string} Controls whether the cookie is sent with cross-site
|
||||
requests. One of `'Strict'`, `'Lax'`, or `'None'`. (optional)
|
||||
* `unparsed` {string[]} Any attributes that were present in the `Set-Cookie`
|
||||
header but not recognized, each formatted as `` `name=value` ``. (optional)
|
||||
|
||||
## `getCookies(headers)`
|
||||
|
||||
Parses the `Cookie` header and returns a list of attributes and values.
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
```js
|
||||
* `headers` {Headers} The `Headers` to read the `Cookie` header from.
|
||||
* Returns: {Record<string, string>} A map of cookie names to values.
|
||||
|
||||
Parses the `Cookie` header of `headers` and returns its name/value pairs as a
|
||||
plain object. An empty object is returned when no `Cookie` header is present.
|
||||
|
||||
```mjs
|
||||
import { getCookies, Headers } from 'undici'
|
||||
|
||||
const headers = new Headers({
|
||||
@@ -48,17 +62,48 @@ const headers = new Headers({
|
||||
console.log(getCookies(headers)) // { get: 'cookies', and: 'attributes' }
|
||||
```
|
||||
|
||||
Arguments:
|
||||
## `deleteCookie(headers, name[, attributes])`
|
||||
|
||||
* **headers** `Headers`
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
Returns: `Record<string, string>`
|
||||
* `headers` {Headers} The `Headers` to append the expiring cookie to.
|
||||
* `name` {string} The name of the cookie to delete.
|
||||
* `attributes` {Object} (optional)
|
||||
* `path` {string} The path the cookie was scoped to. **Default:** `null`.
|
||||
* `domain` {string} The domain the cookie was scoped to. **Default:** `null`.
|
||||
* Returns: {undefined}
|
||||
|
||||
Appends a `Set-Cookie` header to `headers` that expires the named cookie by
|
||||
setting its expiry time to the Unix epoch, causing a client to delete it on
|
||||
receipt. The optional `path` and `domain` must match those of the cookie being
|
||||
removed.
|
||||
|
||||
```mjs
|
||||
import { deleteCookie, Headers } from 'undici'
|
||||
|
||||
const headers = new Headers()
|
||||
deleteCookie(headers, 'name')
|
||||
|
||||
console.log(headers.get('set-cookie')) // name=; Expires=Thu, 01 Jan 1970 00:00:00 GMT
|
||||
```
|
||||
|
||||
## `getSetCookies(headers)`
|
||||
|
||||
Parses all `Set-Cookie` headers.
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
```js
|
||||
* `headers` {Headers} The `Headers` to read the `Set-Cookie` headers from.
|
||||
* Returns: {Cookie[]} The parsed cookies, or an empty array when none are
|
||||
present.
|
||||
|
||||
Parses every `Set-Cookie` header of `headers` and returns the resulting
|
||||
[`Cookie`][] objects. A header that fails to parse yields a `null` entry at its
|
||||
position in the returned array.
|
||||
|
||||
```mjs
|
||||
import { getSetCookies, Headers } from 'undici'
|
||||
|
||||
const headers = new Headers({ 'set-cookie': 'undici=getSetCookies; Secure' })
|
||||
@@ -71,20 +116,45 @@ console.log(getSetCookies(headers))
|
||||
// secure: true
|
||||
// }
|
||||
// ]
|
||||
|
||||
```
|
||||
|
||||
Arguments:
|
||||
## `setCookie(headers, cookie)`
|
||||
|
||||
* **headers** `Headers`
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
Returns: `Cookie[]`
|
||||
* `headers` {Headers} The `Headers` to append the `Set-Cookie` header to.
|
||||
* `cookie` {Cookie} The cookie to serialize. See [`Cookie`][].
|
||||
* Returns: {undefined}
|
||||
|
||||
Serializes `cookie` and appends it to the `Set-Cookie` header of `headers`. When
|
||||
the cookie name is empty, serialization yields no value and no header is
|
||||
appended.
|
||||
|
||||
```mjs
|
||||
import { setCookie, Headers } from 'undici'
|
||||
|
||||
const headers = new Headers()
|
||||
setCookie(headers, { name: 'undici', value: 'setCookie' })
|
||||
|
||||
console.log(headers.get('Set-Cookie')) // undici=setCookie
|
||||
```
|
||||
|
||||
## `parseCookie(cookie)`
|
||||
|
||||
Parses a single `Set-Cookie` header value into a `Cookie` object.
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
```js
|
||||
* `cookie` {string} A `Set-Cookie` header value.
|
||||
* Returns: {Cookie|null} The parsed cookie, or `null` if `cookie` is invalid.
|
||||
|
||||
Parses a single `Set-Cookie` header value into a [`Cookie`][] object. `null` is
|
||||
returned when the value contains control characters or otherwise cannot be
|
||||
parsed.
|
||||
|
||||
```mjs
|
||||
import { parseCookie } from 'undici'
|
||||
|
||||
console.log(parseCookie('undici=getSetCookies; Secure; SameSite=Lax'))
|
||||
@@ -96,33 +166,14 @@ console.log(parseCookie('undici=getSetCookies; Secure; SameSite=Lax'))
|
||||
// }
|
||||
```
|
||||
|
||||
Notes:
|
||||
The cookie value is returned exactly as it appears in the header; percent-encoded
|
||||
sequences such as `%20` or `%0D%0A` are not decoded. The `sameSite` attribute is
|
||||
only set when the value is a case-insensitive match for `Strict`, `Lax`, or
|
||||
`None`.
|
||||
|
||||
* The cookie value is returned as it appears in the header. Percent-encoded sequences such as `%20` or `%0D%0A` are **not** decoded.
|
||||
* `sameSite` is only set for exact case-insensitive matches of `Strict`, `Lax`, or `None`.
|
||||
|
||||
Arguments:
|
||||
|
||||
* **cookie** `string`
|
||||
|
||||
Returns: `Cookie | null`
|
||||
|
||||
## `setCookie(headers, cookie)`
|
||||
|
||||
Appends a cookie to the `Set-Cookie` header.
|
||||
|
||||
```js
|
||||
import { setCookie, Headers } from 'undici'
|
||||
|
||||
const headers = new Headers()
|
||||
setCookie(headers, { name: 'undici', value: 'setCookie' })
|
||||
|
||||
console.log(headers.get('Set-Cookie')) // undici=setCookie
|
||||
```
|
||||
|
||||
Arguments:
|
||||
|
||||
* **headers** `Headers`
|
||||
* **cookie** `Cookie`
|
||||
|
||||
Returns: `void`
|
||||
[RFC 6265bis]: https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis
|
||||
[`Cookie`]: #cookie
|
||||
[`Headers`]: https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
[`getSetCookies()`]: #getsetcookiesheaders
|
||||
[`parseCookie()`]: #parsecookiecookie
|
||||
[`setCookie()`]: #setcookieheaders-cookie
|
||||
+34
-18
@@ -1,16 +1,29 @@
|
||||
# Debug
|
||||
|
||||
Undici (and subsenquently `fetch` and `websocket`) exposes a debug statement that can be enabled by setting `NODE_DEBUG` within the environment.
|
||||
<!--type=misc-->
|
||||
|
||||
The flags available are:
|
||||
Undici emits human-readable debug logs through Node.js' built-in
|
||||
[`util.debuglog()`][]. The logs are disabled by default and are turned on by
|
||||
listing one or more namespaces in the `NODE_DEBUG` environment variable. Because
|
||||
`util.debuglog()` reads `NODE_DEBUG` once, when the namespace is first used, the
|
||||
variable must be set before the process starts.
|
||||
|
||||
Each log line is prefixed with the namespace (uppercased) and the process id,
|
||||
for example `UNDICI 16241: connecting to nodejs.org using https:h1`. Multiple
|
||||
namespaces can be enabled at once by separating them with commas, for example
|
||||
`NODE_DEBUG=undici,websocket`.
|
||||
|
||||
The following sections describe the namespaces exposed by undici.
|
||||
|
||||
## `undici`
|
||||
|
||||
This flag enables debug statements for the core undici library.
|
||||
|
||||
```sh
|
||||
NODE_DEBUG=undici node script.js
|
||||
Enables debug logs for the core undici library, covering connection setup,
|
||||
request dispatch, responses, and trailers. The `fetch` and `websocket` APIs are
|
||||
built on top of this core, so enabling `undici` also surfaces activity that
|
||||
originates from them.
|
||||
|
||||
```console
|
||||
$ NODE_DEBUG=undici node script.js
|
||||
UNDICI 16241: connecting to nodejs.org using https:h1
|
||||
UNDICI 16241: connecting to nodejs.org using https:h1
|
||||
UNDICI 16241: connected to nodejs.org using https:h1
|
||||
@@ -26,13 +39,12 @@ UNDICI 16241: trailers received from GET https://nodejs.org/en
|
||||
|
||||
## `fetch`
|
||||
|
||||
This flag enables debug statements for the `fetch` API.
|
||||
|
||||
> **Note**: statements are pretty similar to the ones in the `undici` flag, but scoped to `fetch`
|
||||
|
||||
```sh
|
||||
NODE_DEBUG=fetch node script.js
|
||||
Enables debug logs scoped to the `fetch` API. The output mirrors the `undici`
|
||||
namespace but is emitted only for requests made through [`fetch()`][], which
|
||||
makes it useful for isolating `fetch` traffic from other undici activity.
|
||||
|
||||
```console
|
||||
$ NODE_DEBUG=fetch node script.js
|
||||
FETCH 16241: connecting to nodejs.org using https:h1
|
||||
FETCH 16241: connecting to nodejs.org using https:h1
|
||||
FETCH 16241: connected to nodejs.org using https:h1
|
||||
@@ -48,15 +60,19 @@ FETCH 16241: trailers received from GET https://nodejs.org/en
|
||||
|
||||
## `websocket`
|
||||
|
||||
This flag enables debug statements for the `Websocket` API.
|
||||
|
||||
> **Note**: statements can overlap with `UNDICI` ones if `undici` or `fetch` flag has been enabled as well.
|
||||
|
||||
```sh
|
||||
NODE_DEBUG=websocket node script.js
|
||||
Enables debug logs scoped to the [`WebSocket`][] API, covering the opening
|
||||
handshake and connection lifecycle. When the `undici` or `fetch` namespace is
|
||||
also enabled, some lines may appear under both namespaces because the handshake
|
||||
is performed through the core library.
|
||||
|
||||
```console
|
||||
$ NODE_DEBUG=websocket node script.js
|
||||
WEBSOCKET 18309: connecting to echo.websocket.org using https:h1
|
||||
WEBSOCKET 18309: connected to echo.websocket.org using https:h1
|
||||
WEBSOCKET 18309: sending request to GET https://echo.websocket.org/
|
||||
WEBSOCKET 18309: connection opened <ip_address>
|
||||
```
|
||||
|
||||
[`WebSocket`]: WebSocket.md#class-websocket
|
||||
[`fetch()`]: Fetch.md
|
||||
[`util.debuglog()`]: https://nodejs.org/api/util.html#utildebuglogsection-callback
|
||||
+362
-162
@@ -1,306 +1,497 @@
|
||||
# Diagnostics Channel Support
|
||||
|
||||
Stability: Experimental.
|
||||
<!--introduced_in=v6.3.0-->
|
||||
<!--type=misc-->
|
||||
|
||||
Undici supports the [`diagnostics_channel`](https://nodejs.org/api/diagnostics_channel.html) (currently available only on Node.js v16+).
|
||||
It is the preferred way to instrument Undici and retrieve internal information.
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
The channels available are the following.
|
||||
Undici is instrumented with Node.js' built-in [`diagnostics_channel`][] module.
|
||||
It is the preferred way to observe undici's internal behaviour without patching
|
||||
the library, and it backs the human-readable [debug logs][Debug].
|
||||
|
||||
## `undici:request:create`
|
||||
Each integration point is exposed as a named channel. To observe an event,
|
||||
obtain the channel by name and subscribe to it:
|
||||
|
||||
This message is published when a new outgoing request is created.
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
|
||||
console.log(request.method, request.origin, request.path)
|
||||
})
|
||||
```
|
||||
|
||||
```cjs
|
||||
const diagnosticsChannel = require('node:diagnostics_channel')
|
||||
|
||||
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
|
||||
console.log(request.method, request.origin, request.path)
|
||||
})
|
||||
```
|
||||
|
||||
The message published to a channel is always a single object whose shape depends
|
||||
on the channel. The sections below describe every channel and the object it
|
||||
publishes. Subscriber callbacks run synchronously inside undici, so they should
|
||||
be fast and must not throw.
|
||||
|
||||
The same `request` object is shared across all `undici:request:*` channels for a
|
||||
given request, so a subscriber may correlate the lifecycle of one request by
|
||||
identity.
|
||||
|
||||
## Event: `'undici:request:create'`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
Published when a new outgoing request is created, before it is dispatched.
|
||||
|
||||
* `message` {Object}
|
||||
* `request` {Object} The request being created.
|
||||
* `origin` {string|URL} The origin the request is sent to.
|
||||
* `completed` {boolean} Whether the request has completed. `false` at this
|
||||
point.
|
||||
* `method` {string} The HTTP method, e.g. `'GET'`.
|
||||
* `path` {string} The request path, including any query string.
|
||||
* `headers` {string[]} The request headers as a flat array of strings,
|
||||
alternating between header name and value.
|
||||
|
||||
The `request` object also exposes an `addHeader(name, value)` method that
|
||||
appends a header before the request is sent.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
|
||||
console.log('origin', request.origin)
|
||||
console.log('completed', request.completed)
|
||||
console.log('method', request.method)
|
||||
console.log('path', request.path)
|
||||
console.log('headers', request.headers) // array of strings, e.g: ['foo', 'bar']
|
||||
console.log('headers', request.headers)
|
||||
request.addHeader('hello', 'world')
|
||||
console.log('headers', request.headers) // e.g. ['foo', 'bar', 'hello', 'world']
|
||||
})
|
||||
```
|
||||
|
||||
Note: a request is only loosely completed to a given socket.
|
||||
A request is only loosely bound to a given socket at this stage.
|
||||
|
||||
## `undici:request:bodyChunkSent`
|
||||
## Event: `'undici:request:bodyChunkSent'`
|
||||
|
||||
This message is published when a chunk of the request body is being sent.
|
||||
<!-- YAML
|
||||
added: v7.11.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published each time a chunk of the request body is written to the socket.
|
||||
|
||||
* `message` {Object}
|
||||
* `request` {Object} The same object published by
|
||||
[`'undici:request:create'`][].
|
||||
* `chunk` {Uint8Array|string} The body chunk being sent.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:bodyChunkSent').subscribe(({ request, chunk }) => {
|
||||
// request is the same object undici:request:create
|
||||
console.log('sent', chunk.length, 'bytes')
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:request:bodySent`
|
||||
## Event: `'undici:request:bodySent'`
|
||||
|
||||
This message is published after the request body has been fully sent.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after the request body has been fully sent.
|
||||
|
||||
* `message` {Object}
|
||||
* `request` {Object} The same object published by
|
||||
[`'undici:request:create'`][].
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:bodySent').subscribe(({ request }) => {
|
||||
// request is the same object undici:request:create
|
||||
// request is the same object as in 'undici:request:create'
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:request:headers`
|
||||
## Event: `'undici:request:headers'`
|
||||
|
||||
This message is published after the response headers have been received.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after the response headers have been received.
|
||||
|
||||
* `message` {Object}
|
||||
* `request` {Object} The same object published by
|
||||
[`'undici:request:create'`][].
|
||||
* `response` {Object} The response being received.
|
||||
* `statusCode` {number} The HTTP status code.
|
||||
* `statusText` {string} The HTTP status message.
|
||||
* `headers` {Buffer[]} The raw response headers as an array of buffers,
|
||||
alternating between header name and value.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:headers').subscribe(({ request, response }) => {
|
||||
// request is the same object undici:request:create
|
||||
console.log('statusCode', response.statusCode)
|
||||
console.log(response.statusText)
|
||||
// response.headers are buffers.
|
||||
console.log(response.headers.map((x) => x.toString()))
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:request:bodyChunkReceived`
|
||||
## Event: `'undici:request:bodyChunkReceived'`
|
||||
|
||||
This message is published after a chunk of the response body has been received.
|
||||
<!-- YAML
|
||||
added: v7.11.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published each time a chunk of the response body is received.
|
||||
|
||||
* `message` {Object}
|
||||
* `request` {Object} The same object published by
|
||||
[`'undici:request:create'`][].
|
||||
* `chunk` {Buffer} The body chunk that was received.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:bodyChunkReceived').subscribe(({ request, chunk }) => {
|
||||
// request is the same object undici:request:create
|
||||
console.log('received', chunk.length, 'bytes')
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:request:trailers`
|
||||
## Event: `'undici:request:trailers'`
|
||||
|
||||
This message is published after the response body and trailers have been received, i.e. the response has been completed.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after the response body and trailers have been received, that is, once
|
||||
the response has fully completed.
|
||||
|
||||
* `message` {Object}
|
||||
* `request` {Object} The same object published by
|
||||
[`'undici:request:create'`][]. `request.completed` is now `true`.
|
||||
* `trailers` {Buffer[]} The raw response trailers as an array of buffers,
|
||||
alternating between header name and value.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:trailers').subscribe(({ request, trailers }) => {
|
||||
// request is the same object undici:request:create
|
||||
console.log('completed', request.completed)
|
||||
// trailers are buffers.
|
||||
console.log(trailers.map((x) => x.toString()))
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:request:error`
|
||||
## Event: `'undici:request:error'`
|
||||
|
||||
This message is published if the request is going to error, but it has not errored yet.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published when the request is about to error, before the error is surfaced to
|
||||
the caller.
|
||||
|
||||
* `message` {Object}
|
||||
* `request` {Object} The same object published by
|
||||
[`'undici:request:create'`][].
|
||||
* `error` {Error} The error the request is about to fail with.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:error').subscribe(({ request, error }) => {
|
||||
// request is the same object undici:request:create
|
||||
console.error(error)
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:client:sendHeaders`
|
||||
## Event: `'undici:client:sendHeaders'`
|
||||
|
||||
This message is published right before the first byte of the request is written to the socket.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
changes:
|
||||
- version: v7.0.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/3585
|
||||
description: Refactor the diagnostics channel internals.
|
||||
-->
|
||||
|
||||
*Note*: It will publish the exact headers that will be sent to the server in raw format.
|
||||
Published immediately before the first byte of the request is written to the
|
||||
socket. The headers are published exactly as they will be sent to the server, in
|
||||
raw form.
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
* `message` {Object}
|
||||
* `request` {Object} The same object published by
|
||||
[`'undici:request:create'`][].
|
||||
* `headers` {string} The request headers as a single raw string, with lines
|
||||
separated by `\r\n`.
|
||||
* `socket` {net.Socket} The socket the request is written to.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:client:sendHeaders').subscribe(({ request, headers, socket }) => {
|
||||
// request is the same object undici:request:create
|
||||
console.log(`Full headers list ${headers.split('\r\n')}`);
|
||||
console.log(`Full headers list: ${headers.split('\r\n')}`)
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:client:beforeConnect`
|
||||
## Event: `'undici:client:beforeConnect'`
|
||||
|
||||
This message is published before creating a new connection for **any** request.
|
||||
You can not assume that this event is related to any specific request.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
changes:
|
||||
- version: v7.0.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/3585
|
||||
description: Refactor the diagnostics channel internals.
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published before a new connection is opened for any request. This event cannot be
|
||||
attributed to a specific request, because connections are pooled and shared.
|
||||
|
||||
* `message` {Object}
|
||||
* `connectParams` {Object} The parameters used to open the connection.
|
||||
* `host` {string} The connection host, including the port if present.
|
||||
* `hostname` {string} The connection hostname.
|
||||
* `protocol` {string} The protocol, e.g. `'https:'`.
|
||||
* `port` {string} The connection port.
|
||||
* `servername` {string|null} The TLS server name, or `null`.
|
||||
* `connector` {Function} The function that creates the socket.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:client:beforeConnect').subscribe(({ connectParams, connector }) => {
|
||||
// const { host, hostname, protocol, port, servername, version } = connectParams
|
||||
// connector is a function that creates the socket
|
||||
const { host, hostname, protocol, port, servername } = connectParams
|
||||
// connector is the function that creates the socket
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:client:connected`
|
||||
## Event: `'undici:client:connected'`
|
||||
|
||||
This message is published after a connection is established.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
changes:
|
||||
- version: v7.0.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/3585
|
||||
description: Refactor the diagnostics channel internals.
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after a connection has been successfully established.
|
||||
|
||||
* `message` {Object}
|
||||
* `socket` {net.Socket} The newly connected socket.
|
||||
* `connectParams` {Object} The parameters used to open the connection. See
|
||||
[`'undici:client:beforeConnect'`][] for the field descriptions.
|
||||
* `connector` {Function} The function that created the socket.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:client:connected').subscribe(({ socket, connectParams, connector }) => {
|
||||
// const { host, hostname, protocol, port, servername, version } = connectParams
|
||||
// connector is a function that creates the socket
|
||||
const { host, hostname, protocol, port, servername } = connectParams
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:client:connectError`
|
||||
## Event: `'undici:client:connectError'`
|
||||
|
||||
This message is published if it did not succeed to create new connection
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
changes:
|
||||
- version: v7.0.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/3585
|
||||
description: Refactor the diagnostics channel internals.
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published when a connection could not be established.
|
||||
|
||||
* `message` {Object}
|
||||
* `error` {Error} The error that prevented the connection.
|
||||
* `socket` {net.Socket} The socket associated with the failed attempt.
|
||||
* `connectParams` {Object} The parameters used to open the connection. See
|
||||
[`'undici:client:beforeConnect'`][] for the field descriptions.
|
||||
* `connector` {Function} The function that created the socket.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:client:connectError').subscribe(({ error, socket, connectParams, connector }) => {
|
||||
// const { host, hostname, protocol, port, servername, version } = connectParams
|
||||
// connector is a function that creates the socket
|
||||
console.log(`Connect failed with ${error.message}`)
|
||||
console.error(`Connect failed with ${error.message}`)
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:websocket:open`
|
||||
## Event: `'undici:websocket:open'`
|
||||
|
||||
This message is published after the client has successfully connected to a server.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after a [`WebSocket`][] has successfully connected to a server.
|
||||
|
||||
diagnosticsChannel.channel('undici:websocket:open').subscribe(({
|
||||
address, // { address: string, family: string, port: number }
|
||||
protocol, // string - negotiated subprotocol
|
||||
extensions, // string - negotiated extensions
|
||||
websocket, // WebSocket - the WebSocket instance
|
||||
handshakeResponse // object - HTTP response that upgraded the connection
|
||||
* `message` {Object}
|
||||
* `address` {Object|null} The remote socket address, as returned by
|
||||
[`socket.address()`][], or `null` if it cannot be determined. When present,
|
||||
it contains `address`, `family`, and `port`.
|
||||
* `protocol` {string} The negotiated subprotocol.
|
||||
* `extensions` {string} The negotiated extensions.
|
||||
* `websocket` {WebSocket} The `WebSocket` instance.
|
||||
* `handshakeResponse` {Object} The HTTP response that upgraded the connection.
|
||||
* `status` {number} The HTTP status code: `101` for an HTTP/1.1 upgrade, or
|
||||
`200` for an HTTP/2 extended `CONNECT`.
|
||||
* `statusText` {string} The HTTP status message: `'Switching Protocols'` for
|
||||
HTTP/1.1, and commonly `'OK'` for HTTP/2.
|
||||
* `headers` {Object} The response headers from the server. The `upgrade` and
|
||||
`connection` headers are only present for HTTP/1.1 handshakes.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:websocket:open').subscribe(({
|
||||
address,
|
||||
protocol,
|
||||
extensions,
|
||||
websocket,
|
||||
handshakeResponse
|
||||
}) => {
|
||||
console.log(address) // address, family, and port
|
||||
console.log(protocol) // negotiated subprotocols
|
||||
console.log(extensions) // negotiated extensions
|
||||
console.log(websocket) // the WebSocket instance
|
||||
|
||||
// Handshake response details
|
||||
console.log(handshakeResponse.status) // 101 for HTTP/1.1, 200 for HTTP/2 extended CONNECT
|
||||
console.log(handshakeResponse.statusText) // 'Switching Protocols' for HTTP/1.1, commonly 'OK' for HTTP/2 in Node.js
|
||||
console.log(handshakeResponse.headers) // Object containing response headers
|
||||
console.log(address)
|
||||
console.log(protocol)
|
||||
console.log(extensions)
|
||||
console.log(handshakeResponse.status)
|
||||
console.log(handshakeResponse.headers)
|
||||
})
|
||||
```
|
||||
|
||||
### Handshake Response Object
|
||||
## Event: `'undici:websocket:close'`
|
||||
|
||||
The `handshakeResponse` object contains the HTTP response that established the WebSocket connection:
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
- `status` (number): The HTTP status code (`101` for HTTP/1.1 upgrade, `200` for HTTP/2 extended CONNECT)
|
||||
- `statusText` (string): The HTTP status message (`'Switching Protocols'` for HTTP/1.1, commonly `'OK'` for HTTP/2 in Node.js)
|
||||
- `headers` (object): The HTTP response headers from the server, including:
|
||||
- `sec-websocket-accept` and other WebSocket-related headers
|
||||
- `upgrade: 'websocket'`
|
||||
- `connection: 'upgrade'`
|
||||
Published after the [`WebSocket`][] connection has closed.
|
||||
|
||||
The `upgrade` and `connection` headers are only present for HTTP/1.1 handshakes.
|
||||
* `message` {Object}
|
||||
* `websocket` {WebSocket} The `WebSocket` instance.
|
||||
* `code` {number} The closing status code.
|
||||
* `reason` {string} The closing reason.
|
||||
|
||||
This information is particularly useful for debugging and monitoring WebSocket connections, as it provides access to the initial HTTP handshake response that established the WebSocket connection.
|
||||
|
||||
## `undici:websocket:close`
|
||||
|
||||
This message is published after the connection has closed.
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:websocket:close').subscribe(({ websocket, code, reason }) => {
|
||||
console.log(websocket) // the WebSocket instance
|
||||
console.log(code) // the closing status code
|
||||
console.log(reason) // the closing reason
|
||||
console.log(code, reason)
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:websocket:socket_error`
|
||||
## Event: `'undici:websocket:socket_error'`
|
||||
|
||||
This message is published if the socket experiences an error.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published when the underlying WebSocket socket experiences an error. Unlike the
|
||||
other WebSocket channels, the published message is the error itself.
|
||||
|
||||
* `error` {Error} The socket error.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:websocket:socket_error').subscribe((error) => {
|
||||
console.log(error)
|
||||
console.error(error)
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:websocket:ping`
|
||||
## Event: `'undici:websocket:ping'`
|
||||
|
||||
This message is published after the client receives a ping frame, if the connection is not closing.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after the client receives a ping frame.
|
||||
|
||||
* `message` {Object}
|
||||
* `payload` {Buffer|undefined} The optional application data carried by the
|
||||
frame.
|
||||
* `websocket` {WebSocket} The `WebSocket` instance.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:websocket:ping').subscribe(({ payload, websocket }) => {
|
||||
// a Buffer or undefined, containing the optional application data of the frame
|
||||
console.log(payload)
|
||||
console.log(websocket) // the WebSocket instance
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:websocket:pong`
|
||||
## Event: `'undici:websocket:pong'`
|
||||
|
||||
This message is published after the client receives a pong frame.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after the client receives a pong frame.
|
||||
|
||||
* `message` {Object}
|
||||
* `payload` {Buffer|undefined} The optional application data carried by the
|
||||
frame.
|
||||
* `websocket` {WebSocket} The `WebSocket` instance.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:websocket:pong').subscribe(({ payload, websocket }) => {
|
||||
// a Buffer or undefined, containing the optional application data of the frame
|
||||
console.log(payload)
|
||||
console.log(websocket) // the WebSocket instance
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:proxy:connected`
|
||||
## Event: `'undici:proxy:connected'`
|
||||
|
||||
This message is published after the `ProxyAgent` establishes a connection to the proxy server.
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
Published after a [`ProxyAgent`][] has established a tunnel to the proxy server.
|
||||
|
||||
* `message` {Object}
|
||||
* `socket` {net.Socket} The socket connected to the proxy.
|
||||
* `connectParams` {Object} The parameters used to establish the tunnel.
|
||||
* `origin` {string|URL} The proxy origin.
|
||||
* `port` {string} The proxy port.
|
||||
* `path` {string} The requested tunnel target, e.g. `'example.com:443'`.
|
||||
* `signal` {AbortSignal|undefined} The signal associated with the request.
|
||||
* `headers` {Object} The headers sent in the `CONNECT` request.
|
||||
* `servername` {string|undefined} The TLS server name used for the proxy.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:proxy:connected').subscribe(({ socket, connectParams }) => {
|
||||
console.log(socket)
|
||||
console.log(connectParams)
|
||||
// const { origin, port, path, signal, headers, servername } = connectParams
|
||||
const { origin, port, path, signal, headers, servername } = connectParams
|
||||
})
|
||||
```
|
||||
|
||||
## `undici:request:pending-requests`
|
||||
## Event: `'undici:request:pending-requests'`
|
||||
|
||||
This message is published when the deduplicate interceptor's pending request map changes. This is useful for monitoring and debugging request deduplication behavior.
|
||||
<!-- YAML
|
||||
added: v6.3.0
|
||||
-->
|
||||
|
||||
The deduplicate interceptor automatically deduplicates concurrent requests for the same resource. When multiple identical requests are made while one is already in-flight, only one request is sent to the origin server, and all waiting handlers receive the same response.
|
||||
Published when the [`dedupe`][] interceptor's set of pending requests changes.
|
||||
The interceptor coalesces concurrent identical requests so that only one reaches
|
||||
the origin; this channel exposes that bookkeeping for monitoring and debugging.
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
* `message` {Object}
|
||||
* `type` {string} Either `'added'` when a new pending request is registered, or
|
||||
`'removed'` when a pending request completes, whether successfully or with an
|
||||
error.
|
||||
* `size` {number} The number of pending requests after the change.
|
||||
* `key` {string} The deduplication key for the request, composed of the origin,
|
||||
method, path, and request headers.
|
||||
|
||||
```mjs
|
||||
import diagnosticsChannel from 'node:diagnostics_channel'
|
||||
|
||||
diagnosticsChannel.channel('undici:request:pending-requests').subscribe(({ type, size, key }) => {
|
||||
console.log(type) // 'added' or 'removed'
|
||||
console.log(size) // current number of pending requests
|
||||
console.log(key) // the deduplication key for this request
|
||||
})
|
||||
```
|
||||
|
||||
### Event Properties
|
||||
|
||||
- `type` (`string`): Either `'added'` when a new pending request is registered, or `'removed'` when a pending request completes (successfully or with an error).
|
||||
- `size` (`number`): The current number of pending requests after the change.
|
||||
- `key` (`string`): The deduplication key for the request, composed of the origin, method, path, and request headers.
|
||||
|
||||
### Example: Monitoring Request Deduplication
|
||||
|
||||
```js
|
||||
import diagnosticsChannel from 'diagnostics_channel'
|
||||
|
||||
const channel = diagnosticsChannel.channel('undici:request:pending-requests')
|
||||
|
||||
channel.subscribe(({ type, size, key }) => {
|
||||
if (type === 'added') {
|
||||
console.log(`New pending request: ${key} (${size} total pending)`)
|
||||
} else {
|
||||
@@ -309,7 +500,16 @@ channel.subscribe(({ type, size, key }) => {
|
||||
})
|
||||
```
|
||||
|
||||
This can be useful for:
|
||||
- Verifying that request deduplication is working as expected
|
||||
- Monitoring the number of concurrent in-flight requests
|
||||
- Debugging deduplication behavior in production environments
|
||||
This is useful for verifying that deduplication is working, monitoring the number
|
||||
of concurrent in-flight requests, and debugging deduplication behaviour in
|
||||
production.
|
||||
|
||||
[`'undici:client:beforeConnect'`]: #event-undiciclientbeforeconnect
|
||||
[`'undici:request:create'`]: #event-undicirequestcreate
|
||||
[`'undici:request:headers'`]: #event-undicirequestheaders
|
||||
[`ProxyAgent`]: ProxyAgent.md#class-proxyagent
|
||||
[`WebSocket`]: WebSocket.md#class-websocket
|
||||
[`dedupe`]: Dispatcher.md#dedupe
|
||||
[`diagnostics_channel`]: https://nodejs.org/api/diagnostics_channel.html
|
||||
[`socket.address()`]: https://nodejs.org/api/net.html#socketaddress
|
||||
[Debug]: Debug.md
|
||||
+449
-1072
File diff suppressed because it is too large.
Load diff
+97
-83
@@ -1,52 +1,85 @@
|
||||
# Class: EnvHttpProxyAgent
|
||||
# EnvHttpProxyAgent
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v6.14.0-->
|
||||
|
||||
EnvHttpProxyAgent automatically reads the proxy configuration from the environment variables `http_proxy`, `https_proxy`, and `no_proxy` and sets up the proxy agents accordingly. When `http_proxy` and `https_proxy` are set, `http_proxy` is used for HTTP requests and `https_proxy` is used for HTTPS requests. If only `http_proxy` is set, `http_proxy` is used for both HTTP and HTTPS requests. If only `https_proxy` is set, it is only used for HTTPS requests.
|
||||
<!--type=module-->
|
||||
|
||||
`no_proxy` is a comma or space-separated list of hostnames that should not be proxied. The list may contain leading wildcard characters (`*`). If `no_proxy` is set, the EnvHttpProxyAgent will bypass the proxy for requests to hosts that match the list. If `no_proxy` is set to `"*"`, the EnvHttpProxyAgent will bypass the proxy for all requests.
|
||||
<!-- source_link=lib/dispatcher/env-http-proxy-agent.js -->
|
||||
|
||||
Uppercase environment variables are also supported: `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY`. However, if both the lowercase and uppercase environment variables are set, the uppercase environment variables will be ignored.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new EnvHttpProxyAgent([options])`
|
||||
`EnvHttpProxyAgent` is a [`Dispatcher`][] that reads its proxy configuration from
|
||||
the `http_proxy`, `https_proxy`, and `no_proxy` environment variables and routes
|
||||
requests through the appropriate proxy automatically. Import it from `undici`:
|
||||
|
||||
Arguments:
|
||||
```mjs
|
||||
import { EnvHttpProxyAgent } from 'undici'
|
||||
```
|
||||
|
||||
* **options** `EnvHttpProxyAgentOptions` (optional) - extends the `Agent` options.
|
||||
When `http_proxy` and `https_proxy` are both set, `http_proxy` is used for HTTP
|
||||
requests and `https_proxy` is used for HTTPS requests. If only `http_proxy` is
|
||||
set, it is used for both HTTP and HTTPS requests. If only `https_proxy` is set,
|
||||
it is used only for HTTPS requests.
|
||||
|
||||
Returns: `EnvHttpProxyAgent`
|
||||
`no_proxy` is a comma- or space-separated list of hosts that must not be
|
||||
proxied. Each entry may include a leading dot or `*.` wildcard (for example
|
||||
`.example.com`) to match subdomains, and an optional `:port` suffix to restrict
|
||||
the match to a specific port. A request bypasses the proxy when its host equals
|
||||
an entry or is a subdomain of one. Setting `no_proxy` to `*` bypasses the proxy
|
||||
for every request.
|
||||
|
||||
### Parameter: `EnvHttpProxyAgentOptions`
|
||||
The uppercase variants `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` are also
|
||||
honored. When both the lowercase and uppercase forms of a variable are set, the
|
||||
lowercase form takes precedence and the uppercase form is ignored. The `no_proxy`
|
||||
value is re-read from the environment on demand, so changes to it after the agent
|
||||
is created take effect on subsequent requests; this does not apply when `noProxy`
|
||||
is passed as an option.
|
||||
|
||||
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
|
||||
## Class: `EnvHttpProxyAgent`
|
||||
|
||||
* **httpProxy** `string` (optional) - When set, it will override the `HTTP_PROXY` environment variable.
|
||||
* **httpsProxy** `string` (optional) - When set, it will override the `HTTPS_PROXY` environment variable.
|
||||
* **noProxy** `string` (optional) - When set, it will override the `NO_PROXY` environment variable.
|
||||
<!-- YAML
|
||||
added: v6.14.0
|
||||
-->
|
||||
|
||||
Examples:
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
```js
|
||||
### `new EnvHttpProxyAgent([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.14.0
|
||||
-->
|
||||
|
||||
* `options` {EnvHttpProxyAgentOptions} Extends the [`ProxyAgent`][] options
|
||||
(except `uri`). (optional)
|
||||
* `httpProxy` {string} When set, overrides the `http_proxy` and `HTTP_PROXY`
|
||||
environment variables. (optional)
|
||||
* `httpsProxy` {string} When set, overrides the `https_proxy` and
|
||||
`HTTPS_PROXY` environment variables. (optional)
|
||||
* `noProxy` {string} When set, overrides the `no_proxy` and `NO_PROXY`
|
||||
environment variables. (optional)
|
||||
* Returns: {EnvHttpProxyAgent}
|
||||
|
||||
Creates a new `EnvHttpProxyAgent`. Any options other than `httpProxy`,
|
||||
`httpsProxy`, and `noProxy` are forwarded to the underlying [`Agent`][] and
|
||||
[`ProxyAgent`][] instances. Constructing the agent only configures it; it does
|
||||
not affect any request until it is registered as a dispatcher, either globally
|
||||
with `setGlobalDispatcher()` or per-request through the `dispatcher` option.
|
||||
|
||||
```mjs
|
||||
import { EnvHttpProxyAgent } from 'undici'
|
||||
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent()
|
||||
// or
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent({ httpProxy: 'my.proxy.server:8080', httpsProxy: 'my.proxy.server:8443', noProxy: 'localhost' })
|
||||
// or, overriding the environment variables explicitly:
|
||||
const envHttpProxyAgentWithOpts = new EnvHttpProxyAgent({
|
||||
httpProxy: 'my.proxy.server:8080',
|
||||
httpsProxy: 'my.proxy.server:8443',
|
||||
noProxy: 'localhost'
|
||||
})
|
||||
```
|
||||
|
||||
#### Example - EnvHttpProxyAgent instantiation
|
||||
#### Example - Proxied `fetch()` with the global dispatcher
|
||||
|
||||
This will instantiate the EnvHttpProxyAgent. It will not do anything until registered as the agent to use with requests.
|
||||
|
||||
```js
|
||||
import { EnvHttpProxyAgent } from 'undici'
|
||||
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent()
|
||||
```
|
||||
|
||||
#### Example - Basic Proxy Fetch with global agent dispatcher
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { setGlobalDispatcher, fetch, EnvHttpProxyAgent } from 'undici'
|
||||
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent()
|
||||
@@ -56,12 +89,12 @@ const response = await fetch('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', response.status) // response received 200
|
||||
|
||||
const data = await response.json() // data { foo: "bar" }
|
||||
const data = await response.json() // data { foo: 'bar' }
|
||||
```
|
||||
|
||||
#### Example - Basic Proxy Request with global agent dispatcher
|
||||
#### Example - Proxied `request()` with the global dispatcher
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { setGlobalDispatcher, request, EnvHttpProxyAgent } from 'undici'
|
||||
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent()
|
||||
@@ -76,17 +109,16 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic Proxy Request with local agent dispatcher
|
||||
#### Example - Proxied `request()` with a local dispatcher
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { EnvHttpProxyAgent, request } from 'undici'
|
||||
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent()
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent })
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo', {
|
||||
dispatcher: envHttpProxyAgent
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
@@ -95,62 +127,44 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic Proxy Fetch with local agent dispatcher
|
||||
#### Example - Proxied `fetch()` with a local dispatcher
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { EnvHttpProxyAgent, fetch } from 'undici'
|
||||
|
||||
const envHttpProxyAgent = new EnvHttpProxyAgent()
|
||||
|
||||
const response = await fetch('http://localhost:3000/foo', { dispatcher: envHttpProxyAgent })
|
||||
const response = await fetch('http://localhost:3000/foo', {
|
||||
dispatcher: envHttpProxyAgent
|
||||
})
|
||||
|
||||
console.log('response received', response.status) // response received 200
|
||||
|
||||
const data = await response.json() // data { foo: "bar" }
|
||||
const data = await response.json() // data { foo: 'bar' }
|
||||
```
|
||||
|
||||
## Instance Methods
|
||||
### `envHttpProxyAgent.dispatch(options, handler)`
|
||||
|
||||
### `EnvHttpProxyAgent.close([callback])`
|
||||
<!-- YAML
|
||||
added: v6.14.0
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
|
||||
* `options` {AgentDispatchOptions}
|
||||
* `origin` {string|URL}
|
||||
* `handler` {DispatchHandler}
|
||||
* Returns: {boolean}
|
||||
|
||||
### `EnvHttpProxyAgent.destroy([error, callback])`
|
||||
Dispatches a request through the proxy selected for `options.origin`. The origin's
|
||||
protocol, host, and port are compared against the `no_proxy` configuration: when
|
||||
the host should be proxied, the request is routed to the HTTPS proxy agent for
|
||||
`https:` origins or the HTTP proxy agent otherwise; when the host is excluded by
|
||||
`no_proxy`, the request is sent directly through a non-proxying [`Agent`][].
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
This method is not normally called directly. Use the higher-level
|
||||
[`Dispatcher`][] methods such as [`dispatcher.request()`][] or the top-level
|
||||
`request()` and `fetch()` helpers instead.
|
||||
|
||||
### `EnvHttpProxyAgent.dispatch(options, handler: AgentDispatchOptions)`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
|
||||
#### Parameter: `AgentDispatchOptions`
|
||||
|
||||
Extends: [`DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions)
|
||||
|
||||
* **origin** `string | URL`
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
|
||||
### `EnvHttpProxyAgent.connect(options[, callback])`
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
|
||||
|
||||
### `EnvHttpProxyAgent.dispatch(options, handler)`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
|
||||
### `EnvHttpProxyAgent.pipeline(options, handler)`
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
|
||||
|
||||
### `EnvHttpProxyAgent.request(options[, callback])`
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
|
||||
### `EnvHttpProxyAgent.stream(options, factory[, callback])`
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
|
||||
|
||||
### `EnvHttpProxyAgent.upgrade(options[, callback])`
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
||||
[`Agent`]: Agent.md#class-agent
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`ProxyAgent`]: ProxyAgent.md#class-proxyagent
|
||||
[`dispatcher.request()`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
+498
-35
@@ -1,49 +1,309 @@
|
||||
# Errors
|
||||
|
||||
Undici exposes a variety of error objects that you can use to enhance your error handling.
|
||||
You can find all the error objects inside the `errors` key.
|
||||
<!--introduced_in=v1.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/core/errors.js -->
|
||||
|
||||
> Stability: 2 - Stable
|
||||
|
||||
undici raises typed error objects so that failures can be distinguished
|
||||
programmatically. Every error class is exposed through the `errors` namespace of
|
||||
the package:
|
||||
|
||||
```js
|
||||
import { errors } from 'undici'
|
||||
|
||||
if (err instanceof errors.ConnectTimeoutError) {
|
||||
// handle connect timeout
|
||||
}
|
||||
```
|
||||
|
||||
| Error | Error Codes | Description |
|
||||
| ------------------------------------ | ------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `UndiciError` | `UND_ERR` | all errors below are extended from `UndiciError`. |
|
||||
| `ConnectTimeoutError` | `UND_ERR_CONNECT_TIMEOUT` | socket is destroyed due to connect timeout. |
|
||||
| `HeadersTimeoutError` | `UND_ERR_HEADERS_TIMEOUT` | socket is destroyed due to headers timeout. |
|
||||
| `HeadersOverflowError` | `UND_ERR_HEADERS_OVERFLOW` | socket is destroyed due to headers' max size being exceeded. |
|
||||
| `BodyTimeoutError` | `UND_ERR_BODY_TIMEOUT` | socket is destroyed due to body timeout. |
|
||||
| `InvalidArgumentError` | `UND_ERR_INVALID_ARG` | passed an invalid argument. |
|
||||
| `InvalidReturnValueError` | `UND_ERR_INVALID_RETURN_VALUE` | returned an invalid value. |
|
||||
| `RequestAbortedError` | `UND_ERR_ABORTED` | the request has been aborted by the user |
|
||||
| `ClientDestroyedError` | `UND_ERR_DESTROYED` | trying to use a destroyed client. |
|
||||
| `ClientClosedError` | `UND_ERR_CLOSED` | trying to use a closed client. |
|
||||
| `SocketError` | `UND_ERR_SOCKET` | there is an error with the socket. |
|
||||
| `NotSupportedError` | `UND_ERR_NOT_SUPPORTED` | encountered unsupported functionality. |
|
||||
| `RequestContentLengthMismatchError` | `UND_ERR_REQ_CONTENT_LENGTH_MISMATCH` | request body does not match content-length header |
|
||||
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header |
|
||||
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
|
||||
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
|
||||
| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
|
||||
| `MessageSizeExceededError` | `UND_ERR_WS_MESSAGE_SIZE_EXCEEDED` | WebSocket decompressed message exceeded the maximum allowed size |
|
||||
| `AbortError` | `UND_ERR_ABORT` | the operation was aborted (base class of `RequestAbortedError`). |
|
||||
| `RequestRetryError` | `UND_ERR_REQ_RETRY` | request failed and could not be retried; carries `statusCode`, `headers` and `data`. |
|
||||
| `ResponseError` | `UND_ERR_RESPONSE` | response returned an error status code; carries `statusCode`, `headers` and `body`. |
|
||||
| `MaxOriginsReachedError` | `UND_ERR_MAX_ORIGINS_REACHED` | the maximum number of allowed origins has been reached. |
|
||||
| `BalancedPoolMissingUpstreamError` | `UND_ERR_BPL_MISSING_UPSTREAM` | no upstream has been added to the `BalancedPool`. |
|
||||
| `Socks5ProxyError` | `UND_ERR_SOCKS5` | an error occurred during SOCKS5 proxy negotiation. |
|
||||
| `HTTPParserError` | `HPE_*` | an error occurred while parsing the HTTP response (extends `Error`, not `UndiciError`). |
|
||||
All errors, except [`HTTPParserError`][], extend [`UndiciError`][]. Each error
|
||||
carries a stable `code` string (for example `UND_ERR_CONNECT_TIMEOUT`) and a
|
||||
`name`.
|
||||
|
||||
Be aware of the possible difference between the global dispatcher version and the actual undici version you might be using. We recommend to avoid the check `instanceof errors.UndiciError` and seek for the `error.code === '<error_code>'` instead to avoid inconsistencies.
|
||||
Because the bundled (global) dispatcher may come from a different undici version
|
||||
than the one you import directly, prefer matching on `error.code` rather than
|
||||
`instanceof errors.UndiciError`:
|
||||
|
||||
### `ConnectTimeoutError`
|
||||
```js
|
||||
if (err.code === 'UND_ERR_CONNECT_TIMEOUT') {
|
||||
// handle connect timeout
|
||||
}
|
||||
```
|
||||
|
||||
When `autoSelectFamily` is enabled and every attempted address fails with a timeout, Node raises an `AggregateError`. Undici surfaces these multi-address timeouts as `ConnectTimeoutError` (so the error shape is the same regardless of whether Node's family-attempt timer or undici's `connectTimeout` wins the race); the original `AggregateError` is preserved on `error.cause`.
|
||||
## Class: `UndiciError`
|
||||
|
||||
### `SocketError`
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made `UndiciError` and its subclasses reliable with `instanceof`
|
||||
across realms and undici versions.
|
||||
-->
|
||||
|
||||
The `SocketError` has a `.socket` property which holds socket metadata:
|
||||
* Extends: {Error}
|
||||
|
||||
The base class for all undici errors. It is reliable with `instanceof` even when
|
||||
the error originates from a different undici version, because the check is based
|
||||
on a well-known symbol rather than the prototype chain.
|
||||
|
||||
* `name` {string} Always `'UndiciError'`.
|
||||
* `code` {string} Always `'UND_ERR'`.
|
||||
|
||||
## Class: `ConnectTimeoutError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The socket was destroyed because establishing the connection exceeded the
|
||||
`connectTimeout` option.
|
||||
|
||||
* `name` {string} Always `'ConnectTimeoutError'`.
|
||||
* `code` {string} Always `'UND_ERR_CONNECT_TIMEOUT'`.
|
||||
|
||||
When `autoSelectFamily` is enabled and every attempted address times out, Node.js
|
||||
raises an `AggregateError`. undici normalizes these multi-address timeouts into a
|
||||
`ConnectTimeoutError` so that the error shape is the same regardless of whether
|
||||
Node.js's per-address timer or undici's `connectTimeout` wins the race. The
|
||||
original `AggregateError` is preserved on `error.cause`.
|
||||
|
||||
## Class: `HeadersTimeoutError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The socket was destroyed because receiving the response headers exceeded the
|
||||
`headersTimeout` option.
|
||||
|
||||
* `name` {string} Always `'HeadersTimeoutError'`.
|
||||
* `code` {string} Always `'UND_ERR_HEADERS_TIMEOUT'`.
|
||||
|
||||
## Class: `HeadersOverflowError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The socket was destroyed because the response headers exceeded the maximum
|
||||
allowed size.
|
||||
|
||||
* `name` {string} Always `'HeadersOverflowError'`.
|
||||
* `code` {string} Always `'UND_ERR_HEADERS_OVERFLOW'`.
|
||||
|
||||
## Class: `BodyTimeoutError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The socket was destroyed because reading the response body exceeded the
|
||||
`bodyTimeout` option.
|
||||
|
||||
* `name` {string} Always `'BodyTimeoutError'`.
|
||||
* `code` {string} Always `'UND_ERR_BODY_TIMEOUT'`.
|
||||
|
||||
## Class: `InvalidArgumentError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
An invalid argument was passed to an undici API.
|
||||
|
||||
* `name` {string} Always `'InvalidArgumentError'`.
|
||||
* `code` {string} Always `'UND_ERR_INVALID_ARG'`.
|
||||
|
||||
## Class: `InvalidReturnValueError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
A user-supplied callback or interceptor returned an invalid value.
|
||||
|
||||
* `name` {string} Always `'InvalidReturnValueError'`.
|
||||
* `code` {string} Always `'UND_ERR_INVALID_RETURN_VALUE'`.
|
||||
|
||||
## Class: `AbortError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The operation was aborted. This is the base class of [`RequestAbortedError`][].
|
||||
|
||||
* `name` {string} Always `'AbortError'`.
|
||||
* `code` {string} Always `'UND_ERR_ABORT'`.
|
||||
|
||||
## Class: `RequestAbortedError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {AbortError}
|
||||
|
||||
The request was aborted by the user, typically through an `AbortSignal`.
|
||||
|
||||
* `name` {string} Always `'AbortError'`.
|
||||
* `code` {string} Always `'UND_ERR_ABORTED'`.
|
||||
|
||||
## Class: `InformationalError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
An expected error carrying a reason, used internally to surface a cause for a
|
||||
failed or retried request (for example as the `cause` of another error).
|
||||
|
||||
* `name` {string} Always `'InformationalError'`.
|
||||
* `code` {string} Always `'UND_ERR_INFO'`.
|
||||
|
||||
## Class: `RequestContentLengthMismatchError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The length of the request body did not match the `content-length` header.
|
||||
|
||||
* `name` {string} Always `'RequestContentLengthMismatchError'`.
|
||||
* `code` {string} Always `'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH'`.
|
||||
|
||||
## Class: `ResponseContentLengthMismatchError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The length of the response body did not match the `content-length` header.
|
||||
|
||||
* `name` {string} Always `'ResponseContentLengthMismatchError'`.
|
||||
* `code` {string} Always `'UND_ERR_RES_CONTENT_LENGTH_MISMATCH'`.
|
||||
|
||||
## Class: `ClientDestroyedError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
An operation was attempted on a client that has already been destroyed.
|
||||
|
||||
* `name` {string} Always `'ClientDestroyedError'`.
|
||||
* `code` {string} Always `'UND_ERR_DESTROYED'`.
|
||||
|
||||
## Class: `ClientClosedError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
An operation was attempted on a client that has already been closed.
|
||||
|
||||
* `name` {string} Always `'ClientClosedError'`.
|
||||
* `code` {string} Always `'UND_ERR_CLOSED'`.
|
||||
|
||||
## Class: `SocketError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
An error occurred on the underlying socket.
|
||||
|
||||
* `name` {string} Always `'SocketError'`.
|
||||
* `code` {string} Always `'UND_ERR_SOCKET'`.
|
||||
* `socket` {SocketInfo|null} Metadata about the socket at the time of the error,
|
||||
or `null` when no socket information is available.
|
||||
|
||||
The `socket` property, when present, holds the following fields:
|
||||
|
||||
```ts
|
||||
interface SocketInfo {
|
||||
@@ -58,4 +318,207 @@ interface SocketInfo {
|
||||
}
|
||||
```
|
||||
|
||||
Be aware that in some cases the `.socket` property can be `null`.
|
||||
## Class: `NotSupportedError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
Unsupported functionality was requested.
|
||||
|
||||
* `name` {string} Always `'NotSupportedError'`.
|
||||
* `code` {string} Always `'UND_ERR_NOT_SUPPORTED'`.
|
||||
|
||||
## Class: `BalancedPoolMissingUpstreamError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
No upstream has been added to the [`BalancedPool`][].
|
||||
|
||||
* `name` {string} Always `'MissingUpstreamError'`.
|
||||
* `code` {string} Always `'UND_ERR_BPL_MISSING_UPSTREAM'`.
|
||||
|
||||
## Class: `HTTPParserError`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Extends: {Error}
|
||||
|
||||
An error occurred while parsing the HTTP response. Unlike the other error
|
||||
classes, `HTTPParserError` extends {Error} directly rather than
|
||||
[`UndiciError`][].
|
||||
|
||||
* `name` {string} Always `'HTTPParserError'`.
|
||||
* `code` {string} An `HPE_`-prefixed parser error code, or `undefined` when no
|
||||
code was provided.
|
||||
* `data` {string} The raw data associated with the parser failure, or
|
||||
`undefined` when none was provided.
|
||||
|
||||
## Class: `ResponseExceededMaxSizeError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The response body exceeded the maximum allowed size.
|
||||
|
||||
* `name` {string} Always `'ResponseExceededMaxSizeError'`.
|
||||
* `code` {string} Always `'UND_ERR_RES_EXCEEDED_MAX_SIZE'`.
|
||||
|
||||
## Class: `RequestRetryError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
A request failed and could not be retried, for example because the body is
|
||||
stateful (a stream or `AsyncIterable`) and cannot be replayed.
|
||||
|
||||
* `name` {string} Always `'RequestRetryError'`.
|
||||
* `code` {string} Always `'UND_ERR_REQ_RETRY'`.
|
||||
* `statusCode` {number} The HTTP status code of the failed response.
|
||||
* `data` {Object} Retry metadata.
|
||||
* `count` {number} The number of retry attempts that were made.
|
||||
* `headers` {Record<string, string|string[]>} The response headers.
|
||||
|
||||
### `new RequestRetryError(message, code, options)`
|
||||
|
||||
* `message` {string} The error message.
|
||||
* `code` {number} The HTTP status code of the failed response.
|
||||
* `options` {Object}
|
||||
* `headers` {Object|string[]|null} The response headers. (optional)
|
||||
* `data` {Object|string|null} Retry metadata. (optional)
|
||||
|
||||
## Class: `ResponseError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The response returned an error status code. This is raised, for example, when the
|
||||
`throwOnError` option is enabled.
|
||||
|
||||
* `name` {string} Always `'ResponseError'`.
|
||||
* `code` {string} Always `'UND_ERR_RESPONSE'`.
|
||||
* `statusCode` {number} The HTTP status code of the response.
|
||||
* `body` {Object|string|null} The response body.
|
||||
* `headers` {Object|string[]|null} The response headers.
|
||||
|
||||
### `new ResponseError(message, code, options)`
|
||||
|
||||
* `message` {string} The error message.
|
||||
* `code` {number} The HTTP status code of the response.
|
||||
* `options` {Object}
|
||||
* `headers` {Object|string[]|null} The response headers. (optional)
|
||||
* `body` {Object|string|null} The response body. (optional)
|
||||
|
||||
## Class: `SecureProxyConnectionError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4472
|
||||
description: Made the error reliable with `instanceof`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
A TLS connection to a proxy failed.
|
||||
|
||||
* `name` {string} Always `'SecureProxyConnectionError'`.
|
||||
* `code` {string} Always `'UND_ERR_PRX_TLS'`.
|
||||
* `cause` {Error} The underlying error that caused the proxy connection to fail.
|
||||
|
||||
### `new SecureProxyConnectionError(cause[, message[, options]])`
|
||||
|
||||
* `cause` {Error} The underlying error. (optional)
|
||||
* `message` {string} The error message. (optional)
|
||||
* `options` {Object} Additional `Error` options merged with `cause`. (optional)
|
||||
|
||||
## Class: `MaxOriginsReachedError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4365
|
||||
description: Added to support capping the number of origins in `Agent`.
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
The maximum number of allowed origins has been reached.
|
||||
|
||||
* `name` {string} Always `'MaxOriginsReachedError'`.
|
||||
* `code` {string} Always `'UND_ERR_MAX_ORIGINS_REACHED'`.
|
||||
|
||||
## Class: `Socks5ProxyError`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.23.0
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
An error occurred during SOCKS5 proxy negotiation.
|
||||
|
||||
* `name` {string} Always `'Socks5ProxyError'`.
|
||||
* `code` {string} The error code. **Default:** `'UND_ERR_SOCKS5'`.
|
||||
|
||||
### `new Socks5ProxyError([message[, code]])`
|
||||
|
||||
* `message` {string} The error message. (optional)
|
||||
* `code` {string} The error code. **Default:** `'UND_ERR_SOCKS5'`. (optional)
|
||||
|
||||
## Class: `MessageSizeExceededError`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
A decompressed WebSocket message exceeded the maximum allowed size.
|
||||
|
||||
* `name` {string} Always `'MessageSizeExceededError'`.
|
||||
* `code` {string} Always `'UND_ERR_WS_MESSAGE_SIZE_EXCEEDED'`.
|
||||
|
||||
[`BalancedPool`]: BalancedPool.md#class-balancedpool
|
||||
[`HTTPParserError`]: #class-httpparsererror
|
||||
[`RequestAbortedError`]: #class-requestabortederror
|
||||
[`UndiciError`]: #class-undicierror
|
||||
+294
-32
@@ -1,14 +1,15 @@
|
||||
# EventSource
|
||||
|
||||
> ⚠️ Warning: the EventSource API is experimental.
|
||||
<!--introduced_in=v6.5.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/web/eventsource/eventsource.js -->
|
||||
|
||||
Undici exposes a WHATWG spec-compliant implementation of [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/EventSource)
|
||||
for [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events).
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
## Instantiating EventSource
|
||||
|
||||
Undici exports an EventSource class. You can instantiate the EventSource as
|
||||
follows:
|
||||
`EventSource` is a [WHATWG-conformant][] implementation of the
|
||||
[server-sent events][] interface. It opens a persistent HTTP connection to a
|
||||
server that responds with the `text/event-stream` content type and dispatches
|
||||
the events it receives without closing the connection.
|
||||
|
||||
```mjs
|
||||
import { EventSource } from 'undici'
|
||||
@@ -19,11 +20,266 @@ eventSource.onmessage = (event) => {
|
||||
}
|
||||
```
|
||||
|
||||
## Receiving events from a server
|
||||
Constructing an `EventSource` for the first time emits a one-time
|
||||
`ExperimentalWarning` with the code `'UNDICI-ES'`. The interface is also
|
||||
installed onto `globalThis` as `globalThis.EventSource`.
|
||||
|
||||
EventSource connects to an HTTP endpoint that responds with a `text/event-stream`
|
||||
content type. The connection stays open and receives events as the server writes
|
||||
them.
|
||||
## Class: `EventSource`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Extends: {EventTarget}
|
||||
|
||||
The `EventSource` interface receives server-sent events over an HTTP
|
||||
connection. Instances are created with the [`new EventSource()`](#new-eventsourceurl-eventsourceinitdict)
|
||||
constructor and emit [`'open'`](#event-open), [`'message'`](#event-message), and
|
||||
[`'error'`](#event-error) events.
|
||||
|
||||
### `new EventSource(url[, eventSourceInitDict])`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `url` {string|URL} The URL of the event stream. Relative URLs are resolved
|
||||
against the environment's base URL.
|
||||
* `eventSourceInitDict` {Object} (optional)
|
||||
* `withCredentials` {boolean} When `true`, the request is made with the
|
||||
credentials mode set to `include` and CORS attribute state set to
|
||||
`use-credentials`; otherwise the credentials mode is `same-origin`.
|
||||
**Default:** `false`.
|
||||
* `dispatcher` {Dispatcher} The dispatcher used for the underlying request.
|
||||
**Default:** the global dispatcher.
|
||||
|
||||
> Stability: 0 - Deprecated. Use `node.dispatcher` instead.
|
||||
* `node` {Object} undici-specific extensions to the standard
|
||||
`EventSourceInit` dictionary.
|
||||
* `dispatcher` {Dispatcher} The dispatcher used for the underlying request.
|
||||
**Default:** the global dispatcher.
|
||||
* `reconnectionTime` {number} The reconnection time, in milliseconds, to
|
||||
wait before re-establishing a dropped connection. The server may override
|
||||
this value with a `retry` field. **Default:** `3000`.
|
||||
|
||||
Creates a new `EventSource` and immediately begins connecting to `url`. The
|
||||
request is sent with the `Accept: text/event-stream` header, a cache mode of
|
||||
`no-store`, and an initiator type of `other`.
|
||||
|
||||
If `url` cannot be parsed, a `SyntaxError` `DOMException` is thrown.
|
||||
|
||||
```mjs
|
||||
import { EventSource } from 'undici'
|
||||
|
||||
const eventSource = new EventSource('http://localhost:3000', {
|
||||
withCredentials: true
|
||||
})
|
||||
```
|
||||
|
||||
A custom [`Dispatcher`][] can be supplied to control the underlying request, for
|
||||
example to add request headers:
|
||||
|
||||
```mjs
|
||||
import { EventSource, Agent } from 'undici'
|
||||
|
||||
class CustomHeaderAgent extends Agent {
|
||||
dispatch (opts) {
|
||||
opts.headers['x-custom-header'] = 'hello world'
|
||||
return super.dispatch(...arguments)
|
||||
}
|
||||
}
|
||||
|
||||
const eventSource = new EventSource('http://localhost:3000', {
|
||||
node: {
|
||||
dispatcher: new CustomHeaderAgent()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### `eventSource.close()`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Returns: {undefined}
|
||||
|
||||
Closes the connection, if any, aborts the underlying request, and sets
|
||||
[`eventSource.readyState`](#eventsourcereadystate) to `CLOSED`. Once closed, the
|
||||
`EventSource` does not attempt to reconnect. Calling `close()` on an already
|
||||
closed `EventSource` has no effect.
|
||||
|
||||
### `eventSource.readyState`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {number}
|
||||
|
||||
A read-only number representing the state of the connection. It is one of the
|
||||
following constants:
|
||||
|
||||
* [`EventSource.CONNECTING`](#eventsourceconnecting) (`0`) — the connection has
|
||||
not yet been established, or it was closed and is being re-established.
|
||||
* [`EventSource.OPEN`](#eventsourceopen) (`1`) — the connection is open and
|
||||
events are being dispatched as they are received.
|
||||
* [`EventSource.CLOSED`](#eventsourceclosed) (`2`) — the connection is not open
|
||||
and is not being re-established.
|
||||
|
||||
### `eventSource.url`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {string}
|
||||
|
||||
A read-only string providing the URL of the event stream, after resolution
|
||||
against the environment's base URL.
|
||||
|
||||
### `eventSource.withCredentials`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {boolean}
|
||||
|
||||
A read-only boolean indicating whether the `EventSource` was instantiated with
|
||||
CORS credentials (`true`), or not (`false`, the default). It reflects the
|
||||
`withCredentials` option passed to the constructor.
|
||||
|
||||
### `eventSource.onopen`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {Function|null} **Default:** `null`.
|
||||
|
||||
An event handler that is invoked when an [`'open'`](#event-open) event is
|
||||
dispatched. Assigning a function registers it as the handler; assigning `null`
|
||||
removes the current handler.
|
||||
|
||||
### `eventSource.onmessage`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {Function|null} **Default:** `null`.
|
||||
|
||||
An event handler that is invoked when a [`'message'`](#event-message) event is
|
||||
dispatched. Assigning a function registers it as the handler; assigning `null`
|
||||
removes the current handler.
|
||||
|
||||
### `eventSource.onerror`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {Function|null} **Default:** `null`.
|
||||
|
||||
An event handler that is invoked when an [`'error'`](#event-error) event is
|
||||
dispatched. Assigning a function registers it as the handler; assigning `null`
|
||||
removes the current handler.
|
||||
|
||||
### `EventSource.CONNECTING`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {number}
|
||||
|
||||
The numeric constant `0`, representing the `CONNECTING`
|
||||
[`readyState`](#eventsourcereadystate). It is defined as a read-only,
|
||||
non-writable property on both the `EventSource` constructor and instances.
|
||||
|
||||
### `EventSource.OPEN`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {number}
|
||||
|
||||
The numeric constant `1`, representing the `OPEN`
|
||||
[`readyState`](#eventsourcereadystate). It is defined as a read-only,
|
||||
non-writable property on both the `EventSource` constructor and instances.
|
||||
|
||||
### `EventSource.CLOSED`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {number}
|
||||
|
||||
The numeric constant `2`, representing the `CLOSED`
|
||||
[`readyState`](#eventsourcereadystate). It is defined as a read-only,
|
||||
non-writable property on both the `EventSource` constructor and instances.
|
||||
|
||||
### Event: `'open'`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Emitted when the connection is established and the
|
||||
[`readyState`](#eventsourcereadystate) becomes `OPEN`. The listener receives an
|
||||
{Event}.
|
||||
|
||||
```mjs
|
||||
import { EventSource } from 'undici'
|
||||
|
||||
const eventSource = new EventSource('http://localhost:3000')
|
||||
eventSource.addEventListener('open', () => {
|
||||
console.log('connection opened')
|
||||
})
|
||||
```
|
||||
|
||||
### Event: `'message'`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
Emitted when a message that has no explicit `event` field is received. The
|
||||
listener receives a {MessageEvent} whose `data`, `lastEventId`, and `origin`
|
||||
properties are populated from the server-sent event. Named events (those with an
|
||||
`event` field) are dispatched under their own type and must be subscribed to with
|
||||
[`addEventListener()`][].
|
||||
|
||||
```mjs
|
||||
import { createServer } from 'node:http'
|
||||
@@ -49,44 +305,50 @@ const server = createServer((request, response) => {
|
||||
server.listen(3000, () => {
|
||||
const eventSource = new EventSource('http://localhost:3000')
|
||||
|
||||
// Named event, delivered under its own type.
|
||||
eventSource.addEventListener('ping', (event) => {
|
||||
console.log('ping:', event.data)
|
||||
})
|
||||
|
||||
// Unnamed event, delivered as 'message'.
|
||||
eventSource.onmessage = (event) => {
|
||||
console.log('message:', event.data)
|
||||
}
|
||||
|
||||
eventSource.onerror = () => {
|
||||
eventSource.close()
|
||||
server.close()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The `message` event receives events without an explicit `event:` field. Use
|
||||
`addEventListener()` to subscribe to named events.
|
||||
### Event: `'error'`
|
||||
|
||||
## Using a custom Dispatcher
|
||||
<!-- YAML
|
||||
added: v6.5.0
|
||||
changes:
|
||||
- version: v7.11.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4247
|
||||
description: The connection is no longer re-established after a network
|
||||
error; the `EventSource` is closed instead.
|
||||
-->
|
||||
|
||||
Undici allows you to set your own Dispatcher in the EventSource constructor.
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
An example which allows you to modify the request headers is:
|
||||
Emitted when the connection fails or is interrupted. The listener receives an
|
||||
{Event}. When the failure is recoverable, the `EventSource` transitions back to
|
||||
`CONNECTING` and retries after the reconnection time; when it is not, the
|
||||
`EventSource` transitions to `CLOSED` and does not reconnect.
|
||||
|
||||
```mjs
|
||||
import { EventSource, Agent } from 'undici'
|
||||
import { EventSource } from 'undici'
|
||||
|
||||
class CustomHeaderAgent extends Agent {
|
||||
dispatch (opts) {
|
||||
opts.headers['x-custom-header'] = 'hello world'
|
||||
return super.dispatch(...arguments)
|
||||
const eventSource = new EventSource('http://localhost:3000')
|
||||
eventSource.onerror = () => {
|
||||
if (eventSource.readyState === EventSource.CLOSED) {
|
||||
console.log('connection closed')
|
||||
} else {
|
||||
console.log('reconnecting')
|
||||
}
|
||||
}
|
||||
|
||||
const eventSource = new EventSource('http://localhost:3000', {
|
||||
dispatcher: new CustomHeaderAgent()
|
||||
})
|
||||
```
|
||||
|
||||
More information about the EventSource API can be found on
|
||||
[MDN](https://developer.mozilla.org/en-US/docs/Web/API/EventSource).
|
||||
[WHATWG-conformant]: https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`addEventListener()`]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
|
||||
[server-sent events]: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events
|
||||
+680
-31
@@ -1,51 +1,643 @@
|
||||
# Fetch
|
||||
|
||||
Undici exposes a fetch() method starts the process of fetching a resource from the network.
|
||||
<!--introduced_in=v1.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/web/fetch/index.js -->
|
||||
|
||||
Documentation and examples can be found on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/fetch).
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## FormData
|
||||
undici implements the [WHATWG Fetch Standard][], providing `fetch()` together
|
||||
with the `Request`, `Response`, `Headers`, and `FormData` classes that mirror
|
||||
the browser APIs. The implementation follows the standard, so the
|
||||
[MDN Fetch documentation][] applies as well.
|
||||
|
||||
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/FormData).
|
||||
```mjs
|
||||
import { fetch, Request, Response, Headers, FormData } from 'undici'
|
||||
|
||||
If any parameters are passed to the FormData constructor other than `undefined`, an error will be thrown. Other parameters are ignored.
|
||||
const response = await fetch('https://example.com')
|
||||
const text = await response.text()
|
||||
```
|
||||
|
||||
When you use `FormData` as a request body, keep `fetch` and `FormData` from the
|
||||
same implementation. Use the built-in global `FormData` with the built-in
|
||||
global `fetch()`, and use `undici`'s `FormData` with `undici.fetch()`.
|
||||
When mixing these classes, keep them from the same implementation: use the
|
||||
global `fetch()` with the global `FormData`, `Request`, `Response`, and
|
||||
`Headers`, and use undici's `fetch()` with undici's classes. Passing a value
|
||||
created by one implementation to the other can throw.
|
||||
|
||||
If you want the installed `undici` package to provide the globals, call
|
||||
[`install()`](/docs/docs/api/GlobalInstallation.md) so `fetch`, `Headers`,
|
||||
`Response`, `Request`, and `FormData` are installed together as a matching set.
|
||||
## `fetch(input[, init])`
|
||||
|
||||
## Response
|
||||
<!-- YAML
|
||||
added: v5.6.0
|
||||
changes:
|
||||
- version: v5.6.1
|
||||
pr-url: https://github.com/nodejs/undici/pull/1529
|
||||
description: No default value is assigned to `init.method`.
|
||||
-->
|
||||
|
||||
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Response)
|
||||
* `input` {string|URL|Request} The resource to fetch. A string or {URL} is
|
||||
treated as the URL to request; a {Request} is used as the request template.
|
||||
* `init` {RequestInit} (optional) An options object that customizes the
|
||||
request.
|
||||
* `body` {string|Buffer|Uint8Array|Blob|FormData|URLSearchParams|ReadableStream|null}
|
||||
The request body. **Default:** `null`.
|
||||
* `cache` {string} The cache mode. One of `'default'`, `'force-cache'`,
|
||||
`'no-cache'`, `'no-store'`, `'only-if-cached'`, or `'reload'`.
|
||||
* `credentials` {string} How credentials are sent. One of `'omit'`,
|
||||
`'include'`, or `'same-origin'`.
|
||||
* `dispatcher` {Dispatcher} The {Dispatcher} used to perform the request.
|
||||
**Default:** the global dispatcher.
|
||||
* `duplex` {string} The duplex mode of the request. Must be `'half'` when a
|
||||
streaming `body` is provided.
|
||||
* `headers` {Headers|Object|Array} The request headers, as a {Headers}
|
||||
instance, a plain object, or an array of `[name, value]` pairs.
|
||||
* `integrity` {string} The subresource integrity metadata of the request.
|
||||
* `keepalive` {boolean} Whether the connection may outlive the page.
|
||||
**Default:** `false`.
|
||||
* `method` {string} The request method, for example `'GET'` or `'POST'`.
|
||||
* `mode` {string} The request mode. One of `'cors'`, `'navigate'`,
|
||||
`'no-cors'`, or `'same-origin'`.
|
||||
* `redirect` {string} How redirects are handled. One of `'error'`,
|
||||
`'follow'`, or `'manual'`.
|
||||
* `referrer` {string} The request referrer.
|
||||
* `referrerPolicy` {string} The referrer policy. One of `''`,
|
||||
`'no-referrer'`, `'no-referrer-when-downgrade'`, `'origin'`,
|
||||
`'origin-when-cross-origin'`, `'same-origin'`, `'strict-origin'`,
|
||||
`'strict-origin-when-cross-origin'`, or `'unsafe-url'`.
|
||||
* `signal` {AbortSignal|null} An {AbortSignal} used to abort the request.
|
||||
**Default:** `null`.
|
||||
* `window` {null} Can only be `null`; reserved by the standard.
|
||||
* Returns: {Promise} Fulfills with a {Response} once the response headers have
|
||||
been received.
|
||||
|
||||
## Request
|
||||
Starts the process of fetching a resource from the network and returns a
|
||||
promise that fulfills with a {Response}. The promise rejects only on network
|
||||
failures; an HTTP error status such as `404` still fulfills the promise, so
|
||||
inspect [`response.ok`](#responseok) to detect failures.
|
||||
|
||||
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Request)
|
||||
```mjs
|
||||
import { fetch } from 'undici'
|
||||
|
||||
## Headers
|
||||
const response = await fetch('https://example.com', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ hello: 'world' }),
|
||||
})
|
||||
|
||||
This API is implemented as per the standard, you can find documentation on [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Headers)
|
||||
console.log(response.status)
|
||||
console.log(await response.json())
|
||||
```
|
||||
|
||||
# Body Mixins
|
||||
To route the request through a custom {Dispatcher} (for example a `ProxyAgent`
|
||||
or `Agent` with specific options), pass it as `init.dispatcher`.
|
||||
|
||||
`Response` and `Request` body inherit body mixin methods. These methods include:
|
||||
```mjs
|
||||
import { fetch, Agent } from 'undici'
|
||||
|
||||
- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
|
||||
- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
|
||||
- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
|
||||
- [`.formData()`](https://fetch.spec.whatwg.org/#dom-body-formdata)
|
||||
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
|
||||
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
|
||||
const response = await fetch('https://example.com', {
|
||||
dispatcher: new Agent({ connect: { rejectUnauthorized: false } }),
|
||||
})
|
||||
```
|
||||
|
||||
There is an ongoing discussion regarding `body.formData()` and its usefulness, performance, and security in server environments. Calling `body.formData()` causes undici to buffer and parse the entire body. Because multipart parsing has inherent security risks, `body.formData()` must only be called on responses from trusted servers.
|
||||
## Class: `FormData`
|
||||
|
||||
For responses from untrusted or user-controlled servers, use a dedicated streaming library for parsing `multipart/form-data` bodies, such as [Busboy](https://www.npmjs.com/package/busboy) or [@fastify/busboy](https://www.npmjs.com/package/@fastify/busboy), and apply application-specific limits.
|
||||
<!-- YAML
|
||||
added: v4.4.4
|
||||
-->
|
||||
|
||||
These libraries can be interfaced with fetch with the following example code:
|
||||
A set of key/value pairs representing form fields and their values, suitable
|
||||
for use as a `fetch()` request `body`. The implementation follows the
|
||||
[WHATWG Fetch Standard][]; see the [MDN `FormData` documentation][].
|
||||
|
||||
When using `FormData` as a request body, keep `fetch` and `FormData` from the
|
||||
same implementation: use the global `FormData` with the global `fetch()`, and
|
||||
undici's `FormData` with undici's `fetch()`.
|
||||
|
||||
### `new FormData()`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.4.4
|
||||
-->
|
||||
|
||||
Creates a new, empty `FormData` instance. Passing any argument other than
|
||||
`undefined` throws; in particular, an `HTMLFormElement` argument is not
|
||||
supported in this environment.
|
||||
|
||||
### `formData.append(name, value[, filename])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the field.
|
||||
* `value` {string|Blob} The value of the field.
|
||||
* `filename` {string} The filename reported to the server when `value` is a
|
||||
{Blob}. (optional)
|
||||
|
||||
Appends a new value to an existing key, or adds the key if it does not exist.
|
||||
Unlike [`formData.set()`](#formdatasetname-value-filename), `append()` keeps
|
||||
any existing values for `name`.
|
||||
|
||||
### `formData.delete(name)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the field to remove.
|
||||
|
||||
Deletes all values associated with `name`.
|
||||
|
||||
### `formData.get(name)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the field to read.
|
||||
* Returns: {string|File|null} The first value associated with `name`, or
|
||||
`null` if there is none.
|
||||
|
||||
### `formData.getAll(name)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the field to read.
|
||||
* Returns: {Array} All values associated with `name`, as an array of
|
||||
{string} and {File} entries.
|
||||
|
||||
### `formData.has(name)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the field to look up.
|
||||
* Returns: {boolean} `true` if at least one value is associated with `name`.
|
||||
|
||||
### `formData.set(name, value[, filename])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the field.
|
||||
* `value` {string|Blob} The value of the field.
|
||||
* `filename` {string} The filename reported to the server when `value` is a
|
||||
{Blob}. (optional)
|
||||
|
||||
Sets a new value for an existing key, or adds the key if it does not exist,
|
||||
replacing any values previously associated with `name`.
|
||||
|
||||
## Class: `Response`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.4.0
|
||||
-->
|
||||
|
||||
* Extends: {BodyMixin}
|
||||
|
||||
Represents the response to a request. Instances are typically obtained by
|
||||
awaiting `fetch()`, but can also be constructed directly. The implementation
|
||||
follows the [WHATWG Fetch Standard][]; see the
|
||||
[MDN `Response` documentation][]. `Response` inherits the body-reading methods
|
||||
described in [Body mixin](#body-mixin).
|
||||
|
||||
### `new Response([body[, init]])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.4.0
|
||||
-->
|
||||
|
||||
* `body` {string|Buffer|Uint8Array|Blob|FormData|URLSearchParams|ReadableStream|null}
|
||||
The response body. **Default:** `null`.
|
||||
* `init` {ResponseInit} (optional)
|
||||
* `status` {number} The response status code. **Default:** `200`.
|
||||
* `statusText` {string} The response status message. **Default:** `''`.
|
||||
* `headers` {Headers|Object|Array} The response headers.
|
||||
|
||||
Creates a new `Response`.
|
||||
|
||||
### Static method: `Response.error()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Response} A network-error response.
|
||||
|
||||
Returns a new `Response` representing a network error, with its
|
||||
[`type`](#responsetype) set to `'error'`.
|
||||
|
||||
### Static method: `Response.json(data[, init])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `data` {any} The value to serialize as JSON.
|
||||
* `init` {ResponseInit} (optional)
|
||||
* `status` {number} The response status code. **Default:** `200`.
|
||||
* `statusText` {string} The response status message. **Default:** `''`.
|
||||
* `headers` {Headers|Object|Array} The response headers.
|
||||
* Returns: {Response} A response whose body is the JSON serialization of
|
||||
`data` and whose `Content-Type` is `application/json`.
|
||||
|
||||
```mjs
|
||||
import { Response } from 'undici'
|
||||
|
||||
const response = Response.json({ ok: true }, { status: 201 })
|
||||
```
|
||||
|
||||
### Static method: `Response.redirect(url[, status])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `url` {string|URL} The URL to redirect to.
|
||||
* `status` {number} The redirect status code. One of `301`, `302`, `303`,
|
||||
`307`, or `308`. **Default:** `302`.
|
||||
* Returns: {Response} A redirect response with the `Location` header set to
|
||||
`url`.
|
||||
|
||||
### `response.clone()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Response} A copy of the response.
|
||||
|
||||
Creates a clone of the response. Throws a `TypeError` if the body has already
|
||||
been read or is locked.
|
||||
|
||||
### `response.type`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The response type. One of `'basic'`, `'cors'`, `'default'`,
|
||||
`'error'`, `'opaque'`, or `'opaqueredirect'`.
|
||||
|
||||
### `response.url`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The final URL of the response after any redirects, or the
|
||||
empty string if not available.
|
||||
|
||||
### `response.redirected`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {boolean} `true` if the response is the result of one or more
|
||||
redirects.
|
||||
|
||||
### `response.status`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {number} The HTTP status code of the response.
|
||||
|
||||
### `response.ok`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {boolean} `true` when [`status`](#responsestatus) is in the range
|
||||
`200`–`299`.
|
||||
|
||||
### `response.statusText`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The status message corresponding to the status code.
|
||||
|
||||
### `response.headers`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {Headers} The {Headers} object associated with the response.
|
||||
|
||||
## Class: `Request`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Extends: {BodyMixin}
|
||||
|
||||
Represents a resource request. Instances can be passed to `fetch()` in place
|
||||
of a URL string. The implementation follows the [WHATWG Fetch Standard][]; see
|
||||
the [MDN `Request` documentation][]. `Request` inherits the body-reading
|
||||
methods described in [Body mixin](#body-mixin).
|
||||
|
||||
### `new Request(input[, init])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `input` {string|URL|Request} The resource to request, as a URL string,
|
||||
{URL}, or another {Request} to copy.
|
||||
* `init` {RequestInit} (optional) An options object with the same fields as
|
||||
the `init` argument of [`fetch()`](#fetchinput-init).
|
||||
* Returns: {Request}
|
||||
|
||||
Creates a new `Request`.
|
||||
|
||||
### `request.clone()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Request} A copy of the request.
|
||||
|
||||
Creates a clone of the request. Throws a `TypeError` if the body has already
|
||||
been read or is locked.
|
||||
|
||||
### `request.method`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The request method, for example `'GET'`.
|
||||
|
||||
### `request.url`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The serialized URL of the request.
|
||||
|
||||
### `request.headers`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {Headers} The {Headers} object associated with the request.
|
||||
|
||||
### `request.destination`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The request destination, indicating the type of content being
|
||||
requested, for example `''`, `'image'`, or `'script'`.
|
||||
|
||||
### `request.referrer`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The referrer of the request. May be `'about:client'` or a URL
|
||||
string.
|
||||
|
||||
### `request.referrerPolicy`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The referrer policy of the request.
|
||||
|
||||
### `request.mode`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The mode of the request. One of `'cors'`, `'navigate'`,
|
||||
`'no-cors'`, or `'same-origin'`.
|
||||
|
||||
### `request.credentials`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The credentials mode of the request. One of `'omit'`,
|
||||
`'include'`, or `'same-origin'`.
|
||||
|
||||
### `request.cache`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The cache mode of the request.
|
||||
|
||||
### `request.redirect`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The redirect mode of the request. One of `'error'`,
|
||||
`'follow'`, or `'manual'`.
|
||||
|
||||
### `request.integrity`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The subresource integrity metadata of the request.
|
||||
|
||||
### `request.keepalive`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {boolean} Whether the request may outlive the environment that created
|
||||
it.
|
||||
|
||||
### `request.isReloadNavigation`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {boolean} `true` if the request is a reload navigation.
|
||||
|
||||
### `request.isHistoryNavigation`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {boolean} `true` if the request is a history navigation.
|
||||
|
||||
### `request.signal`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {AbortSignal} The {AbortSignal} associated with the request.
|
||||
|
||||
### `request.duplex`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {string} The duplex mode of the request. Always `'half'`.
|
||||
|
||||
## Class: `Headers`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.4.0
|
||||
-->
|
||||
|
||||
Represents the header list of a request or response and provides methods to
|
||||
read and modify it. The implementation follows the [WHATWG Fetch Standard][];
|
||||
see the [MDN `Headers` documentation][]. `Headers` is iterable, yielding
|
||||
`[name, value]` pairs sorted by name.
|
||||
|
||||
### `new Headers([init])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.4.0
|
||||
-->
|
||||
|
||||
* `init` {Headers|Object|Array} (optional) Initial headers, as a {Headers}
|
||||
instance, a plain object of name/value pairs, or an array of `[name, value]`
|
||||
pairs.
|
||||
|
||||
Creates a new `Headers` object.
|
||||
|
||||
### `headers.append(name, value)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the header.
|
||||
* `value` {string} The value of the header.
|
||||
|
||||
Appends a value to a header, or adds the header if it does not exist. Existing
|
||||
values for `name` are preserved.
|
||||
|
||||
### `headers.delete(name)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the header to remove.
|
||||
|
||||
Removes the header named `name`.
|
||||
|
||||
### `headers.get(name)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the header to read.
|
||||
* Returns: {string|null} The combined values of the header, or `null` if it is
|
||||
not present.
|
||||
|
||||
### `headers.has(name)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the header to look up.
|
||||
* Returns: {boolean} `true` if the header is present.
|
||||
|
||||
### `headers.set(name, value)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `name` {string} The name of the header.
|
||||
* `value` {string} The value of the header.
|
||||
|
||||
Sets a header to a single value, replacing any existing values for `name`.
|
||||
|
||||
### `headers.getSetCookie()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {string[]} An array of the values of all `Set-Cookie` headers.
|
||||
|
||||
Returns each `Set-Cookie` header as a separate string, without combining them.
|
||||
|
||||
## Body mixin
|
||||
|
||||
`Request` and `Response` both extend {BodyMixin}, which provides methods and
|
||||
properties for reading a body. Each consuming method reads the body once; after
|
||||
the body has been consumed, [`bodyUsed`](#bodybodyused) becomes `true` and
|
||||
calling another consuming method throws a `TypeError`.
|
||||
|
||||
### `body.arrayBuffer()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Fulfills with an {ArrayBuffer} containing the body bytes.
|
||||
|
||||
### `body.blob()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Fulfills with a {Blob} containing the body.
|
||||
|
||||
### `body.bytes()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Fulfills with a {Uint8Array} containing the body bytes.
|
||||
|
||||
### `body.formData()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
> Stability: 0 - Deprecated
|
||||
|
||||
* Returns: {Promise} Fulfills with a {FormData} parsed from the body.
|
||||
|
||||
Buffers and parses the entire body as `multipart/form-data` or
|
||||
`application/x-www-form-urlencoded`. Because multipart parsing has inherent
|
||||
security risks and the whole body is buffered, this method must only be called
|
||||
on responses from trusted servers.
|
||||
|
||||
For responses from untrusted or user-controlled servers, use a dedicated
|
||||
streaming parser such as [@fastify/busboy][] and apply application-specific
|
||||
limits:
|
||||
|
||||
```mjs
|
||||
import { Busboy } from '@fastify/busboy'
|
||||
@@ -53,10 +645,67 @@ import { Readable } from 'node:stream'
|
||||
|
||||
const response = await fetch('...')
|
||||
const busboy = new Busboy({
|
||||
headers: {
|
||||
'content-type': response.headers.get('content-type')
|
||||
}
|
||||
headers: { 'content-type': response.headers.get('content-type') },
|
||||
})
|
||||
|
||||
// Handle the events emitted by `busboy`.
|
||||
|
||||
Readable.fromWeb(response.body).pipe(busboy)
|
||||
```
|
||||
|
||||
### `body.json()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Fulfills with the result of parsing the body as JSON.
|
||||
|
||||
### `body.text()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Fulfills with a {string} containing the body decoded as
|
||||
UTF-8.
|
||||
|
||||
### `body.textStream()`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Returns: {ReadableStream} A {ReadableStream} of {string} chunks produced by
|
||||
decoding the body as UTF-8.
|
||||
|
||||
An undici-specific extension that exposes the body as a stream of decoded text
|
||||
chunks rather than buffering it. It is not part of the [WHATWG Fetch
|
||||
Standard][].
|
||||
|
||||
### `body.body`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {ReadableStream|null} The body as a {ReadableStream}, or `null` if the
|
||||
message has no body.
|
||||
|
||||
### `body.bodyUsed`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* Type: {boolean} `true` once the body has been read.
|
||||
|
||||
[@fastify/busboy]: https://www.npmjs.com/package/@fastify/busboy
|
||||
[MDN Fetch documentation]: https://developer.mozilla.org/en-US/docs/Web/API/fetch
|
||||
[MDN `FormData` documentation]: https://developer.mozilla.org/en-US/docs/Web/API/FormData
|
||||
[MDN `Headers` documentation]: https://developer.mozilla.org/en-US/docs/Web/API/Headers
|
||||
[MDN `Request` documentation]: https://developer.mozilla.org/en-US/docs/Web/API/Request
|
||||
[MDN `Response` documentation]: https://developer.mozilla.org/en-US/docs/Web/API/Response
|
||||
[WHATWG Fetch Standard]: https://fetch.spec.whatwg.org/
|
||||
+77
-97
@@ -1,24 +1,70 @@
|
||||
# Global Installation
|
||||
|
||||
Undici provides an `install()` function to add fetch-related and other web API classes to `globalThis`, making them available globally without requiring imports.
|
||||
<!--introduced_in=v7.11.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=index.js -->
|
||||
|
||||
> Stability: 2 - Stable
|
||||
|
||||
undici ships its own implementations of several WHATWG web APIs. The `install()`
|
||||
function assigns those implementations onto `globalThis`, so they can be used
|
||||
without importing them from `'undici'` first. This is useful for polyfilling
|
||||
environments, guaranteeing consistent behavior across Node.js versions, and
|
||||
satisfying third-party libraries that expect the relevant classes to be global.
|
||||
|
||||
```mjs
|
||||
import { install } from 'undici'
|
||||
|
||||
install()
|
||||
|
||||
// `fetch`, `Headers`, `Response`, `Request`, etc. now resolve to undici's
|
||||
// implementations.
|
||||
const response = await fetch('https://example.com')
|
||||
```
|
||||
|
||||
```cjs
|
||||
const { install } = require('undici')
|
||||
|
||||
install()
|
||||
```
|
||||
|
||||
## `install()`
|
||||
|
||||
Install undici's global web APIs on `globalThis`.
|
||||
<!-- YAML
|
||||
added: v7.11.0
|
||||
-->
|
||||
|
||||
**Example:**
|
||||
* Returns: {undefined}
|
||||
|
||||
```js
|
||||
Overwrites the following properties of `globalThis` with undici's
|
||||
implementations:
|
||||
|
||||
* `globalThis.fetch`
|
||||
* `globalThis.Headers`
|
||||
* `globalThis.Response`
|
||||
* `globalThis.Request`
|
||||
* `globalThis.FormData`
|
||||
* `globalThis.WebSocket`
|
||||
* `globalThis.CloseEvent`
|
||||
* `globalThis.ErrorEvent`
|
||||
* `globalThis.MessageEvent`
|
||||
* `globalThis.EventSource`
|
||||
|
||||
Each installed value is the same object exported by `'undici'` under the
|
||||
matching name, so the global identifiers form a single, self-consistent set.
|
||||
The assignment is unconditional: any pre-existing global of the same name,
|
||||
including the Node.js built-in, is replaced. The change persists for the
|
||||
lifetime of the process. Calling `install()` more than once is safe and simply
|
||||
re-assigns the same values.
|
||||
|
||||
Documentation for each installed class lives on its own page: see
|
||||
[`fetch`][], [`WebSocket`][], and [`EventSource`][].
|
||||
|
||||
```mjs
|
||||
import { install } from 'undici'
|
||||
|
||||
// Install undici's global web APIs
|
||||
install()
|
||||
|
||||
// Now you can use fetch classes globally without importing
|
||||
const response = await fetch('https://api.example.com/data')
|
||||
const data = await response.json()
|
||||
|
||||
// All classes are available globally:
|
||||
const headers = new Headers([['content-type', 'application/json']])
|
||||
const request = new Request('https://example.com')
|
||||
const formData = new FormData()
|
||||
@@ -26,116 +72,50 @@ const ws = new WebSocket('wss://example.com')
|
||||
const eventSource = new EventSource('https://example.com/events')
|
||||
```
|
||||
|
||||
## Installed Classes
|
||||
### Pairing `fetch` and `FormData`
|
||||
|
||||
The `install()` function adds the following classes to `globalThis`:
|
||||
When a request body is a `FormData` instance, the `fetch` and `FormData`
|
||||
implementations must come from the same source. After `install()`, both globals
|
||||
resolve to undici, so they always match:
|
||||
|
||||
| Class | Description |
|
||||
|-------|-------------|
|
||||
| `fetch` | The fetch function for making HTTP requests |
|
||||
| `Headers` | HTTP headers management |
|
||||
| `Response` | HTTP response representation |
|
||||
| `Request` | HTTP request representation |
|
||||
| `FormData` | Form data handling |
|
||||
| `WebSocket` | WebSocket client |
|
||||
| `CloseEvent` | WebSocket close event |
|
||||
| `ErrorEvent` | WebSocket error event |
|
||||
| `MessageEvent` | WebSocket message event |
|
||||
| `EventSource` | Server-sent events client |
|
||||
|
||||
## Using `FormData` with `fetch`
|
||||
|
||||
If you send a `FormData` body, use matching implementations for `fetch` and
|
||||
`FormData`.
|
||||
|
||||
These two patterns are safe:
|
||||
|
||||
```js
|
||||
// Built-in globals from Node.js
|
||||
const body = new FormData()
|
||||
await fetch('https://example.com', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
```
|
||||
|
||||
```js
|
||||
// Globals installed from the undici package
|
||||
```mjs
|
||||
import { install } from 'undici'
|
||||
|
||||
install()
|
||||
|
||||
const body = new FormData()
|
||||
await fetch('https://example.com', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
await fetch('https://example.com', { method: 'POST', body })
|
||||
```
|
||||
|
||||
After `install()`, `fetch`, `Headers`, `Response`, `Request`, and `FormData`
|
||||
all come from the installed `undici` package, so they work as a matching set.
|
||||
`WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and `EventSource`
|
||||
also come from the installed `undici` package.
|
||||
If global installation is not desired, import the matching pair directly from
|
||||
`'undici'` instead:
|
||||
|
||||
If you do not want to install globals, import both from `undici` instead:
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { fetch, FormData } from 'undici'
|
||||
|
||||
const body = new FormData()
|
||||
await fetch('https://example.com', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
await fetch('https://example.com', { method: 'POST', body })
|
||||
```
|
||||
|
||||
Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData`
|
||||
with the built-in global `fetch()`. Keeping them paired avoids surprising
|
||||
multipart behavior across Node.js and undici versions.
|
||||
Mixing a global `FormData` with `undici.fetch()`, or `undici.FormData` with the
|
||||
built-in global `fetch()`, can produce surprising multipart behavior across
|
||||
Node.js and undici versions. Keep the two paired.
|
||||
|
||||
## Use Cases
|
||||
### Conditional installation
|
||||
|
||||
Global installation is useful for:
|
||||
`install()` can be guarded so undici's `fetch` is only installed when no global
|
||||
`fetch` already exists:
|
||||
|
||||
- **Polyfilling environments** that don't have native fetch support
|
||||
- **Ensuring consistent behavior** across different Node.js versions
|
||||
- **Library compatibility** when third-party libraries expect global fetch
|
||||
- **Migration scenarios** where you want to replace built-in implementations
|
||||
- **Testing environments** where you need predictable fetch behavior
|
||||
|
||||
## Example: Polyfilling an Environment
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { install } from 'undici'
|
||||
|
||||
// Check if fetch is available and install if needed
|
||||
if (typeof globalThis.fetch === 'undefined') {
|
||||
install()
|
||||
console.log('Undici fetch installed globally')
|
||||
}
|
||||
|
||||
// Now fetch is guaranteed to be available
|
||||
const response = await fetch('https://api.example.com')
|
||||
const response = await fetch('https://example.com')
|
||||
```
|
||||
|
||||
## Example: Testing Environment
|
||||
|
||||
```js
|
||||
import { install } from 'undici'
|
||||
|
||||
// In test setup, ensure consistent fetch behavior
|
||||
install()
|
||||
|
||||
// Now all tests use undici's implementations
|
||||
test('fetch API test', async () => {
|
||||
const response = await fetch('https://example.com')
|
||||
expect(response).toBeInstanceOf(Response)
|
||||
})
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- The `install()` function overwrites any existing global implementations
|
||||
- Classes installed are undici's implementations, not Node.js built-ins
|
||||
- This provides access to undici's latest fetch, WebSocket, and EventSource features and performance improvements
|
||||
- The global installation persists for the lifetime of the process
|
||||
[`EventSource`]: EventSource.md#class-eventsource
|
||||
[`WebSocket`]: WebSocket.md#class-websocket
|
||||
[`fetch`]: Fetch.md
|
||||
+300
-178
@@ -1,263 +1,385 @@
|
||||
# Class: H2CClient
|
||||
# H2CClient
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v7.7.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/dispatcher/h2c-client.js -->
|
||||
|
||||
A basic H2C client.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
**Example**
|
||||
An `H2CClient` is a [`Dispatcher`][] that speaks HTTP/2 in cleartext (h2c) over a
|
||||
single TCP connection to an `http:` origin, without the protocol upgrade or prior
|
||||
knowledge negotiation that a regular [`Client`][] performs. It is a thin
|
||||
specialization of [`Client`][] that forces `allowH2` and `useH2c` on and aliases
|
||||
`pipelining` to `maxConcurrentStreams`, so multiple requests are multiplexed as
|
||||
HTTP/2 streams rather than pipelined HTTP/1.x requests.
|
||||
|
||||
```js
|
||||
const { createServer } = require('node:http2')
|
||||
const { once } = require('node:events')
|
||||
const { H2CClient } = require('undici')
|
||||
Use an `H2CClient` when the server is known to accept cleartext HTTP/2 directly,
|
||||
for example a service behind a trusted proxy or in a test environment. Only the
|
||||
`http:` protocol is supported; passing any other protocol throws
|
||||
`InvalidArgumentError`.
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
res.writeHead(200)
|
||||
res.end('Hello, world!')
|
||||
})
|
||||
```mjs
|
||||
import { H2CClient } from 'undici'
|
||||
|
||||
server.listen()
|
||||
once(server, 'listening').then(async () => {
|
||||
const client = new H2CClient(`http://localhost:${server.address().port}/`)
|
||||
const client = new H2CClient('http://localhost:3000')
|
||||
|
||||
const response = await client.request({ path: '/', method: 'GET' })
|
||||
console.log(response.statusCode) // 200
|
||||
response.body.text.then((text) => {
|
||||
console.log(text) // Hello, world!
|
||||
})
|
||||
})
|
||||
const { statusCode, body } = await client.request({ path: '/', method: 'GET' })
|
||||
console.log(statusCode)
|
||||
console.log(await body.text())
|
||||
```
|
||||
|
||||
## `new H2CClient(url[, options])`
|
||||
## Class: `H2CClient`
|
||||
|
||||
Arguments:
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
changes:
|
||||
- version: v7.17.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4690
|
||||
description: Added support for h2c over Unix domain sockets.
|
||||
-->
|
||||
|
||||
- **url** `URL | string` - Should only include the **protocol, hostname, and port**. It only supports `http` protocol.
|
||||
- **options** `H2CClientOptions` (optional)
|
||||
* Extends: {Client}
|
||||
|
||||
Returns: `H2CClient`
|
||||
`H2CClient` inherits the full instance API of [`Client`][] (and therefore of
|
||||
[`Dispatcher`][]). The sections below cover the surface that is meaningful for an
|
||||
h2c client; refer to [`Dispatcher`][] for the complete semantics of each method
|
||||
and event.
|
||||
|
||||
### Parameter: `H2CClientOptions`
|
||||
### `new H2CClient(url[, options])`
|
||||
|
||||
- **bodyTimeout** `number | null` (optional) - Default: `300e3` - The timeout after which a request will time out, in milliseconds. Monitors time between receiving body data. Use `0` to disable it entirely. Defaults to 300 seconds. Please note the `timeout` will be reset if you keep writing data to the socket everytime.
|
||||
- **headersTimeout** `number | null` (optional) - Default: `300e3` - The amount of time, in milliseconds, the parser will wait to receive the complete HTTP headers while not sending the request. Defaults to 300 seconds.
|
||||
- **keepAliveMaxTimeout** `number | null` (optional) - Default: `600e3` - The maximum allowed `keepAliveTimeout`, in milliseconds, when overridden by _keep-alive_ hints from the server. Defaults to 10 minutes.
|
||||
- **keepAliveTimeout** `number | null` (optional) - Default: `4e3` - The timeout, in milliseconds, after which a socket without active requests will time out. Monitors time between activity on a connected socket. This value may be overridden by _keep-alive_ hints from the server. See [MDN: HTTP - Headers - Keep-Alive directives](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Keep-Alive#directives) for more details. Defaults to 4 seconds.
|
||||
- **keepAliveTimeoutThreshold** `number | null` (optional) - Default: `2e3` - A number of milliseconds subtracted from server _keep-alive_ hints when overriding `keepAliveTimeout` to account for timing inaccuracies caused by e.g. transport latency. Defaults to 2 seconds.
|
||||
- **maxHeaderSize** `number | null` (optional) - Default: `--max-http-header-size` or `16384` - The maximum length of request headers in bytes. Defaults to Node.js' --max-http-header-size or 16KiB.
|
||||
- **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable.
|
||||
- **maxConcurrentStreams**: `number` - Default: `100`. The maximum number of concurrent HTTP/2 streams per session — also advertised to the server as `peerMaxConcurrentStreams` (the cap on streams the server may push back). The initial value is replaced by the server's `SETTINGS_MAX_CONCURRENT_STREAMS` whenever the server sends one, so a user-supplied value acts as a pre-`SETTINGS` default rather than a hard cap.
|
||||
- **pipelining** `number | null` (optional) - Default to `maxConcurrentStreams` - The amount of concurrent requests sent over a single HTTP/2 session in accordance with [RFC-7540](https://httpwg.org/specs/rfc7540.html#StreamsLayer) Stream specification. Streams can be closed up by remote server at any time. Unlike on a regular [`Client`](/docs/docs/api/Client.md), `H2CClient` aliases `pipelining` to `maxConcurrentStreams` at construction time, so the two move together.
|
||||
- **pingInterval**: `number` - Default: `60e3`. The time interval in milliseconds between PING frames sent to the server. Set to `0` to disable PING frames. This is only applicable for HTTP/2 connections.
|
||||
- **connect** `ConnectOptions | null` (optional) - Default: `null`.
|
||||
- **strictContentLength** `Boolean` (optional) - Default: `true` - Whether to treat request content length mismatches as errors. If true, an error is thrown when the request content-length header doesn't match the length of the request body. **Security Warning:** Disabling this option can expose your application to HTTP Request Smuggling attacks, where mismatched content-length headers cause servers and proxies to interpret request boundaries differently. This can lead to cache poisoning, credential hijacking, and bypassing security controls. Only disable this in controlled environments where you fully trust the request source.
|
||||
- **autoSelectFamily**: `boolean` (optional) - Default: depends on local Node version, on Node 18.13.0 and above is `false`. Enables a family autodetection algorithm that loosely implements section 5 of [RFC 8305](https://tools.ietf.org/html/rfc8305#section-5). See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details. This option is ignored if not supported by the current Node version.
|
||||
- **autoSelectFamilyAttemptTimeout**: `number` - Default: depends on local Node version, on Node 18.13.0 and above is `250`. The amount of time in milliseconds to wait for a connection attempt to finish before trying the next address when using the `autoSelectFamily` option. See [here](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) for more details.
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
#### Parameter: `H2CConnectOptions`
|
||||
* `url` {URL|string} The origin to connect to. Should include only the protocol,
|
||||
hostname, and port. Only the `http:` protocol is supported.
|
||||
* `options` {H2CClientOptions} (optional)
|
||||
* `maxConcurrentStreams` {number} The maximum number of concurrent HTTP/2
|
||||
streams for the session. It is advertised to the server and may be overridden
|
||||
by a remote `SETTINGS` frame. Must be a positive integer. **Default:** `100`.
|
||||
* `pipelining` {number} The number of concurrent requests multiplexed over the
|
||||
single connection. For an `H2CClient` this is aliased to
|
||||
`maxConcurrentStreams` and may not exceed it. Must be a positive integer.
|
||||
**Default:** `100`.
|
||||
* `maxHeaderSize` {number} The maximum length of request headers in bytes.
|
||||
**Default:** Node.js' `--max-http-header-size` or `16384` (16 KiB).
|
||||
* `headersTimeout` {number} The amount of time, in milliseconds, the parser
|
||||
waits to receive the complete HTTP headers. **Default:** `300e3`.
|
||||
* `connectTimeout` {number} The timeout for establishing a socket connection,
|
||||
in milliseconds. Use `0` to disable it entirely. **Default:** `10e3`.
|
||||
* `bodyTimeout` {number} The timeout, in milliseconds, after which a request
|
||||
times out. Monitors the time between received body data. Use `0` to disable
|
||||
it entirely. **Default:** `300e3`.
|
||||
* `keepAliveTimeout` {number} The timeout, in milliseconds, after which a
|
||||
socket without active requests times out. May be overridden by _keep-alive_
|
||||
hints from the server. **Default:** `4e3`.
|
||||
* `keepAliveMaxTimeout` {number} The maximum allowed `keepAliveTimeout`, in
|
||||
milliseconds, when overridden by _keep-alive_ hints from the server.
|
||||
**Default:** `600e3`.
|
||||
* `keepAliveTimeoutThreshold` {number} A number of milliseconds subtracted from
|
||||
server _keep-alive_ hints when overriding `keepAliveTimeout`, to account for
|
||||
timing inaccuracies such as transport latency. **Default:** `1e3`.
|
||||
* `socketPath` {string} An IPC endpoint, either a Unix domain socket or a
|
||||
Windows named pipe. **Default:** `null`.
|
||||
* `strictContentLength` {boolean} If `true`, an error is thrown when the
|
||||
request `content-length` header does not match the length of the request
|
||||
body. **Default:** `true`.
|
||||
* `maxCachedSessions` {number} The maximum number of TLS sessions cached by the
|
||||
built-in connector. Use `0` to disable TLS session caching. **Default:** `100`.
|
||||
* `connect` {Object|Function} Connector options passed to `buildConnector`, or
|
||||
a custom connector function. The `allowH2` option may not be set here.
|
||||
**Default:** `null`.
|
||||
* `maxRequestsPerClient` {number} The maximum number of requests to send over
|
||||
a single connection before it is reset. Use `0` to disable this limit.
|
||||
**Default:** `null`.
|
||||
* `localAddress` {string} The local IP address the socket should connect from.
|
||||
* `maxResponseSize` {number} The maximum allowed response body size in bytes.
|
||||
Use `-1` to disable. **Default:** `-1`.
|
||||
* `autoSelectFamily` {boolean} Enables a family autodetection algorithm that
|
||||
loosely implements section 5 of [RFC 8305][]. Ignored if not supported by the
|
||||
current Node.js version.
|
||||
* `autoSelectFamilyAttemptTimeout` {number} The amount of time, in
|
||||
milliseconds, to wait for a connection attempt to finish before trying the
|
||||
next address when `autoSelectFamily` is enabled.
|
||||
|
||||
- **socketPath** `string | null` (optional) - Default: `null` - An IPC endpoint, either Unix domain socket or Windows named pipe.
|
||||
- **timeout** `number | null` (optional) - In milliseconds, Default `10e3`.
|
||||
- **servername** `string | null` (optional)
|
||||
- **keepAlive** `boolean | null` (optional) - Default: `true` - TCP keep-alive enabled
|
||||
- **keepAliveInitialDelay** `number | null` (optional) - Default: `60000` - TCP keep-alive interval for the socket in milliseconds
|
||||
Creates a new `H2CClient` for the given origin. The client does not connect until
|
||||
a request is queued; call [`h2cClient.connect()`](#h2cclientconnectoptions-callback)
|
||||
to connect eagerly.
|
||||
|
||||
### Example - Basic Client instantiation
|
||||
```mjs
|
||||
import { H2CClient } from 'undici'
|
||||
|
||||
This will instantiate the undici H2CClient, but it will not connect to the origin until something is queued. Consider using `client.connect` to prematurely connect to the origin, or just call `client.request`.
|
||||
|
||||
```js
|
||||
"use strict";
|
||||
import { H2CClient } from "undici";
|
||||
|
||||
const client = new H2CClient("http://localhost:3000");
|
||||
const client = new H2CClient('http://localhost:3000')
|
||||
```
|
||||
|
||||
## Instance Methods
|
||||
### `h2cClient.close([callback])`
|
||||
|
||||
### `H2CClient.close([callback])`
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
|
||||
* `callback` {Function} (optional) Invoked once all queued requests have settled
|
||||
and the client is closed.
|
||||
* Returns: {Promise|undefined} A `Promise` that resolves once the client is
|
||||
closed, when `callback` is not provided.
|
||||
|
||||
### `H2CClient.destroy([error, callback])`
|
||||
Closes the client and gracefully waits for enqueued requests to complete before
|
||||
resolving. See [`dispatcher.close([callback])`][].
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
### `h2cClient.destroy([error[, callback]])`
|
||||
|
||||
Waits until socket is closed before invoking the callback (or returning a promise if no callback is provided).
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
### `H2CClient.connect(options[, callback])`
|
||||
* `error` {Error} (optional) The error with which to reject pending requests.
|
||||
**Default:** `null`.
|
||||
* `callback` {Function} (optional) Invoked once the client is destroyed.
|
||||
* Returns: {Promise|undefined} A `Promise` that resolves once the client is
|
||||
destroyed, when `callback` is not provided.
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
|
||||
Destroys the client abruptly, aborting any pending requests. Waits until the
|
||||
socket is closed before invoking `callback` (or resolving the returned `Promise`).
|
||||
See [`dispatcher.destroy([error[, callback]])`][].
|
||||
|
||||
### `H2CClient.dispatch(options, handlers)`
|
||||
### `h2cClient.connect(options[, callback])`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
### `H2CClient.pipeline(options, handler)`
|
||||
* `options` {Object}
|
||||
* `path` {string}
|
||||
* `headers` {Object} (optional) **Default:** `null`.
|
||||
* `signal` {AbortSignal|EventEmitter|null} (optional) **Default:** `null`.
|
||||
* `opaque` {any} (optional) An opaque value passed through to the returned
|
||||
connect data.
|
||||
* `responseHeaders` {string|null} (optional) Set to `'raw'` to return the
|
||||
response headers as a raw array instead of an object. **Default:** `null`.
|
||||
* `callback` {Function} (optional) Invoked with `(err, data)` once the connection
|
||||
is established. If omitted, a `Promise` is returned instead.
|
||||
* Returns: {Promise|undefined} A `Promise` resolving to the connect data, when
|
||||
`callback` is not provided.
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
|
||||
Starts an HTTP `CONNECT` request. See [`dispatcher.connect(options[, callback])`][].
|
||||
|
||||
### `H2CClient.request(options[, callback])`
|
||||
### `h2cClient.dispatch(options, handlers)`
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
### `H2CClient.stream(options, factory[, callback])`
|
||||
* `options` {Object} The request options.
|
||||
* `handlers` {Object} The dispatch handler callbacks.
|
||||
* Returns: {boolean} `false` if the dispatcher is busy and the caller should wait
|
||||
for the [`'drain'`](#event-drain) event before dispatching again.
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
|
||||
The lowest-level request API. All other request methods are implemented on top of
|
||||
it. See [`dispatcher.dispatch(options, handler)`][].
|
||||
|
||||
### `H2CClient.upgrade(options[, callback])`
|
||||
### `h2cClient.pipeline(options, handler)`
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
## Instance Properties
|
||||
* `options` {Object} The request options.
|
||||
* `handler` {Function} A handler that receives the response data and returns a
|
||||
{Readable} body.
|
||||
* Returns: {Duplex} A duplex stream for the request body and response body.
|
||||
|
||||
### `H2CClient.closed`
|
||||
Sends a request and returns a duplex stream, suitable for piping. See
|
||||
[`dispatcher.pipeline(options, handler)`][].
|
||||
|
||||
- `boolean`
|
||||
### `h2cClient.request(options[, callback])`
|
||||
|
||||
`true` after `H2CClient.close()` has been called.
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
### `H2CClient.destroyed`
|
||||
* `options` {Object} The request options.
|
||||
* `callback` {Function} (optional) Invoked with `(err, data)` once the response
|
||||
is received. If omitted, a `Promise` is returned instead.
|
||||
* Returns: {Promise|undefined} A `Promise` resolving to the response data, when
|
||||
`callback` is not provided.
|
||||
|
||||
- `boolean`
|
||||
Performs an HTTP request. See [`dispatcher.request(options[, callback])`][].
|
||||
|
||||
`true` after `client.destroyed()` has been called or `client.close()` has been called and the client shutdown has completed.
|
||||
### `h2cClient.stream(options, factory[, callback])`
|
||||
|
||||
### `H2CClient.pipelining`
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
- `number`
|
||||
* `options` {Object} The request options.
|
||||
* `factory` {Function} A factory that returns a {Writable} to which the response
|
||||
body is written.
|
||||
* `callback` {Function} (optional) Invoked once the request completes. If
|
||||
omitted, a `Promise` is returned instead.
|
||||
* Returns: {Promise|undefined} A `Promise` that resolves once the request
|
||||
completes, when `callback` is not provided.
|
||||
|
||||
Property to get and set the pipelining factor.
|
||||
Sends a request and writes the response body to the stream returned by `factory`.
|
||||
See [`dispatcher.stream(options, factory[, callback])`][].
|
||||
|
||||
## Instance Events
|
||||
### `h2cClient.upgrade(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
* `options` {Object} The request options.
|
||||
* `callback` {Function} (optional) Invoked with `(err, data)` once the upgrade
|
||||
completes. If omitted, a `Promise` is returned instead.
|
||||
* Returns: {Promise|undefined} A `Promise` resolving to the upgrade data, when
|
||||
`callback` is not provided.
|
||||
|
||||
Upgrades a connection to a different protocol. See
|
||||
[`dispatcher.upgrade(options[, callback])`][].
|
||||
|
||||
### `h2cClient.closed`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
* Type: {boolean}
|
||||
|
||||
`true` after [`h2cClient.close()`](#h2cclientclosecallback) has been called.
|
||||
|
||||
### `h2cClient.destroyed`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
* Type: {boolean}
|
||||
|
||||
`true` after [`h2cClient.destroy()`](#h2cclientdestroyerror-callback) has been
|
||||
called, or after [`h2cClient.close()`](#h2cclientclosecallback) has been called
|
||||
and the client shutdown has completed.
|
||||
|
||||
### `h2cClient.pipelining`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Gets and sets the pipelining factor, i.e. the number of requests multiplexed over
|
||||
the connection.
|
||||
|
||||
### Event: `'connect'`
|
||||
|
||||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
Parameters:
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>}
|
||||
|
||||
- **origin** `URL`
|
||||
- **targets** `Array<Dispatcher>`
|
||||
Emitted when the socket has been created and connected. The client connects once
|
||||
`client.size > 0`. See [Dispatcher Event: `'connect'`][].
|
||||
|
||||
Emitted when a socket has been created and connected. The client will connect once `client.size > 0`.
|
||||
|
||||
#### Example - Client connect event
|
||||
|
||||
```js
|
||||
import { createServer } from "node:http2";
|
||||
import { H2CClient } from "undici";
|
||||
import { once } from "events";
|
||||
```mjs
|
||||
import { createServer } from 'node:http2'
|
||||
import { once } from 'node:events'
|
||||
import { H2CClient } from 'undici'
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.end("Hello, World!");
|
||||
}).listen();
|
||||
response.end('Hello, World!')
|
||||
}).listen()
|
||||
|
||||
await once(server, "listening");
|
||||
await once(server, 'listening')
|
||||
|
||||
const client = new H2CClient(`http://localhost:${server.address().port}`);
|
||||
const client = new H2CClient(`http://localhost:${server.address().port}`)
|
||||
|
||||
client.on("connect", (origin) => {
|
||||
console.log(`Connected to ${origin}`); // should print before the request body statement
|
||||
});
|
||||
client.on('connect', (origin) => {
|
||||
console.log(`Connected to ${origin}`)
|
||||
})
|
||||
|
||||
try {
|
||||
const { body } = await client.request({
|
||||
path: "/",
|
||||
method: "GET",
|
||||
});
|
||||
body.setEncoding("utf-8");
|
||||
body.on("data", console.log);
|
||||
client.close();
|
||||
server.close();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
client.close();
|
||||
server.close();
|
||||
}
|
||||
const { body } = await client.request({ path: '/', method: 'GET' })
|
||||
console.log(await body.text())
|
||||
|
||||
client.close()
|
||||
server.close()
|
||||
```
|
||||
|
||||
### Event: `'disconnect'`
|
||||
|
||||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
Parameters:
|
||||
* `origin` {URL}
|
||||
* `targets` {Array<Dispatcher>}
|
||||
* `error` {Error}
|
||||
|
||||
- **origin** `URL`
|
||||
- **targets** `Array<Dispatcher>`
|
||||
- **error** `Error`
|
||||
Emitted when the socket has disconnected. The `error` argument is the error that
|
||||
caused the socket to disconnect. The client reconnects if (or once)
|
||||
`client.size > 0`. See [Dispatcher Event: `'disconnect'`][].
|
||||
|
||||
Emitted when socket has disconnected. The error argument of the event is the error which caused the socket to disconnect. The client will reconnect if or once `client.size > 0`.
|
||||
|
||||
#### Example - Client disconnect event
|
||||
|
||||
```js
|
||||
import { createServer } from "node:http2";
|
||||
import { H2CClient } from "undici";
|
||||
import { once } from "events";
|
||||
```mjs
|
||||
import { createServer } from 'node:http2'
|
||||
import { once } from 'node:events'
|
||||
import { H2CClient } from 'undici'
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.destroy();
|
||||
}).listen();
|
||||
response.destroy()
|
||||
}).listen()
|
||||
|
||||
await once(server, "listening");
|
||||
await once(server, 'listening')
|
||||
|
||||
const client = new H2CClient(`http://localhost:${server.address().port}`);
|
||||
const client = new H2CClient(`http://localhost:${server.address().port}`)
|
||||
|
||||
client.on("disconnect", (origin) => {
|
||||
console.log(`Disconnected from ${origin}`);
|
||||
});
|
||||
client.on('disconnect', (origin) => {
|
||||
console.log(`Disconnected from ${origin}`)
|
||||
})
|
||||
|
||||
try {
|
||||
await client.request({
|
||||
path: "/",
|
||||
method: "GET",
|
||||
});
|
||||
await client.request({ path: '/', method: 'GET' })
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
client.close();
|
||||
server.close();
|
||||
console.error(error.message)
|
||||
} finally {
|
||||
client.close()
|
||||
server.close()
|
||||
}
|
||||
```
|
||||
|
||||
### Event: `'drain'`
|
||||
|
||||
Emitted when pipeline is no longer busy.
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
|
||||
* `origin` {URL}
|
||||
|
||||
#### Example - Client drain event
|
||||
|
||||
```js
|
||||
import { createServer } from "node:http2";
|
||||
import { H2CClient } from "undici";
|
||||
import { once } from "events";
|
||||
|
||||
const server = createServer((request, response) => {
|
||||
response.end("Hello, World!");
|
||||
}).listen();
|
||||
|
||||
await once(server, "listening");
|
||||
|
||||
const client = new H2CClient(`http://localhost:${server.address().port}`);
|
||||
|
||||
client.on("drain", () => {
|
||||
console.log("drain event");
|
||||
client.close();
|
||||
server.close();
|
||||
});
|
||||
|
||||
const requests = [
|
||||
client.request({ path: "/", method: "GET" }),
|
||||
client.request({ path: "/", method: "GET" }),
|
||||
client.request({ path: "/", method: "GET" }),
|
||||
];
|
||||
|
||||
await Promise.all(requests);
|
||||
|
||||
console.log("requests completed");
|
||||
```
|
||||
Emitted when the client is no longer busy and can accept more requests. See
|
||||
[Dispatcher Event: `'drain'`][].
|
||||
|
||||
### Event: `'error'`
|
||||
|
||||
Invoked for user errors such as throwing in the `onResponseError` handler.
|
||||
<!-- YAML
|
||||
added: v7.7.0
|
||||
-->
|
||||
|
||||
* `error` {Error}
|
||||
|
||||
Emitted for user errors, such as an error thrown from an `onResponseError`
|
||||
handler.
|
||||
|
||||
[RFC 8305]: https://tools.ietf.org/html/rfc8305#section-5
|
||||
[Dispatcher Event: `'connect'`]: Dispatcher.md#event-connect
|
||||
[Dispatcher Event: `'disconnect'`]: Dispatcher.md#event-disconnect
|
||||
[Dispatcher Event: `'drain'`]: Dispatcher.md#event-drain
|
||||
[`Client`]: Client.md#class-client
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`dispatcher.close([callback])`]: Dispatcher.md#dispatcherclosecallback
|
||||
[`dispatcher.connect(options[, callback])`]: Dispatcher.md#dispatcherconnectoptions-callback
|
||||
[`dispatcher.destroy([error[, callback]])`]: Dispatcher.md#dispatcherdestroyerror-callback
|
||||
[`dispatcher.dispatch(options, handler)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`dispatcher.pipeline(options, handler)`]: Dispatcher.md#dispatcherpipelineoptions-handler
|
||||
[`dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`dispatcher.stream(options, factory[, callback])`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
|
||||
[`dispatcher.upgrade(options[, callback])`]: Dispatcher.md#dispatcherupgradeoptions-callback
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
# 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 })
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `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 `Content-Length` 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 once the
|
||||
redirect limit is reached instead of returning the final redirect response.
|
||||
**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`, `compress`, `x-compress`, or `zstd`.
|
||||
|
||||
> **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`.
|
||||
|
||||
**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
|
||||
})
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## `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) — currently reserved for future use; may be
|
||||
omitted.
|
||||
|
||||
**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
|
||||
+329
-337
@@ -1,40 +1,77 @@
|
||||
# Class: MockAgent
|
||||
# MockAgent
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v4.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/mock/mock-agent.js -->
|
||||
|
||||
A mocked Agent class that implements the Agent API. It allows one to intercept HTTP requests made through undici and return mocked responses instead.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new MockAgent([options])`
|
||||
A `MockAgent` is a {Dispatcher} that intercepts HTTP requests made through
|
||||
undici and replies with programmed mock responses instead of contacting the
|
||||
network. It is useful for testing code that performs HTTP requests without
|
||||
relying on a live server.
|
||||
|
||||
Arguments:
|
||||
A `MockAgent` does not intercept requests on its own. Mock responses are
|
||||
registered on the {MockClient} or {MockPool} instances returned by
|
||||
[`mockAgent.get(origin)`][], and requests are routed through them once the
|
||||
`MockAgent` is set as the dispatcher (for example through
|
||||
[`setGlobalDispatcher()`][] or a per-request `dispatcher` option).
|
||||
|
||||
* **options** `MockAgentOptions` (optional) - It extends the `Agent` options.
|
||||
|
||||
Returns: `MockAgent`
|
||||
|
||||
### Parameter: `MockAgentOptions`
|
||||
|
||||
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
|
||||
|
||||
* **agent** `Agent` (optional) - Default: `new Agent([options])` - a custom agent encapsulated by the MockAgent.
|
||||
|
||||
* **ignoreTrailingSlash** `boolean` (optional) - Default: `false` - set the default value for `ignoreTrailingSlash` for interceptors.
|
||||
|
||||
* **acceptNonStandardSearchParameters** `boolean` (optional) - Default: `false` - set to `true` if the matcher should also accept non standard search parameters such as multi-value items specified with `[]` (e.g. `param[]=1¶m[]=2¶m[]=3`) and multi-value items which values are comma separated (e.g. `param=1,2,3`).
|
||||
|
||||
### Example - Basic MockAgent instantiation
|
||||
|
||||
This will instantiate the MockAgent. It will not do anything until registered as the agent to use with requests and mock interceptions are added.
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
```
|
||||
|
||||
### Example - Basic MockAgent instantiation with custom agent
|
||||
## Class: `MockAgent`
|
||||
|
||||
```js
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
Intercepts HTTP requests made through undici and returns mocked responses.
|
||||
|
||||
### `new MockAgent([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
changes:
|
||||
- version: v7.16.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4479
|
||||
description: Mock options are always normalized to an object internally.
|
||||
-->
|
||||
|
||||
* `options` {MockAgentOptions} (optional)
|
||||
* Returns: {MockAgent}
|
||||
|
||||
When instantiated, a `MockAgent` is automatically activated. It does not
|
||||
intercept any request until mock interceptors are registered on the dispatchers
|
||||
returned by [`mockAgent.get(origin)`][].
|
||||
|
||||
#### Parameter: `MockAgentOptions`
|
||||
|
||||
* Extends: {AgentOptions}
|
||||
* `agent` {Dispatcher} (optional) A custom agent to be encapsulated by the
|
||||
`MockAgent`. It must implement the `Agent` API (that is, expose a `dispatch`
|
||||
function). **Default:** a new {Agent} constructed from `options`.
|
||||
* `ignoreTrailingSlash` {boolean} (optional) Whether trailing slashes in the
|
||||
request path are ignored when matching interceptors. **Default:** `false`.
|
||||
* `acceptNonStandardSearchParameters` {boolean} (optional) Whether the matcher
|
||||
also accepts URLs using non-standard search-parameter syntaxes, such as
|
||||
multi-value items written with `[]` (for example `param[]=1¶m[]=2`) or
|
||||
comma-separated values (for example `param=1,2,3`). **Default:** `false`.
|
||||
* `enableCallHistory` {boolean} (optional) Whether call history recording is
|
||||
enabled. See [`mockAgent.getCallHistory()`][]. **Default:** `false`.
|
||||
|
||||
```mjs displayName="Basic instantiation"
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
```
|
||||
|
||||
```mjs displayName="Instantiation with a custom agent"
|
||||
import { Agent, MockAgent } from 'undici'
|
||||
|
||||
const agent = new Agent()
|
||||
@@ -42,34 +79,30 @@ const agent = new Agent()
|
||||
const mockAgent = new MockAgent({ agent })
|
||||
```
|
||||
|
||||
## Instance Methods
|
||||
### `mockAgent.get(origin)`
|
||||
|
||||
### `MockAgent.get(origin)`
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
This method creates and retrieves MockPool or MockClient instances which can then be used to intercept HTTP requests. If the number of connections on the mock agent is set to 1, a MockClient instance is returned. Otherwise a MockPool instance is returned.
|
||||
* `origin` {string|RegExp|Function} A matcher for the origin to retrieve. The
|
||||
function form has the signature `(origin) => boolean`.
|
||||
* Returns: {MockClient|MockPool}
|
||||
|
||||
For subsequent `MockAgent.get` calls on the same origin, the same mock instance will be returned.
|
||||
Creates and retrieves the mock dispatcher used to intercept requests for the
|
||||
given origin. When the `connections` option of the `MockAgent` is `1`, a
|
||||
{MockClient} is returned; otherwise a {MockPool} is returned. Subsequent calls
|
||||
with the same origin return the same instance.
|
||||
|
||||
Arguments:
|
||||
The way `origin` is matched against incoming requests depends on its type:
|
||||
|
||||
* **origin** `string | RegExp | (value) => boolean` - a matcher for the pool origin to be retrieved from the MockAgent.
|
||||
| Matcher type | Condition to pass |
|
||||
| :----------: | ---------------------------- |
|
||||
| `string` | Exact match against the value |
|
||||
| `RegExp` | The regular expression matches |
|
||||
| `Function` | The function returns `true` |
|
||||
|
||||
| Matcher type | Condition to pass |
|
||||
|:------------:| -------------------------- |
|
||||
| `string` | Exact match against string |
|
||||
| `RegExp` | Regex must pass |
|
||||
| `Function` | Function must return true |
|
||||
|
||||
Returns: `MockClient | MockPool`.
|
||||
|
||||
| `MockAgentOptions` | Mock instance returned |
|
||||
| -------------------- | ---------------------- |
|
||||
| `connections === 1` | `MockClient` |
|
||||
| `connections` > `1` | `MockPool` |
|
||||
|
||||
#### Example - Basic Mocked Request
|
||||
|
||||
```js
|
||||
```mjs displayName="Basic mocked request"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -87,9 +120,7 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic Mocked Request with local mock agent dispatcher
|
||||
|
||||
```js
|
||||
```mjs displayName="Mocked request using a local dispatcher"
|
||||
import { MockAgent, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -97,10 +128,9 @@ const mockAgent = new MockAgent()
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo', { dispatcher: mockAgent })
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo', {
|
||||
dispatcher: mockAgent
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
@@ -109,31 +139,7 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic Mocked Request with local mock pool dispatcher
|
||||
|
||||
```js
|
||||
import { MockAgent, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo', { dispatcher: mockPool })
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic Mocked Request with local mock client dispatcher
|
||||
|
||||
```js
|
||||
```mjs displayName="Returning a MockClient"
|
||||
import { MockAgent, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ connections: 1 })
|
||||
@@ -141,10 +147,9 @@ const mockAgent = new MockAgent({ connections: 1 })
|
||||
const mockClient = mockAgent.get('http://localhost:3000')
|
||||
mockClient.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo', { dispatcher: mockClient })
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo', {
|
||||
dispatcher: mockClient
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
@@ -153,96 +158,7 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic Mocked requests with multiple intercepts
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
mockPool.intercept({ path: '/hello'}).reply(200, 'hello')
|
||||
|
||||
const result1 = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', result1.statusCode) // response received 200
|
||||
|
||||
for await (const data of result1.body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
|
||||
const result2 = await request('http://localhost:3000/hello')
|
||||
|
||||
console.log('response received', result2.statusCode) // response received 200
|
||||
|
||||
for await (const data of result2.body) {
|
||||
console.log('data', data.toString('utf8')) // data hello
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Mock different requests within the same file
|
||||
|
||||
```js
|
||||
const { MockAgent, setGlobalDispatcher } = require('undici');
|
||||
const agent = new MockAgent();
|
||||
agent.disableNetConnect();
|
||||
setGlobalDispatcher(agent);
|
||||
describe('Test', () => {
|
||||
it('200', async () => {
|
||||
const mockAgent = agent.get('http://test.com');
|
||||
// your test
|
||||
});
|
||||
it('200', async () => {
|
||||
const mockAgent = agent.get('http://testing.com');
|
||||
// your test
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### Example - Mocked request with query body, headers and trailers
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo?hello=there&see=ya',
|
||||
method: 'POST',
|
||||
body: 'form1=data1&form2=data2'
|
||||
}).reply(200, { foo: 'bar' }, {
|
||||
headers: { 'content-type': 'application/json' },
|
||||
trailers: { 'Content-MD5': 'test' }
|
||||
})
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
headers,
|
||||
trailers,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo?hello=there&see=ya', {
|
||||
method: 'POST',
|
||||
body: 'form1=data1&form2=data2'
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
console.log('headers', headers) // { 'content-type': 'application/json' }
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // '{"foo":"bar"}'
|
||||
}
|
||||
|
||||
console.log('trailers', trailers) // { 'content-md5': 'test' }
|
||||
```
|
||||
|
||||
#### Example - Mocked request with origin regex
|
||||
|
||||
```js
|
||||
```mjs displayName="Matching the origin with a regular expression"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -251,21 +167,12 @@ setGlobalDispatcher(mockAgent)
|
||||
const mockPool = mockAgent.get(new RegExp('http://localhost:3000'))
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo')
|
||||
const { statusCode } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Mocked request with origin function
|
||||
|
||||
```js
|
||||
```mjs displayName="Matching the origin with a function"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -274,46 +181,29 @@ setGlobalDispatcher(mockAgent)
|
||||
const mockPool = mockAgent.get((origin) => origin === 'http://localhost:3000')
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo')
|
||||
const { statusCode } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
```
|
||||
|
||||
### `MockAgent.close()`
|
||||
### `mockAgent.dispatch(options, handler)`
|
||||
|
||||
Closes the mock agent and waits for registered mock pools and clients to also close before resolving.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
Returns: `Promise<void>`
|
||||
* `options` {AgentDispatchOptions}
|
||||
* `handler` {DispatchHandler}
|
||||
* Returns: {boolean}
|
||||
|
||||
#### Example - clean up after tests are complete
|
||||
Dispatches a mocked request. This implements
|
||||
[`Dispatcher.dispatch()`][] for the encapsulated agent: it ensures the mock
|
||||
dispatcher for `options.origin` exists, records the call in the call history
|
||||
when enabled, and forwards the request to the underlying agent. It is normally
|
||||
invoked indirectly through higher-level methods such as
|
||||
[`mockAgent.request()`][].
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
await mockAgent.close()
|
||||
```
|
||||
|
||||
### `MockAgent.dispatch(options, handlers)`
|
||||
|
||||
Implements [`Agent.dispatch(options, handlers)`](/docs/docs/api/Agent.md#parameter-agentdispatchoptions).
|
||||
|
||||
### `MockAgent.request(options[, callback])`
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
|
||||
#### Example - MockAgent request
|
||||
|
||||
```js
|
||||
```mjs displayName="Using mockAgent.request()"
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -321,10 +211,7 @@ const mockAgent = new MockAgent()
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await mockAgent.request({
|
||||
const { statusCode, body } = await mockAgent.request({
|
||||
origin: 'http://localhost:3000',
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
@@ -337,15 +224,39 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
### `MockAgent.deactivate()`
|
||||
### `mockAgent.close()`
|
||||
|
||||
This method disables mocking in MockAgent.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
Returns: `void`
|
||||
* Returns: {Promise<void>} Resolves once the agent and its registered mock pools
|
||||
and clients have closed.
|
||||
|
||||
#### Example - Deactivate Mocking
|
||||
Clears the call history, closes the encapsulated agent, and waits for all
|
||||
registered mock pools and clients to close.
|
||||
|
||||
```js
|
||||
```mjs displayName="Clean up after tests"
|
||||
import { MockAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
await mockAgent.close()
|
||||
```
|
||||
|
||||
### `mockAgent.deactivate()`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Returns: {undefined}
|
||||
|
||||
Disables mocking on the `MockAgent`. While deactivated, requests are no longer
|
||||
intercepted.
|
||||
|
||||
```mjs displayName="Deactivate mocking"
|
||||
import { MockAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -354,15 +265,19 @@ setGlobalDispatcher(mockAgent)
|
||||
mockAgent.deactivate()
|
||||
```
|
||||
|
||||
### `MockAgent.activate()`
|
||||
### `mockAgent.activate()`
|
||||
|
||||
This method enables mocking in a MockAgent instance. When instantiated, a MockAgent is automatically activated. Therefore, this method is only effective after `MockAgent.deactivate` has been called.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
Returns: `void`
|
||||
* Returns: {undefined}
|
||||
|
||||
#### Example - Activate Mocking
|
||||
Enables mocking on the `MockAgent`. A `MockAgent` is activated automatically
|
||||
when instantiated, so this method is only required after
|
||||
[`mockAgent.deactivate()`][] has been called.
|
||||
|
||||
```js
|
||||
```mjs displayName="Re-activate mocking"
|
||||
import { MockAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -375,21 +290,23 @@ mockAgent.deactivate()
|
||||
mockAgent.activate()
|
||||
```
|
||||
|
||||
### `MockAgent.enableNetConnect([host])`
|
||||
### `mockAgent.enableNetConnect([matcher])`
|
||||
|
||||
When requests are not matched in a MockAgent intercept, a real HTTP request is attempted. We can control this further through the use of `enableNetConnect`. This is achieved by defining host matchers so only matching requests will be attempted.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
When using a string, it should only include the **hostname and optionally, the port**. In addition, calling this method multiple times with a string will allow all HTTP requests that match these values.
|
||||
* `matcher` {string|RegExp|Function} (optional) A host matcher. When a string is
|
||||
used it should only contain the hostname and, optionally, the port. The
|
||||
function form has the signature `(host) => boolean`. When omitted, all
|
||||
non-matching requests are allowed to perform a real request.
|
||||
* Returns: {undefined}
|
||||
|
||||
Arguments:
|
||||
Defines host matchers so that requests that are not intercepted by a mock
|
||||
dispatcher are allowed to perform a real HTTP request. Calling this method
|
||||
multiple times with a string appends each value to the list of allowed hosts.
|
||||
|
||||
* **host** `string | RegExp | (value) => boolean` - (optional)
|
||||
|
||||
Returns: `void`
|
||||
|
||||
#### Example - Allow all non-matching urls to be dispatched in a real HTTP request
|
||||
|
||||
```js
|
||||
```mjs displayName="Allow all non-matching requests"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -401,9 +318,7 @@ await request('http://example.com')
|
||||
// A real request is made
|
||||
```
|
||||
|
||||
#### Example - Allow requests matching a host string to make real requests
|
||||
|
||||
```js
|
||||
```mjs displayName="Allow requests matching specific hosts"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -422,43 +337,31 @@ await request('http://example-3.com')
|
||||
// Will throw
|
||||
```
|
||||
|
||||
#### Example - Allow requests matching a host regex to make real requests
|
||||
|
||||
```js
|
||||
```mjs displayName="Allow requests matching a regular expression or function"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
mockAgent.enableNetConnect(new RegExp('example.com'))
|
||||
mockAgent.enableNetConnect((host) => host === 'example.org')
|
||||
|
||||
await request('http://example.com')
|
||||
// A real request is made
|
||||
```
|
||||
|
||||
#### Example - Allow requests matching a host function to make real requests
|
||||
### `mockAgent.disableNetConnect()`
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
* Returns: {undefined}
|
||||
|
||||
mockAgent.enableNetConnect((value) => value === 'example.com')
|
||||
Causes every request that is not matched by a mock interceptor to throw,
|
||||
disallowing real HTTP requests.
|
||||
|
||||
await request('http://example.com')
|
||||
// A real request is made
|
||||
```
|
||||
|
||||
### `MockAgent.disableNetConnect()`
|
||||
|
||||
This method causes all requests to throw when requests are not matched in a MockAgent intercept.
|
||||
|
||||
Returns: `void`
|
||||
|
||||
#### Example - Disable all non-matching requests by throwing an error for each
|
||||
|
||||
```js
|
||||
```mjs displayName="Throw on every non-matching request"
|
||||
import { MockAgent, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -469,28 +372,132 @@ await request('http://example.com')
|
||||
// Will throw
|
||||
```
|
||||
|
||||
### `MockAgent.pendingInterceptors()`
|
||||
### `mockAgent.enableCallHistory()`
|
||||
|
||||
This method returns any pending interceptors registered on a mock agent. A pending interceptor meets one of the following criteria:
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
|
||||
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
|
||||
- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
|
||||
* Returns: {MockAgent} The same `MockAgent` instance, for chaining.
|
||||
|
||||
Returns: `PendingInterceptor[]` (where `PendingInterceptor` is a `MockDispatch` with an additional `origin: string`)
|
||||
Enables call history recording. Once enabled, subsequent calls are registered
|
||||
and can be retrieved through [`mockAgent.getCallHistory()`][]. Call history can
|
||||
also be enabled at construction time with the `enableCallHistory` option.
|
||||
|
||||
#### Example - List all pending interceptors
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
```js
|
||||
const agent = new MockAgent()
|
||||
agent.disableNetConnect()
|
||||
const mockAgent = new MockAgent()
|
||||
|
||||
agent
|
||||
mockAgent.enableCallHistory()
|
||||
```
|
||||
|
||||
### `mockAgent.disableCallHistory()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Returns: {MockAgent} The same `MockAgent` instance, for chaining.
|
||||
|
||||
Disables call history recording. Subsequent calls are no longer registered.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true })
|
||||
|
||||
mockAgent.disableCallHistory()
|
||||
```
|
||||
|
||||
### `mockAgent.getCallHistory()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Returns: {MockCallHistory|undefined} The {MockCallHistory} instance, or
|
||||
`undefined` when call history is not enabled.
|
||||
|
||||
Returns the call history instance, which records every request made through the
|
||||
`MockAgent` (whether intercepted or not). Call history is not enabled by
|
||||
default; enable it with the `enableCallHistory` option or
|
||||
[`mockAgent.enableCallHistory()`][].
|
||||
|
||||
```mjs displayName="Inspect recorded calls"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true })
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
await request('http://example.com', { query: { item: 1 } })
|
||||
|
||||
mockAgent.getCallHistory()?.firstCall()
|
||||
// Returns
|
||||
// MockCallHistoryLog {
|
||||
// body: undefined,
|
||||
// headers: undefined,
|
||||
// method: 'GET',
|
||||
// origin: 'http://example.com',
|
||||
// fullUrl: 'http://example.com/?item=1',
|
||||
// path: '/',
|
||||
// searchParams: { item: '1' },
|
||||
// protocol: 'http:',
|
||||
// host: 'example.com',
|
||||
// port: ''
|
||||
// }
|
||||
```
|
||||
|
||||
### `mockAgent.clearCallHistory()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Returns: {undefined}
|
||||
|
||||
Clears the call history, deleting every recorded {MockCallHistoryLog} on the
|
||||
{MockCallHistory} instance. It is a no-op when call history has never been
|
||||
enabled.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true })
|
||||
|
||||
mockAgent.clearCallHistory()
|
||||
```
|
||||
|
||||
### `mockAgent.pendingInterceptors()`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.1.0
|
||||
-->
|
||||
|
||||
* Returns: {PendingInterceptor[]} A `PendingInterceptor` is a `MockDispatch`
|
||||
with an additional `origin` {string} property.
|
||||
|
||||
Returns the interceptors registered on the `MockAgent` that are still pending.
|
||||
An interceptor is pending when it meets one of the following criteria:
|
||||
|
||||
* It is registered with neither `.times(<number>)` nor `.persist()` and has not
|
||||
been invoked.
|
||||
* It is persistent (registered with `.persist()`) and has not been invoked.
|
||||
* It is registered with `.times(<number>)` and has not been invoked `<number>`
|
||||
times.
|
||||
|
||||
```mjs displayName="List pending interceptors"
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
mockAgent.disableNetConnect()
|
||||
|
||||
mockAgent
|
||||
.get('https://example.com')
|
||||
.intercept({ method: 'GET', path: '/' })
|
||||
.reply(200)
|
||||
|
||||
const pendingInterceptors = agent.pendingInterceptors()
|
||||
const pendingInterceptors = mockAgent.pendingInterceptors()
|
||||
// Returns [
|
||||
// {
|
||||
// timesInvoked: 0,
|
||||
@@ -514,26 +521,35 @@ const pendingInterceptors = agent.pendingInterceptors()
|
||||
// ]
|
||||
```
|
||||
|
||||
### `MockAgent.assertNoPendingInterceptors([options])`
|
||||
### `mockAgent.assertNoPendingInterceptors([options])`
|
||||
|
||||
This method throws if the mock agent has any pending interceptors. A pending interceptor meets one of the following criteria:
|
||||
<!-- YAML
|
||||
added: v5.1.0
|
||||
-->
|
||||
|
||||
- Is registered with neither `.times(<number>)` nor `.persist()`, and has not been invoked;
|
||||
- Is persistent (i.e., registered with `.persist()`) and has not been invoked;
|
||||
- Is registered with `.times(<number>)` and has not been invoked `<number>` of times.
|
||||
* `options` {Object} (optional)
|
||||
* `pendingInterceptorsFormatter` {Object} An object exposing a
|
||||
`format(pendingInterceptors)` method used to render the pending interceptors
|
||||
in the thrown error message. **Default:** a built-in formatter that prints a
|
||||
table.
|
||||
* Returns: {undefined}
|
||||
|
||||
#### Example - Check that there are no pending interceptors
|
||||
Throws an {UndiciError} when the `MockAgent` has any pending interceptors. The
|
||||
criteria for an interceptor being pending are the same as for
|
||||
[`mockAgent.pendingInterceptors()`][].
|
||||
|
||||
```js
|
||||
const agent = new MockAgent()
|
||||
agent.disableNetConnect()
|
||||
```mjs displayName="Assert there are no pending interceptors"
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
agent
|
||||
const mockAgent = new MockAgent()
|
||||
mockAgent.disableNetConnect()
|
||||
|
||||
mockAgent
|
||||
.get('https://example.com')
|
||||
.intercept({ method: 'GET', path: '/' })
|
||||
.reply(200)
|
||||
|
||||
agent.assertNoPendingInterceptors()
|
||||
mockAgent.assertNoPendingInterceptors()
|
||||
// Throws an UndiciError with the following message:
|
||||
//
|
||||
// 1 interceptor is pending:
|
||||
@@ -545,59 +561,35 @@ agent.assertNoPendingInterceptors()
|
||||
// └─────────┴────────┴───────────────────────┴──────┴─────────────┴────────────┴─────────────┴───────────┘
|
||||
```
|
||||
|
||||
#### Example - access call history on MockAgent
|
||||
### `mockAgent.isMockActive`
|
||||
|
||||
You can register every call made within a MockAgent to be able to retrieve the body, headers and so on.
|
||||
<!-- YAML
|
||||
added: v5.3.0
|
||||
-->
|
||||
|
||||
This is not enabled by default.
|
||||
* Type: {boolean} `true` while mocking is active, `false` after
|
||||
[`mockAgent.deactivate()`][] has been called.
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
A read-only property indicating whether mocking is currently active on the
|
||||
`MockAgent`.
|
||||
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true })
|
||||
setGlobalDispatcher(mockAgent)
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
await request('http://example.com', { query: { item: 1 }})
|
||||
|
||||
mockAgent.getCallHistory()?.firstCall()
|
||||
// Returns
|
||||
// MockCallHistoryLog {
|
||||
// body: undefined,
|
||||
// headers: undefined,
|
||||
// method: 'GET',
|
||||
// origin: 'http://example.com',
|
||||
// fullUrl: 'http://example.com/?item=1',
|
||||
// path: '/',
|
||||
// searchParams: { item: '1' },
|
||||
// protocol: 'http:',
|
||||
// host: 'example.com',
|
||||
// port: ''
|
||||
// }
|
||||
```
|
||||
|
||||
#### Example - clear call history
|
||||
|
||||
```js
|
||||
const mockAgent = new MockAgent()
|
||||
|
||||
mockAgent.clearCallHistory()
|
||||
console.log(mockAgent.isMockActive) // true
|
||||
|
||||
mockAgent.deactivate()
|
||||
|
||||
console.log(mockAgent.isMockActive) // false
|
||||
```
|
||||
|
||||
#### Example - call history instance class method
|
||||
|
||||
```js
|
||||
const mockAgent = new MockAgent()
|
||||
|
||||
const mockAgentHistory = mockAgent.getCallHistory()
|
||||
|
||||
mockAgentHistory?.calls() // returns an array of MockCallHistoryLogs
|
||||
mockAgentHistory?.firstCall() // returns the first MockCallHistoryLogs or undefined
|
||||
mockAgentHistory?.lastCall() // returns the last MockCallHistoryLogs or undefined
|
||||
mockAgentHistory?.nthCall(3) // returns the third MockCallHistoryLogs or undefined
|
||||
mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint OR hash === #hash-value
|
||||
mockAgentHistory?.filterCalls({ path: '/endpoint', hash: '#hash-value' }, { operator: 'AND' }) // returns an Array of MockCallHistoryLogs WHERE path === /endpoint AND hash === #hash-value
|
||||
mockAgentHistory?.filterCalls(/"data": "{}"/) // returns an Array of MockCallHistoryLogs where any value match regexp
|
||||
mockAgentHistory?.filterCalls('application/json') // returns an Array of MockCallHistoryLogs where any value === 'application/json'
|
||||
mockAgentHistory?.filterCalls((log) => log.path === '/endpoint') // returns an Array of MockCallHistoryLogs when given function returns true
|
||||
mockAgentHistory?.clear() // clear the history
|
||||
```
|
||||
[`Dispatcher.dispatch()`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`mockAgent.deactivate()`]: #mockagentdeactivate
|
||||
[`mockAgent.enableCallHistory()`]: #mockagentenablecallhistory
|
||||
[`mockAgent.get(origin)`]: #mockagentgetorigin
|
||||
[`mockAgent.getCallHistory()`]: #mockagentgetcallhistory
|
||||
[`mockAgent.pendingInterceptors()`]: #mockagentpendinginterceptors
|
||||
[`mockAgent.request()`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`setGlobalDispatcher()`]: Dispatcher.md#setglobaldispatcherdispatcher
|
||||
+276
-102
@@ -1,197 +1,371 @@
|
||||
# Class: MockCallHistory
|
||||
# MockCallHistory
|
||||
|
||||
Access to an instance with :
|
||||
<!--introduced_in=v7.5.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/mock/mock-call-history.js -->
|
||||
|
||||
> Stability: 2 - Stable
|
||||
|
||||
A `MockCallHistory` records the configuration of every request intercepted by a
|
||||
[`MockAgent`][] for which call history is enabled. Each recorded request is
|
||||
stored as a [`MockCallHistoryLog`][], and the `MockCallHistory` exposes helpers
|
||||
to read and filter those logs in tests.
|
||||
|
||||
Instances are not created directly. Enable call history on a [`MockAgent`][] and
|
||||
retrieve the history with `mockAgent.getCallHistory()`:
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
```js
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true })
|
||||
mockAgent.getCallHistory()
|
||||
|
||||
// or
|
||||
const mockAgent = new MockAgent()
|
||||
mockAgent.enableMockHistory()
|
||||
mockAgent.getCallHistory()
|
||||
|
||||
const callHistory = mockAgent.getCallHistory()
|
||||
```
|
||||
|
||||
a MockCallHistory instance implements a **Symbol.iterator** letting you iterate on registered logs :
|
||||
Call history can also be enabled after construction:
|
||||
|
||||
```ts
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
mockAgent.enableCallHistory()
|
||||
const callHistory = mockAgent.getCallHistory()
|
||||
```
|
||||
|
||||
## Class: `MockCallHistory`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
Tracks the configuration of intercepted requests as an ordered collection of
|
||||
[`MockCallHistoryLog`][] entries.
|
||||
|
||||
A `MockCallHistory` is iterable. Use it with a `for...of` loop, the spread
|
||||
operator, or any constructor that accepts an iterable:
|
||||
|
||||
```mjs
|
||||
for (const log of mockAgent.getCallHistory()) {
|
||||
//...
|
||||
// ...
|
||||
}
|
||||
|
||||
const array: Array<MockCallHistoryLog> = [...mockAgent.getCallHistory()]
|
||||
const set: Set<MockCallHistoryLog> = new Set(mockAgent.getCallHistory())
|
||||
const logs = [...mockAgent.getCallHistory()]
|
||||
const logSet = new Set(mockAgent.getCallHistory())
|
||||
```
|
||||
|
||||
## class methods
|
||||
### `mockCallHistory.calls()`
|
||||
|
||||
### clear
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
Clear all MockCallHistoryLog registered. This is automatically done when calling `mockAgent.close()`
|
||||
* Returns: {MockCallHistoryLog[]} All recorded logs, in the order the requests
|
||||
were intercepted.
|
||||
|
||||
```js
|
||||
mockAgent.clearCallHistory()
|
||||
// same as
|
||||
mockAgent.getCallHistory()?.clear()
|
||||
```
|
||||
Returns every recorded [`MockCallHistoryLog`][] as an array.
|
||||
|
||||
### calls
|
||||
|
||||
Get all MockCallHistoryLog registered as an array
|
||||
|
||||
```js
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.calls()
|
||||
```
|
||||
|
||||
### firstCall
|
||||
### `mockCallHistory.firstCall()`
|
||||
|
||||
Get the first MockCallHistoryLog registered or undefined
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
```js
|
||||
* Returns: {MockCallHistoryLog|undefined} The first recorded log, or `undefined`
|
||||
when no request has been recorded.
|
||||
|
||||
Returns the first recorded [`MockCallHistoryLog`][].
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.firstCall()
|
||||
```
|
||||
|
||||
### lastCall
|
||||
### `mockCallHistory.lastCall()`
|
||||
|
||||
Get the last MockCallHistoryLog registered or undefined
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
```js
|
||||
* Returns: {MockCallHistoryLog|undefined} The last recorded log, or `undefined`
|
||||
when no request has been recorded.
|
||||
|
||||
Returns the last recorded [`MockCallHistoryLog`][].
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.lastCall()
|
||||
```
|
||||
|
||||
### nthCall
|
||||
### `mockCallHistory.nthCall(position)`
|
||||
|
||||
Get the nth MockCallHistoryLog registered or undefined
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
```js
|
||||
mockAgent.getCallHistory()?.nthCall(3) // the third MockCallHistoryLog registered
|
||||
* `position` {number} The 1-based position of the log to return. Must be a
|
||||
positive integer.
|
||||
* Returns: {MockCallHistoryLog|undefined} The log at `position`, or `undefined`
|
||||
when no log exists at that position.
|
||||
|
||||
Returns the [`MockCallHistoryLog`][] at the given 1-based `position`. The index
|
||||
is 1-based for readability, so `nthCall(1)` is equivalent to `firstCall()`.
|
||||
|
||||
Throws an [`InvalidArgumentError`][] when `position` is not a number, not an
|
||||
integer, or not positive. Use `firstCall()` or `lastCall()` instead of passing a
|
||||
non-positive value.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.nthCall(3) // the third recorded log
|
||||
```
|
||||
|
||||
### filterCallsByProtocol
|
||||
### `mockCallHistory.filterCalls(criteria[, options])`
|
||||
|
||||
Filter MockCallHistoryLog by protocol.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {Function|RegExp|Object} The filter to apply.
|
||||
* When a {Function} is given, it is used as the predicate of
|
||||
`Array.prototype.filter` and receives each [`MockCallHistoryLog`][]; logs for
|
||||
which it returns a truthy value are kept.
|
||||
* When a {RegExp} is given, a log is kept when the expression matches its
|
||||
[`mockCallHistoryLog.toString()`][] representation.
|
||||
* When an {Object} is given, each property is a request field to filter on and
|
||||
each value is a [filter parameter](#filter-parameter). An empty object
|
||||
returns every log.
|
||||
* `protocol` {string|RegExp|null|undefined} Filters by request protocol.
|
||||
* `host` {string|RegExp|null|undefined} Filters by request host.
|
||||
* `port` {string|RegExp|null|undefined} Filters by request port.
|
||||
* `origin` {string|RegExp|null|undefined} Filters by request origin.
|
||||
* `path` {string|RegExp|null|undefined} Filters by request path.
|
||||
* `hash` {string|RegExp|null|undefined} Filters by request hash.
|
||||
* `fullUrl` {string|RegExp|null|undefined} Filters by request full URL.
|
||||
* `method` {string|RegExp|null|undefined} Filters by request method.
|
||||
* `options` {Object} Adjusts the filtering behavior. Only applied when `criteria`
|
||||
is an object. (optional)
|
||||
* `operator` {string} How to combine multiple object properties. `'OR'` keeps
|
||||
a log matching any criterion; `'AND'` keeps a log matching every criterion.
|
||||
The value is case-insensitive. **Default:** `'OR'`.
|
||||
* Returns: {MockCallHistoryLog[]} The matching logs. Duplicates produced by an
|
||||
`'OR'` combination are removed.
|
||||
|
||||
```js
|
||||
A convenience method for applying one or several filters at once. It is a more
|
||||
expressive alternative to the per-field helpers below.
|
||||
|
||||
Throws an [`InvalidArgumentError`][] when `criteria` is not a function, regular
|
||||
expression, or object, or when `options.operator` is neither `'OR'` nor `'AND'`.
|
||||
|
||||
```mjs
|
||||
const callHistory = mockAgent.getCallHistory()
|
||||
|
||||
callHistory?.filterCalls((log) =>
|
||||
log.hash === '#hash' && log.headers?.authorization !== undefined)
|
||||
|
||||
callHistory?.filterCalls(/"errors": "wrong body"/)
|
||||
|
||||
// Logs with a hash containing `my-hash` OR a path equal to `/endpoint`.
|
||||
callHistory?.filterCalls({ hash: /my-hash/, path: '/endpoint' })
|
||||
|
||||
// Logs with a hash containing `my-hash` AND a path equal to `/endpoint`.
|
||||
callHistory?.filterCalls(
|
||||
{ hash: /my-hash/, path: '/endpoint' },
|
||||
{ operator: 'AND' }
|
||||
)
|
||||
```
|
||||
|
||||
### `mockCallHistory.filterCallsByProtocol(criteria)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose protocol matches `criteria`.
|
||||
|
||||
Filters the recorded logs by request protocol.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByProtocol(/https/)
|
||||
mockAgent.getCallHistory()?.filterCallsByProtocol('https:')
|
||||
```
|
||||
|
||||
### filterCallsByHost
|
||||
### `mockCallHistory.filterCallsByHost(criteria)`
|
||||
|
||||
Filter MockCallHistoryLog by host.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose host matches `criteria`.
|
||||
|
||||
```js
|
||||
Filters the recorded logs by request host.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByHost(/localhost/)
|
||||
mockAgent.getCallHistory()?.filterCallsByHost('localhost:3000')
|
||||
```
|
||||
|
||||
### filterCallsByPort
|
||||
### `mockCallHistory.filterCallsByPort(criteria)`
|
||||
|
||||
Filter MockCallHistoryLog by port.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose port matches `criteria`.
|
||||
|
||||
```js
|
||||
Filters the recorded logs by request port.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByPort(/3000/)
|
||||
mockAgent.getCallHistory()?.filterCallsByPort('3000')
|
||||
mockAgent.getCallHistory()?.filterCallsByPort('')
|
||||
```
|
||||
|
||||
### filterCallsByOrigin
|
||||
### `mockCallHistory.filterCallsByOrigin(criteria)`
|
||||
|
||||
Filter MockCallHistoryLog by origin.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose origin matches `criteria`.
|
||||
|
||||
```js
|
||||
Filters the recorded logs by request origin.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByOrigin(/http:\/\/localhost:3000/)
|
||||
mockAgent.getCallHistory()?.filterCallsByOrigin('http://localhost:3000')
|
||||
```
|
||||
|
||||
### filterCallsByPath
|
||||
### `mockCallHistory.filterCallsByPath(criteria)`
|
||||
|
||||
Filter MockCallHistoryLog by path.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose path matches `criteria`.
|
||||
|
||||
```js
|
||||
Filters the recorded logs by request path.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByPath(/api\/v1\/graphql/)
|
||||
mockAgent.getCallHistory()?.filterCallsByPath('/api/v1/graphql')
|
||||
```
|
||||
|
||||
### filterCallsByHash
|
||||
### `mockCallHistory.filterCallsByHash(criteria)`
|
||||
|
||||
Filter MockCallHistoryLog by hash.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose hash matches `criteria`.
|
||||
|
||||
```js
|
||||
mockAgent.getCallHistory()?.filterCallsByPath(/hash/)
|
||||
mockAgent.getCallHistory()?.filterCallsByPath('#hash')
|
||||
Filters the recorded logs by request hash.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByHash(/hash/)
|
||||
mockAgent.getCallHistory()?.filterCallsByHash('#hash')
|
||||
```
|
||||
|
||||
### filterCallsByFullUrl
|
||||
### `mockCallHistory.filterCallsByFullUrl(criteria)`
|
||||
|
||||
Filter MockCallHistoryLog by fullUrl. fullUrl contains protocol, host, port, path, hash, and query params
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose full URL matches `criteria`.
|
||||
|
||||
```js
|
||||
Filters the recorded logs by request full URL. The full URL contains the
|
||||
protocol, host, port, path, query parameters, and hash.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByFullUrl(/https:\/\/localhost:3000\/\?query=value#hash/)
|
||||
mockAgent.getCallHistory()?.filterCallsByFullUrl('https://localhost:3000/?query=value#hash')
|
||||
```
|
||||
|
||||
### filterCallsByMethod
|
||||
### `mockCallHistory.filterCallsByMethod(criteria)`
|
||||
|
||||
Filter MockCallHistoryLog by method.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
> more details for the first parameter can be found [here](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
* `criteria` {string|RegExp|null|undefined} See
|
||||
[filter parameter](#filter-parameter).
|
||||
* Returns: {MockCallHistoryLog[]} The logs whose method matches `criteria`.
|
||||
|
||||
```js
|
||||
Filters the recorded logs by request method.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.filterCallsByMethod(/POST/)
|
||||
mockAgent.getCallHistory()?.filterCallsByMethod('POST')
|
||||
```
|
||||
|
||||
### filterCalls
|
||||
### `mockCallHistory.clear()`
|
||||
|
||||
This class method is a meta function / alias to apply complex filtering in a single way.
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
Parameters :
|
||||
* Returns: {undefined}
|
||||
|
||||
- criteria : the first parameter. a function, regexp or object.
|
||||
- function : filter MockCallHistoryLog when the function returns false
|
||||
- regexp : filter MockCallHistoryLog when the regexp does not match on MockCallHistoryLog.toString() ([see](/docs/docs/api/MockCallHistoryLog.md#to-string))
|
||||
- object : an object with MockCallHistoryLog properties as keys to apply multiple filters. each values are a [filter parameter](/docs/docs/api/MockCallHistory.md#filter-parameter)
|
||||
- options : the second parameter. an object.
|
||||
- options.operator : `'AND'` or `'OR'` (default `'OR'`). Used only if criteria is an object. see below
|
||||
Removes every recorded [`MockCallHistoryLog`][]. This is performed
|
||||
automatically when `mockAgent.close()` is called.
|
||||
|
||||
```js
|
||||
mockAgent.getCallHistory()?.filterCalls((log) => log.hash === value && log.headers?.['authorization'] !== undefined)
|
||||
mockAgent.getCallHistory()?.filterCalls(/"data": "{ "errors": "wrong body" }"/)
|
||||
|
||||
// returns an Array of MockCallHistoryLog which all have
|
||||
// - a hash containing my-hash
|
||||
// - OR
|
||||
// - a path equal to /endpoint
|
||||
mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' })
|
||||
|
||||
// returns an Array of MockCallHistoryLog which all have
|
||||
// - a hash containing my-hash
|
||||
// - AND
|
||||
// - a path equal to /endpoint
|
||||
mockAgent.getCallHistory()?.filterCalls({ hash: /my-hash/, path: '/endpoint' }, { operator: 'AND' })
|
||||
```mjs
|
||||
mockAgent.clearCallHistory()
|
||||
// Equivalent to:
|
||||
mockAgent.getCallHistory()?.clear()
|
||||
```
|
||||
|
||||
## filter parameter
|
||||
### `mockCallHistory[Symbol.iterator]()`
|
||||
|
||||
Can be :
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
- string. MockCallHistoryLog filtered if `value !== parameterValue`
|
||||
- null. MockCallHistoryLog filtered if `value !== parameterValue`
|
||||
- undefined. MockCallHistoryLog filtered if `value !== parameterValue`
|
||||
- regexp. MockCallHistoryLog filtered if `!parameterValue.test(value)`
|
||||
* Returns: {Generator} A generator yielding each recorded
|
||||
[`MockCallHistoryLog`][] in order.
|
||||
|
||||
Makes a `MockCallHistory` iterable, yielding the same logs returned by
|
||||
`calls()`. This enables `for...of` iteration, the spread operator, and
|
||||
constructing other iterables from a history.
|
||||
|
||||
```mjs
|
||||
const callHistory = mockAgent.getCallHistory()
|
||||
|
||||
for (const log of callHistory) {
|
||||
console.log(log.method, log.fullUrl)
|
||||
}
|
||||
|
||||
const logs = [...callHistory]
|
||||
```
|
||||
|
||||
## Filter parameter
|
||||
|
||||
The per-field `filterCallsBy*` helpers and the object form of `filterCalls()`
|
||||
accept a filter parameter that is matched against the corresponding field of each
|
||||
[`MockCallHistoryLog`][]:
|
||||
|
||||
* A {string} keeps a log only when the field is strictly equal to the value.
|
||||
* `null` keeps a log only when the field is strictly equal to `null`.
|
||||
* `undefined` keeps a log only when the field is strictly equal to `undefined`.
|
||||
* A {RegExp} keeps a log only when the expression matches the field.
|
||||
|
||||
Any other value throws an [`InvalidArgumentError`][].
|
||||
|
||||
[`InvalidArgumentError`]: Errors.md#class-invalidargumenterror
|
||||
[`MockAgent`]: MockAgent.md#class-mockagent
|
||||
[`MockCallHistoryLog`]: MockCallHistoryLog.md#class-mockcallhistorylog
|
||||
[`mockCallHistoryLog.toString()`]: MockCallHistoryLog.md#mockcallhistorylogtostring
|
||||
+189
-26
@@ -1,43 +1,206 @@
|
||||
# Class: MockCallHistoryLog
|
||||
# MockCallHistoryLog
|
||||
|
||||
Access to an instance with :
|
||||
<!--introduced_in=v7.5.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/mock/mock-call-history.js -->
|
||||
|
||||
> Stability: 2 - Stable
|
||||
|
||||
A `MockCallHistoryLog` is an immutable snapshot of the configuration of a single
|
||||
request that was intercepted by a [`MockAgent`][]. Each property mirrors a part
|
||||
of the request, with the URL components already parsed by the WHATWG URL parser.
|
||||
|
||||
Instances are not created directly. They are produced by a [`MockCallHistory`][]
|
||||
when call history is enabled on a [`MockAgent`][], and can be retrieved through
|
||||
the call history helpers such as `firstCall()`, `lastCall()`, `nthCall()`,
|
||||
`calls()`, and `filterCalls()`.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
```js
|
||||
const mockAgent = new MockAgent({ enableCallHistory: true })
|
||||
mockAgent.getCallHistory()?.firstCall()
|
||||
|
||||
// after some requests have been intercepted:
|
||||
const log = mockAgent.getCallHistory()?.firstCall()
|
||||
```
|
||||
|
||||
## class properties
|
||||
## Class: `MockCallHistoryLog`
|
||||
|
||||
- body `mockAgent.getCallHistory()?.firstCall()?.body`
|
||||
- headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
|
||||
- method `mockAgent.getCallHistory()?.firstCall()?.method` a string
|
||||
- fullUrl `mockAgent.getCallHistory()?.firstCall()?.fullUrl` a string containing the protocol, origin, path, query and hash
|
||||
- origin `mockAgent.getCallHistory()?.firstCall()?.origin` a string containing the protocol and the host
|
||||
- headers `mockAgent.getCallHistory()?.firstCall()?.headers` an object
|
||||
- path `mockAgent.getCallHistory()?.firstCall()?.path` a string always starting with `/`
|
||||
- searchParams `mockAgent.getCallHistory()?.firstCall()?.searchParams` an object
|
||||
- protocol `mockAgent.getCallHistory()?.firstCall()?.protocol` a string (`https:`)
|
||||
- host `mockAgent.getCallHistory()?.firstCall()?.host` a string
|
||||
- port `mockAgent.getCallHistory()?.firstCall()?.port` an empty string or a string containing numbers
|
||||
- hash `mockAgent.getCallHistory()?.firstCall()?.hash` an empty string or a string starting with `#`
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
## class methods
|
||||
Represents the configuration of one intercepted request.
|
||||
|
||||
### toMap
|
||||
### `new MockCallHistoryLog(requestInit)`
|
||||
|
||||
Returns a Map instance
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
```js
|
||||
mockAgent.getCallHistory()?.firstCall()?.toMap()?.get('hash')
|
||||
// #hash
|
||||
* `requestInit` {DispatchOptions} The dispatch options of the intercepted
|
||||
request. **Default:** `{}`.
|
||||
|
||||
Creates a new `MockCallHistoryLog`. The `body`, `headers`, and `method`
|
||||
properties are copied directly from `requestInit`, while the URL-derived
|
||||
properties (`fullUrl`, `origin`, `path`, `searchParams`, `protocol`, `host`,
|
||||
`port`, and `hash`) are computed from `requestInit.path` and `requestInit.origin`
|
||||
using the WHATWG URL parser.
|
||||
|
||||
This constructor is internal-facing: instances are created by
|
||||
[`MockCallHistory`][] when a request is intercepted. It is documented because the
|
||||
class is part of the public API surface.
|
||||
|
||||
### `mockCallHistoryLog.body`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string|null|undefined}
|
||||
|
||||
The body of the intercepted request, exactly as supplied to the dispatcher.
|
||||
|
||||
### `mockCallHistoryLog.headers`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {Record<string, string|string[]>|null|undefined}
|
||||
|
||||
The headers of the intercepted request.
|
||||
|
||||
### `mockCallHistoryLog.method`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The HTTP method of the intercepted request, for example `'GET'` or `'PUT'`.
|
||||
|
||||
### `mockCallHistoryLog.fullUrl`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The full URL of the intercepted request, including protocol, host, path, search
|
||||
parameters, and hash.
|
||||
|
||||
### `mockCallHistoryLog.origin`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The origin of the intercepted request, comprising the protocol and host, for
|
||||
example `'https://localhost:4000'`.
|
||||
|
||||
### `mockCallHistoryLog.path`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The pathname of the intercepted request. It always starts with `/` and never
|
||||
contains search parameters.
|
||||
|
||||
### `mockCallHistoryLog.searchParams`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {Record<string, string>}
|
||||
|
||||
The search parameters of the intercepted request as a plain object of key/value
|
||||
pairs.
|
||||
|
||||
### `mockCallHistoryLog.protocol`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The protocol of the intercepted request, including the trailing colon, for
|
||||
example `'https:'` or `'http:'`.
|
||||
|
||||
### `mockCallHistoryLog.host`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The host of the intercepted request, including the port when present.
|
||||
|
||||
### `mockCallHistoryLog.port`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The port of the intercepted request. It is an empty string when no explicit port
|
||||
was used, otherwise a string of digits.
|
||||
|
||||
### `mockCallHistoryLog.hash`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The hash fragment of the intercepted request. It is an empty string when absent,
|
||||
otherwise a string starting with `#`.
|
||||
|
||||
### `mockCallHistoryLog.toMap()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
* Returns: {Map} A `Map` whose keys are the property names `protocol`, `host`,
|
||||
`port`, `origin`, `path`, `hash`, `searchParams`, `fullUrl`, `method`, `body`,
|
||||
and `headers`, each mapped to the corresponding value of this log.
|
||||
|
||||
Returns the log as a `Map` of property name to value.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.firstCall()?.toMap().get('hash')
|
||||
// '#hash'
|
||||
```
|
||||
|
||||
### toString
|
||||
### `mockCallHistoryLog.toString()`
|
||||
|
||||
Returns a string computed with any class property name and value pair
|
||||
<!-- YAML
|
||||
added: v7.5.0
|
||||
-->
|
||||
|
||||
```js
|
||||
* Returns: {string} A string representation of the log.
|
||||
|
||||
Returns a string built from every property name and value pair. Each pair is
|
||||
joined with `->` and pairs are separated with `|`. Object values such as
|
||||
`searchParams` and `headers` are serialized with `JSON.stringify()`.
|
||||
|
||||
```mjs
|
||||
mockAgent.getCallHistory()?.firstCall()?.toString()
|
||||
// protocol->https:|host->localhost:4000|port->4000|origin->https://localhost:4000|path->/endpoint|hash->#here|searchParams->{"query":"value"}|fullUrl->https://localhost:4000/endpoint?query=value#here|method->PUT|body->"{ "data": "hello" }"|headers->{"content-type":"application/json"}
|
||||
```
|
||||
|
||||
[`MockAgent`]: MockAgent.md#class-mockagent
|
||||
[`MockCallHistory`]: MockCallHistory.md#class-mockcallhistory
|
||||
+158
-40
@@ -1,73 +1,183 @@
|
||||
# Class: MockClient
|
||||
# MockClient
|
||||
|
||||
Extends: `undici.Client`
|
||||
<!--introduced_in=v4.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/mock/mock-client.js -->
|
||||
|
||||
A mock client class that implements the same api as [MockPool](/docs/docs/api/MockPool.md).
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new MockClient(origin, [options])`
|
||||
A `MockClient` is a [`Client`][] that intercepts requests matching registered
|
||||
routes and replies with mocked responses instead of contacting the network.
|
||||
It is created by a [`MockAgent`][] when the agent is configured with a single
|
||||
connection, and it exposes the same interception API as [`MockPool`][].
|
||||
|
||||
Arguments:
|
||||
A `MockClient` is not constructed directly in most cases; instead it is obtained
|
||||
through [`mockAgent.get(origin)`][] once the agent has `connections` set to `1`.
|
||||
|
||||
* **origin** `string` - It should only include the **protocol, hostname, and port**.
|
||||
* **options** `MockClientOptions` - It extends the `Client` options.
|
||||
|
||||
Returns: `MockClient`
|
||||
|
||||
### Parameter: `MockClientOptions`
|
||||
|
||||
Extends: `ClientOptions`
|
||||
|
||||
* **agent** `Agent` - the agent to associate this MockClient with.
|
||||
|
||||
### Example - Basic MockClient instantiation
|
||||
|
||||
We can use MockAgent to instantiate a MockClient ready to be used to intercept specified requests. It will not do anything until registered as the agent to use and any mock request are registered.
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
// Connections must be set to 1 to return a MockClient instance
|
||||
// `connections: 1` makes the agent hand out `MockClient` instances.
|
||||
const mockAgent = new MockAgent({ connections: 1 })
|
||||
|
||||
const mockClient = mockAgent.get('http://localhost:3000')
|
||||
```
|
||||
|
||||
## Instance Methods
|
||||
```cjs
|
||||
const { MockAgent } = require('undici')
|
||||
|
||||
### `MockClient.intercept(options)`
|
||||
const mockAgent = new MockAgent({ connections: 1 })
|
||||
const mockClient = mockAgent.get('http://localhost:3000')
|
||||
```
|
||||
|
||||
Implements: [`MockPool.intercept(options)`](/docs/docs/api/MockPool.md#mockpoolinterceptoptions)
|
||||
## Class: `MockClient`
|
||||
|
||||
### `MockClient.cleanMocks()`
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
Implements: [`MockPool.cleanMocks()`](/docs/docs/api/MockPool.md#mockpoolcleanmocks)
|
||||
* Extends: {Client}
|
||||
|
||||
### `MockClient.close()`
|
||||
Extends [`Client`][] and implements the `Interceptable` interface, allowing
|
||||
requests made through it to be matched against registered mocks.
|
||||
|
||||
Implements: [`MockPool.close()`](/docs/docs/api/MockPool.md#mockpoolclose)
|
||||
### `new MockClient(origin[, options])`
|
||||
|
||||
### `MockClient.dispatch(options, handlers)`
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
* `origin` {string} The origin to associate this mock client with. It should
|
||||
only include the protocol, hostname, and port.
|
||||
* `options` {MockClientOptions} Extends the [`Client`][] options.
|
||||
* Returns: {MockClient}
|
||||
|
||||
### `MockClient.request(options[, callback])`
|
||||
The `agent` option is required and must implement the `Agent` interface;
|
||||
otherwise an `InvalidArgumentError` is thrown.
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
#### Parameter: `MockClientOptions`
|
||||
|
||||
#### Example - MockClient request
|
||||
Extends: `ClientOptions`
|
||||
|
||||
```js
|
||||
* `agent` {MockAgent} The agent to associate this mock client with.
|
||||
* `ignoreTrailingSlash` {boolean} Whether trailing slashes should be ignored
|
||||
when matching the path of intercepted requests. **Default:** `false`.
|
||||
|
||||
### `mockClient.intercept(options)`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* `options` {MockInterceptor.Options} The matching criteria for the requests to
|
||||
intercept.
|
||||
* `path` {string|RegExp|Function} The path to match against. A function
|
||||
receives the request path as a `string` and returns a `boolean`.
|
||||
* `method` {string|RegExp|Function} The method to match against. A function
|
||||
receives the request method as a `string` and returns a `boolean`.
|
||||
**Default:** `'GET'`.
|
||||
* `body` {string|RegExp|Function} The body to match against. A function
|
||||
receives the request body as a `string` and returns a `boolean`.
|
||||
* `headers` {Object|Function} The headers to match against, as a map of header
|
||||
name to a `string`, `RegExp`, or matching function, or a single function
|
||||
that receives all headers and returns a `boolean`.
|
||||
* `query` {Object} The query parameters to match against.
|
||||
* `ignoreTrailingSlash` {boolean} Whether a trailing slash on `path` is
|
||||
ignored when matching. Inherited from the mock client's
|
||||
`ignoreTrailingSlash` option when not set.
|
||||
* Returns: {MockInterceptor} The interceptor used to define the mocked reply.
|
||||
|
||||
Registers a route on the mock client and returns a [`MockInterceptor`][] for any
|
||||
matching request that uses the same origin as this mock client. The interceptor
|
||||
is then used to define the reply, for example with `reply()` or
|
||||
`replyWithError()`.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ connections: 1 })
|
||||
const mockClient = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockClient.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo')
|
||||
```
|
||||
|
||||
### `mockClient.cleanMocks()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.11.0
|
||||
-->
|
||||
|
||||
Removes all registered interceptors from the mock client. Pending mocks defined
|
||||
before this call no longer match incoming requests.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ connections: 1 })
|
||||
const mockClient = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockClient.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
mockClient.cleanMocks()
|
||||
```
|
||||
|
||||
### `mockClient.close()`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Fulfills with `undefined` once the mock client is closed.
|
||||
|
||||
Closes the mock client, gracefully waiting for any enqueued requests to
|
||||
complete, and removes it from the associated [`MockAgent`][].
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ connections: 1 })
|
||||
const mockClient = mockAgent.get('http://localhost:3000')
|
||||
|
||||
await mockClient.close()
|
||||
```
|
||||
|
||||
### `mockClient.dispatch(options, handlers)`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* `options` {DispatchOptions} The request options.
|
||||
* `handlers` {DispatchHandler} The handlers invoked over the request lifecycle.
|
||||
* Returns: {boolean} `false` if the dispatcher is busy and the caller should
|
||||
wait before dispatching further requests, otherwise `true`.
|
||||
|
||||
Dispatches a request, matching it against the registered mocks. This override of
|
||||
[`dispatcher.dispatch(options, handlers)`][] is what drives the mocking
|
||||
behaviour for every higher-level method such as `request`.
|
||||
|
||||
### `mockClient.request(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* `options` {DispatchOptions}
|
||||
* `callback` {Function} (optional) Invoked with the response when no `Promise`
|
||||
is requested.
|
||||
* Returns: {Promise} Fulfills with the mocked response when no `callback` is
|
||||
provided.
|
||||
|
||||
Performs a request and resolves it against the registered mocks. Inherited from
|
||||
[`Client`][]; see [`dispatcher.request(options[, callback])`][] for the full
|
||||
parameter and return value documentation.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent({ connections: 1 })
|
||||
const mockClient = mockAgent.get('http://localhost:3000')
|
||||
mockClient.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await mockClient.request({
|
||||
const { statusCode, body } = await mockClient.request({
|
||||
origin: 'http://localhost:3000',
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
@@ -79,3 +189,11 @@ for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
```
|
||||
|
||||
[`Client`]: Client.md#class-client
|
||||
[`MockAgent`]: MockAgent.md#class-mockagent
|
||||
[`MockInterceptor`]: MockPool.md#return-mockinterceptor
|
||||
[`MockPool`]: MockPool.md#class-mockpool
|
||||
[`dispatcher.dispatch(options, handlers)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`mockAgent.get(origin)`]: MockAgent.md#mockagentgetorigin
|
||||
+56
-5
@@ -1,12 +1,63 @@
|
||||
# MockErrors
|
||||
|
||||
Undici exposes a variety of mock error objects that you can use to enhance your mock error handling.
|
||||
You can find all the mock error objects inside the `mockErrors` key.
|
||||
<!--introduced_in=v4.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/mock/mock-errors.js -->
|
||||
|
||||
> Stability: 2 - Stable
|
||||
|
||||
undici raises a dedicated error when a mocked request cannot be matched against
|
||||
any registered interceptor. The mock error classes are exposed through the
|
||||
`mockErrors` namespace of the package:
|
||||
|
||||
```js
|
||||
import { mockErrors } from 'undici'
|
||||
|
||||
if (err instanceof mockErrors.MockNotMatchedError) {
|
||||
// handle an unmatched mocked request
|
||||
}
|
||||
```
|
||||
|
||||
| Mock Error | Mock Error Codes | Description |
|
||||
| --------------------- | ------------------------------- | ---------------------------------------------------------- |
|
||||
| `MockNotMatchedError` | `UND_MOCK_ERR_MOCK_NOT_MATCHED` | The request does not match any registered mock dispatches. |
|
||||
Like every other undici error, the classes in this namespace extend
|
||||
[`UndiciError`][] and carry a stable `code` string, so failures can be
|
||||
distinguished programmatically without relying on the error message.
|
||||
|
||||
## Class: `MockNotMatchedError`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Extends: {UndiciError}
|
||||
|
||||
Thrown when a request issued against a [`MockAgent`][] does not match any of the
|
||||
registered mock dispatches and the agent is configured to disable net connect.
|
||||
|
||||
* `name` {string} Always `'MockNotMatchedError'`.
|
||||
* `code` {string} Always `'UND_MOCK_ERR_MOCK_NOT_MATCHED'`.
|
||||
|
||||
### `new MockNotMatchedError([message])`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* `message` {string} The error message. **Default:** `'The request does not match any registered mock dispatches'`.
|
||||
|
||||
Creates a new `MockNotMatchedError`. When `message` is omitted, a default
|
||||
message describing the unmatched request is used.
|
||||
|
||||
```js
|
||||
import { mockErrors } from 'undici'
|
||||
|
||||
try {
|
||||
// ... perform a request that no mock matches ...
|
||||
} catch (err) {
|
||||
if (err.code === 'UND_MOCK_ERR_MOCK_NOT_MATCHED') {
|
||||
console.error('No registered mock matched the request')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
[`MockAgent`]: MockAgent.md#class-mockagent
|
||||
[`UndiciError`]: Errors.md#class-undicierror
|
||||
+319
-283
@@ -1,109 +1,209 @@
|
||||
# Class: MockPool
|
||||
# MockPool
|
||||
|
||||
Extends: `undici.Pool`
|
||||
<!--introduced_in=v4.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/mock/mock-pool.js -->
|
||||
|
||||
A mock Pool class that implements the Pool API and is used by MockAgent to intercept real requests and return mocked responses.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new MockPool(origin, [options])`
|
||||
A `MockPool` is a [`Pool`][] that intercepts requests matching registered routes
|
||||
and replies with mocked responses instead of contacting the network. It is
|
||||
created by a [`MockAgent`][] and exposes the same interception API as
|
||||
[`MockClient`][].
|
||||
|
||||
Arguments:
|
||||
A `MockPool` is not constructed directly in most cases; instead it is obtained
|
||||
through [`mockAgent.get(origin)`][]. Once registered as the global dispatcher,
|
||||
any request whose origin matches the pool is matched against the mocks it holds.
|
||||
|
||||
* **origin** `string` - It should only include the **protocol, hostname, and port**.
|
||||
* **options** `MockPoolOptions` - It extends the `Pool` options.
|
||||
|
||||
Returns: `MockPool`
|
||||
|
||||
### Parameter: `MockPoolOptions`
|
||||
|
||||
Extends: `PoolOptions`
|
||||
|
||||
* **agent** `Agent` - the agent to associate this MockPool with.
|
||||
|
||||
### Example - Basic MockPool instantiation
|
||||
|
||||
We can use MockAgent to instantiate a MockPool ready to be used to intercept specified requests. It will not do anything until registered as the agent to use and any mock request are registered.
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
```
|
||||
|
||||
## Instance Methods
|
||||
```cjs
|
||||
const { MockAgent } = require('undici')
|
||||
|
||||
### `MockPool.intercept(options)`
|
||||
const mockAgent = new MockAgent()
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
```
|
||||
|
||||
This method defines the interception rules for matching against requests for a MockPool or MockPool. We can intercept multiple times on a single instance, but each intercept is only used once. For example if you expect to make 2 requests inside a test, you need to call `intercept()` twice. Assuming you use `disableNetConnect()` you will get `MockNotMatchedError` on the second request when you only call `intercept()` once.
|
||||
## Class: `MockPool`
|
||||
|
||||
When defining interception rules, all the rules must pass for a request to be intercepted. If a request is not intercepted, a real request will be attempted.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
| Matcher type | Condition to pass |
|
||||
|:------------:| -------------------------- |
|
||||
| `string` | Exact match against string |
|
||||
| `RegExp` | Regex must pass |
|
||||
| `Function` | Function must return true |
|
||||
* Extends: {Pool}
|
||||
|
||||
Arguments:
|
||||
Extends [`Pool`][] and implements the `Interceptable` interface, allowing
|
||||
requests made through it to be matched against registered mocks.
|
||||
|
||||
* **options** `MockPoolInterceptOptions` - Interception options.
|
||||
### `new MockPool(origin[, options])`
|
||||
|
||||
Returns: `MockInterceptor` corresponding to the input options.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
### Parameter: `MockPoolInterceptOptions`
|
||||
* `origin` {string} The origin to associate this mock pool with. It should only
|
||||
include the protocol, hostname, and port.
|
||||
* `options` {MockPoolOptions} Extends the [`Pool`][] options.
|
||||
* Returns: {MockPool}
|
||||
|
||||
* **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. When a `RegExp` or callback is used, it will match against the request path including all query parameters in alphabetical order. When a `string` is provided, the query parameters can be conveniently specified through the `MockPoolInterceptOptions.query` setting.
|
||||
* **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`.
|
||||
* **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body.
|
||||
* **headers** `Record<string, string | RegExp | (body: string) => boolean`> - (optional) - a matcher for the HTTP request headers. To be intercepted, a request must match all defined headers. Extra headers not defined here may (or may not) be included in the request and do not affect the interception in any way.
|
||||
* **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params. Only applies when a `string` was provided for `MockPoolInterceptOptions.path`.
|
||||
* **ignoreTrailingSlash** `boolean` - (optional) - set to `true` if the matcher should also match by ignoring potential trailing slashes in `MockPoolInterceptOptions.path`.
|
||||
The `agent` option is required and must implement the `Agent` interface;
|
||||
otherwise an `InvalidArgumentError` is thrown.
|
||||
|
||||
### Return: `MockInterceptor`
|
||||
#### Parameter: `MockPoolOptions`
|
||||
|
||||
We can define the behaviour of an intercepted request with the following options.
|
||||
Extends: `PoolOptions`
|
||||
|
||||
* **reply** `(statusCode: number, replyData: string | Buffer | object | MockInterceptor.MockResponseDataHandler, responseOptions?: MockResponseOptions) => MockScope` - define a reply for a matching request. You can define the replyData as a callback to read incoming request data. Default for `responseOptions` is `{}`.
|
||||
* **reply** `(callback: MockInterceptor.MockReplyOptionsCallback) => MockScope` - define a reply for a matching request, allowing dynamic mocking of all reply options rather than just the data.
|
||||
* **replyWithError** `(error: Error) => MockScope` - define an error for a matching request to throw.
|
||||
* **defaultReplyHeaders** `(headers: Record<string, string>) => MockInterceptor` - define default headers to be included in subsequent replies. These are in addition to headers on a specific reply.
|
||||
* **defaultReplyTrailers** `(trailers: Record<string, string>) => MockInterceptor` - define default trailers to be included in subsequent replies. These are in addition to trailers on a specific reply.
|
||||
* **replyContentLength** `() => MockInterceptor` - define automatically calculated `content-length` headers to be included in subsequent replies.
|
||||
* `agent` {MockAgent} The agent to associate this mock pool with.
|
||||
* `ignoreTrailingSlash` {boolean} Whether trailing slashes should be ignored
|
||||
when matching the path of intercepted requests. **Default:** `false`.
|
||||
|
||||
The reply data of an intercepted request may either be a string, buffer, or JavaScript object. Objects are converted to JSON while strings and buffers are sent as-is.
|
||||
### `mockPool.intercept(options)`
|
||||
|
||||
By default, `reply` and `replyWithError` define the behaviour for the first matching request only. Subsequent requests will not be affected (this can be changed using the returned `MockScope`).
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
### Parameter: `MockResponseOptions`
|
||||
* `options` {MockPoolInterceptOptions} The matching criteria for the requests to
|
||||
intercept.
|
||||
* Returns: {MockInterceptor} The interceptor used to define the mocked reply.
|
||||
|
||||
* **headers** `Record<string, string>` - headers to be included on the mocked reply.
|
||||
* **trailers** `Record<string, string>` - trailers to be included on the mocked reply.
|
||||
Registers a route on the mock pool and returns a [`MockInterceptor`][] for any
|
||||
matching request that uses the same origin as this mock pool. The interceptor is
|
||||
then used to define the reply, for example with `reply()` or `replyWithError()`.
|
||||
|
||||
### Return: `MockScope`
|
||||
Each `intercept()` call is consumed by a single matching request. To match more
|
||||
than one request, call `intercept()` once per expected request, or use
|
||||
`persist()` or `times()` on the returned [`MockScope`][]. When no registered
|
||||
mock matches a request, a real request is attempted unless network connections
|
||||
have been disabled on the [`MockAgent`][], in which case a `MockNotMatchedError`
|
||||
is thrown.
|
||||
|
||||
A `MockScope` is associated with a single `MockInterceptor`. With this, we can configure the default behaviour of an intercepted reply.
|
||||
A request is intercepted only when every defined matcher passes. The accepted
|
||||
matcher forms behave as follows:
|
||||
|
||||
* **delay** `(waitInMs: number) => MockScope` - delay the associated reply by a set amount in ms.
|
||||
* **persist** `() => MockScope` - any matching request will always reply with the defined response indefinitely.
|
||||
* **times** `(repeatTimes: number) => MockScope` - any matching request will reply with the defined response a fixed amount of times. This is overridden by **persist**.
|
||||
| Matcher type | Condition to pass |
|
||||
| :----------: | ---------------------------- |
|
||||
| `string` | Exact match against the value |
|
||||
| `RegExp` | The expression must match |
|
||||
| `Function` | The function must return `true` |
|
||||
|
||||
#### Example - Basic Mocked Request
|
||||
#### Parameter: `MockPoolInterceptOptions`
|
||||
|
||||
```js
|
||||
* `path` {string|RegExp|Function} A matcher for the HTTP request path. The
|
||||
function form has the signature `(path: string) => boolean`. When a `RegExp`
|
||||
or function is used, it matches against the request path including all query
|
||||
parameters in alphabetical order. When a `string` is used, query parameters
|
||||
can instead be supplied through `query`.
|
||||
* `method` {string|RegExp|Function} A matcher for the HTTP request method. The
|
||||
function form has the signature `(method: string) => boolean`.
|
||||
**Default:** `'GET'`.
|
||||
* `body` {string|RegExp|Function} A matcher for the HTTP request body. The
|
||||
function form has the signature `(body: string) => boolean`.
|
||||
* `headers` {Object|Function} A matcher for the HTTP request headers, given
|
||||
either as a map of header name to a `string`, `RegExp`, or
|
||||
`(value: string) => boolean` matcher, or as a single function that receives
|
||||
all headers and returns a `boolean`. When a map is used, the request must
|
||||
match every defined header; extra headers not listed here do not affect
|
||||
matching.
|
||||
* `query` {Object} A matcher for the HTTP request query string parameters. Only
|
||||
applies when a `string` was provided for `path`.
|
||||
* `ignoreTrailingSlash` {boolean} Whether a trailing slash on `path` is ignored
|
||||
when matching. Inherited from the mock pool's `ignoreTrailingSlash` option
|
||||
when not set.
|
||||
|
||||
#### Return: `MockInterceptor`
|
||||
|
||||
The reply behaviour of a matching request is defined through the returned
|
||||
`MockInterceptor`.
|
||||
|
||||
* `reply(statusCode[, data[, responseOptions]])` {Function} Defines the reply
|
||||
for a matching request.
|
||||
* `statusCode` {number} The status code of the mocked reply.
|
||||
* `data` {string|Buffer|Object|Function} The body of the mocked reply.
|
||||
Objects are serialized to JSON; strings and `Buffer`s are sent as-is. The
|
||||
function form has the signature
|
||||
`(opts: MockResponseCallbackOptions) => string | Buffer | Object` and is
|
||||
invoked with the incoming request to compute the reply body.
|
||||
* `responseOptions` {MockResponseOptions} Additional reply options.
|
||||
**Default:** `{}`.
|
||||
* Returns: {MockScope}
|
||||
* `reply(callback)` {Function} Defines the reply for a matching request,
|
||||
computing all reply options dynamically rather than just the body.
|
||||
* `callback` {Function} A `(opts: MockResponseCallbackOptions) =>
|
||||
{ statusCode, data, responseOptions }` function invoked with the incoming
|
||||
request.
|
||||
* Returns: {MockScope}
|
||||
* `replyWithError(error)` {Function} Defines an error for a matching request to
|
||||
throw.
|
||||
* `error` {Error} The error thrown when a request matches.
|
||||
* Returns: {MockScope}
|
||||
* `defaultReplyHeaders(headers)` {Function} Sets default headers included on
|
||||
every subsequent reply defined on this interceptor, in addition to any headers
|
||||
set on a specific reply.
|
||||
* `headers` {Object} A map of header name to value.
|
||||
* Returns: {MockInterceptor}
|
||||
* `defaultReplyTrailers(trailers)` {Function} Sets default trailers included on
|
||||
every subsequent reply defined on this interceptor, in addition to any
|
||||
trailers set on a specific reply.
|
||||
* `trailers` {Object} A map of trailer name to value.
|
||||
* Returns: {MockInterceptor}
|
||||
* `replyContentLength()` {Function} Sets an automatically calculated
|
||||
`content-length` header on every subsequent reply defined on this interceptor.
|
||||
* Returns: {MockInterceptor}
|
||||
|
||||
By default, `reply()` and `replyWithError()` define the behaviour for the first
|
||||
matching request only; subsequent requests are not affected unless `persist()`
|
||||
or `times()` is used on the returned [`MockScope`][].
|
||||
|
||||
#### Parameter: `MockResponseCallbackOptions`
|
||||
|
||||
The argument passed to the `reply()` data and options callbacks.
|
||||
|
||||
* `path` {string} The path of the intercepted request.
|
||||
* `method` {string} The method of the intercepted request.
|
||||
* `headers` {Object|Headers} The headers of the intercepted request.
|
||||
* `origin` {string} The origin of the intercepted request.
|
||||
* `body` {string|null} The body of the intercepted request.
|
||||
|
||||
#### Parameter: `MockResponseOptions`
|
||||
|
||||
* `headers` {Object} Headers to include on the mocked reply.
|
||||
* `trailers` {Object} Trailers to include on the mocked reply.
|
||||
|
||||
#### Return: `MockScope`
|
||||
|
||||
A `MockScope` is associated with a single [`MockInterceptor`][] and configures
|
||||
how many times the defined reply is used.
|
||||
|
||||
* `delay(waitInMs)` {Function} Delays the associated reply by a set amount of
|
||||
time.
|
||||
* `waitInMs` {number} The delay in milliseconds.
|
||||
* Returns: {MockScope}
|
||||
* `persist()` {Function} Makes the associated reply match indefinitely, so every
|
||||
matching request receives the defined response.
|
||||
* Returns: {MockScope}
|
||||
* `times(repeatTimes)` {Function} Makes the associated reply match a fixed number
|
||||
of times. This is overridden by `persist()`.
|
||||
* `repeatTimes` {number} The number of matching requests the reply applies to.
|
||||
* Returns: {MockScope}
|
||||
|
||||
The following examples show common interception patterns.
|
||||
|
||||
```mjs displayName="Basic mocked request"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
// MockPool
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo')
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
@@ -112,9 +212,7 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Mocked request using reply data callbacks
|
||||
|
||||
```js
|
||||
```mjs displayName="Reply with a data callback"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -124,100 +222,70 @@ const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/echo',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'undici',
|
||||
Host: 'example.com'
|
||||
}
|
||||
}).reply(200, ({ headers }) => ({ message: headers.get('message') }))
|
||||
|
||||
const { statusCode, body, headers } = await request('http://localhost:3000', {
|
||||
headers: {
|
||||
message: 'hello world!'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
console.log('headers', headers) // { 'content-type': 'application/json' }
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // { "message":"hello world!" }
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Mocked request using reply options callback
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/echo',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'User-Agent': 'undici',
|
||||
Host: 'example.com'
|
||||
}
|
||||
}).reply(({ headers }) => ({ statusCode: 200, data: { message: headers.get('message') }})))
|
||||
|
||||
const { statusCode, body, headers } = await request('http://localhost:3000', {
|
||||
headers: {
|
||||
message: 'hello world!'
|
||||
}
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
console.log('headers', headers) // { 'content-type': 'application/json' }
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // { "message":"hello world!" }
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic Mocked requests with multiple intercepts
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
}).reply(200, 'foo')
|
||||
}).reply(200, ({ headers }) => ({ message: headers.message }))
|
||||
|
||||
const { statusCode, body } = await request('http://localhost:3000/echo', {
|
||||
headers: { message: 'hello world!' }
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // {"message":"hello world!"}
|
||||
}
|
||||
```
|
||||
|
||||
```mjs displayName="Reply with an options callback"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/hello',
|
||||
method: 'GET',
|
||||
}).reply(200, 'hello')
|
||||
path: '/echo',
|
||||
method: 'GET'
|
||||
}).reply(({ headers }) => ({
|
||||
statusCode: 200,
|
||||
data: { message: headers.message }
|
||||
}))
|
||||
|
||||
const result1 = await request('http://localhost:3000/foo')
|
||||
const { statusCode, body } = await request('http://localhost:3000/echo', {
|
||||
headers: { message: 'hello world!' }
|
||||
})
|
||||
|
||||
console.log('response received', result1.statusCode) // response received 200
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of result1.body) {
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // {"message":"hello world!"}
|
||||
}
|
||||
```
|
||||
|
||||
```mjs displayName="Multiple intercepts"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo')
|
||||
mockPool.intercept({ path: '/hello', method: 'GET' }).reply(200, 'hello')
|
||||
|
||||
const fooResult = await request('http://localhost:3000/foo')
|
||||
for await (const data of fooResult.body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
|
||||
const result2 = await request('http://localhost:3000/hello')
|
||||
|
||||
console.log('response received', result2.statusCode) // response received 200
|
||||
|
||||
for await (const data of result2.body) {
|
||||
const helloResult = await request('http://localhost:3000/hello')
|
||||
for await (const data of helloResult.body) {
|
||||
console.log('data', data.toString('utf8')) // data hello
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Mocked request with query body, request headers and response headers and trailers
|
||||
|
||||
```js
|
||||
```mjs displayName="Match query, body, request headers; reply with headers and trailers"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -238,12 +306,9 @@ mockPool.intercept({
|
||||
trailers: { 'Content-MD5': 'test' }
|
||||
})
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
headers,
|
||||
trailers,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo?hello=there&see=ya', {
|
||||
const { statusCode, headers, trailers, body } = await request(
|
||||
'http://localhost:3000/foo?hello=there&see=ya',
|
||||
{
|
||||
method: 'POST',
|
||||
body: 'form1=data1&form2=data2',
|
||||
headers: {
|
||||
@@ -251,21 +316,20 @@ const {
|
||||
'User-Agent': 'undici',
|
||||
Host: 'example.com'
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
console.log('headers', headers) // { 'content-type': 'application/json' }
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // '{"foo":"bar"}'
|
||||
console.log('data', data.toString('utf8')) // {"foo":"bar"}
|
||||
}
|
||||
|
||||
console.log('trailers', trailers) // { 'content-md5': 'test' }
|
||||
```
|
||||
|
||||
#### Example - Mocked request using different matchers
|
||||
|
||||
```js
|
||||
```mjs displayName="Different matcher forms"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -279,14 +343,11 @@ mockPool.intercept({
|
||||
body: (value) => value === 'form=data',
|
||||
headers: {
|
||||
'User-Agent': 'undici',
|
||||
Host: /^example.com$/
|
||||
Host: /^example\.com$/
|
||||
}
|
||||
}).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo', {
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo', {
|
||||
method: 'GET',
|
||||
body: 'form=data',
|
||||
headers: {
|
||||
@@ -303,9 +364,7 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Mocked request with reply with a defined error
|
||||
|
||||
```js
|
||||
```mjs displayName="Reply with an error"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -319,18 +378,13 @@ mockPool.intercept({
|
||||
}).replyWithError(new Error('kaboom'))
|
||||
|
||||
try {
|
||||
await request('http://localhost:3000/foo', {
|
||||
method: 'GET'
|
||||
})
|
||||
await request('http://localhost:3000/foo', { method: 'GET' })
|
||||
} catch (error) {
|
||||
console.error(error) // TypeError: fetch failed
|
||||
console.error(error.cause) // Error: kaboom
|
||||
console.error(error.message) // kaboom
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Mocked request with defaultReplyHeaders
|
||||
|
||||
```js
|
||||
```mjs displayName="Default reply headers"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -338,10 +392,8 @@ setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
}).defaultReplyHeaders({ foo: 'bar' })
|
||||
mockPool.intercept({ path: '/foo', method: 'GET' })
|
||||
.defaultReplyHeaders({ foo: 'bar' })
|
||||
.reply(200, 'foo')
|
||||
|
||||
const { headers } = await request('http://localhost:3000/foo')
|
||||
@@ -349,9 +401,7 @@ const { headers } = await request('http://localhost:3000/foo')
|
||||
console.log('headers', headers) // headers { foo: 'bar' }
|
||||
```
|
||||
|
||||
#### Example - Mocked request with defaultReplyTrailers
|
||||
|
||||
```js
|
||||
```mjs displayName="Default reply trailers"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -359,10 +409,8 @@ setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
}).defaultReplyTrailers({ foo: 'bar' })
|
||||
mockPool.intercept({ path: '/foo', method: 'GET' })
|
||||
.defaultReplyTrailers({ foo: 'bar' })
|
||||
.reply(200, 'foo')
|
||||
|
||||
const { trailers } = await request('http://localhost:3000/foo')
|
||||
@@ -370,9 +418,7 @@ const { trailers } = await request('http://localhost:3000/foo')
|
||||
console.log('trailers', trailers) // trailers { foo: 'bar' }
|
||||
```
|
||||
|
||||
#### Example - Mocked request with automatic content-length calculation
|
||||
|
||||
```js
|
||||
```mjs displayName="Automatic content-length"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -380,19 +426,16 @@ setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
}).replyContentLength().reply(200, 'foo')
|
||||
mockPool.intercept({ path: '/foo', method: 'GET' })
|
||||
.replyContentLength()
|
||||
.reply(200, 'foo')
|
||||
|
||||
const { headers } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('headers', headers) // headers { 'content-length': '3' }
|
||||
```
|
||||
|
||||
#### Example - Mocked request with automatic content-length calculation on an object
|
||||
|
||||
```js
|
||||
```mjs displayName="Persisted reply"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -400,19 +443,13 @@ setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
}).replyContentLength().reply(200, { foo: 'bar' })
|
||||
mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo').persist()
|
||||
|
||||
const { headers } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('headers', headers) // headers { 'content-length': '13' }
|
||||
await request('http://localhost:3000/foo') // Matches and returns the mock
|
||||
await request('http://localhost:3000/foo') // Matches again, indefinitely
|
||||
```
|
||||
|
||||
#### Example - Mocked request with persist enabled
|
||||
|
||||
```js
|
||||
```mjs displayName="Reply a fixed number of times"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -420,57 +457,23 @@ setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
}).reply(200, 'foo').persist()
|
||||
mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo').times(2)
|
||||
|
||||
const result1 = await request('http://localhost:3000/foo')
|
||||
// Will match and return mocked data
|
||||
|
||||
const result2 = await request('http://localhost:3000/foo')
|
||||
// Will match and return mocked data
|
||||
|
||||
// Etc
|
||||
await request('http://localhost:3000/foo') // Matches and returns the mock
|
||||
await request('http://localhost:3000/foo') // Matches and returns the mock
|
||||
await request('http://localhost:3000/foo') // No match; a real request is attempted
|
||||
```
|
||||
|
||||
#### Example - Mocked request with times enabled
|
||||
|
||||
```js
|
||||
```mjs displayName="Match the path with a function"
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
import querystring from 'node:querystring'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
}).reply(200, 'foo').times(2)
|
||||
|
||||
const result1 = await request('http://localhost:3000/foo')
|
||||
// Will match and return mocked data
|
||||
|
||||
const result2 = await request('http://localhost:3000/foo')
|
||||
// Will match and return mocked data
|
||||
|
||||
const result3 = await request('http://localhost:3000/foo')
|
||||
// Will not match and make attempt a real request
|
||||
```
|
||||
|
||||
#### Example - Mocked request with path callback
|
||||
|
||||
```js
|
||||
import { MockAgent, setGlobalDispatcher, request } from 'undici'
|
||||
import querystring from 'querystring'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
setGlobalDispatcher(mockAgent)
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
const matchPath = requestPath => {
|
||||
const matchPath = (requestPath) => {
|
||||
const [pathname, search] = requestPath.split('?')
|
||||
const requestQuery = querystring.parse(search)
|
||||
|
||||
@@ -478,31 +481,26 @@ const matchPath = requestPath => {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!Object.keys(requestQuery).includes('foo') || requestQuery.foo !== 'bar') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
return requestQuery.foo === 'bar'
|
||||
}
|
||||
|
||||
mockPool.intercept({
|
||||
path: matchPath,
|
||||
method: 'GET'
|
||||
}).reply(200, 'foo')
|
||||
mockPool.intercept({ path: matchPath, method: 'GET' }).reply(200, 'foo')
|
||||
|
||||
const result = await request('http://localhost:3000/foo?foo=bar')
|
||||
// Will match and return mocked data
|
||||
await request('http://localhost:3000/foo?foo=bar') // Matches and returns the mock
|
||||
```
|
||||
|
||||
### `MockPool.close()`
|
||||
### `mockPool.close()`
|
||||
|
||||
Closes the mock pool and de-registers from associated MockAgent.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
Returns: `Promise<void>`
|
||||
* Returns: {Promise} Fulfills with `undefined` once the mock pool is closed.
|
||||
|
||||
#### Example - clean up after tests are complete
|
||||
Closes the mock pool, gracefully waiting for any enqueued requests to complete,
|
||||
and removes it from the associated [`MockAgent`][].
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
@@ -511,31 +509,45 @@ const mockPool = mockAgent.get('http://localhost:3000')
|
||||
await mockPool.close()
|
||||
```
|
||||
|
||||
### `MockPool.dispatch(options, handlers)`
|
||||
### `mockPool.dispatch(options, handlers)`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
### `MockPool.request(options[, callback])`
|
||||
* `options` {DispatchOptions} The request options.
|
||||
* `handlers` {DispatchHandler} The handlers invoked over the request lifecycle.
|
||||
* Returns: {boolean} `false` if the dispatcher is busy and the caller should
|
||||
wait before dispatching further requests, otherwise `true`.
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
Dispatches a request, matching it against the registered mocks. This override of
|
||||
[`dispatcher.dispatch(options, handlers)`][] is what drives the mocking
|
||||
behaviour for every higher-level method such as `request`.
|
||||
|
||||
#### Example - MockPool request
|
||||
### `mockPool.request(options[, callback])`
|
||||
|
||||
```js
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* `options` {DispatchOptions}
|
||||
* `callback` {Function} (optional) Invoked with the response when no `Promise`
|
||||
is requested.
|
||||
* Returns: {Promise} Fulfills with the mocked response when no `callback` is
|
||||
provided.
|
||||
|
||||
Performs a request and resolves it against the registered mocks. Inherited from
|
||||
[`Pool`][]; see [`dispatcher.request(options[, callback])`][] for the full
|
||||
parameter and return value documentation.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
mockPool.intercept({
|
||||
path: '/foo',
|
||||
method: 'GET',
|
||||
}).reply(200, 'foo')
|
||||
mockPool.intercept({ path: '/foo', method: 'GET' }).reply(200, 'foo')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await mockPool.request({
|
||||
const { statusCode, body } = await mockPool.request({
|
||||
origin: 'http://localhost:3000',
|
||||
path: '/foo',
|
||||
method: 'GET'
|
||||
@@ -548,8 +560,32 @@ for await (const data of body) {
|
||||
}
|
||||
```
|
||||
|
||||
### `MockPool.cleanMocks()`
|
||||
### `mockPool.cleanMocks()`
|
||||
|
||||
This method cleans up all the prepared mocks.
|
||||
<!-- YAML
|
||||
added: v7.11.0
|
||||
-->
|
||||
|
||||
Returns: `void`
|
||||
* Returns: {undefined}
|
||||
|
||||
Removes all registered interceptors from the mock pool. Pending mocks defined
|
||||
before this call no longer match incoming requests.
|
||||
|
||||
```mjs
|
||||
import { MockAgent } from 'undici'
|
||||
|
||||
const mockAgent = new MockAgent()
|
||||
const mockPool = mockAgent.get('http://localhost:3000')
|
||||
|
||||
mockPool.intercept({ path: '/foo' }).reply(200, 'foo')
|
||||
mockPool.cleanMocks()
|
||||
```
|
||||
|
||||
[`MockAgent`]: MockAgent.md#class-mockagent
|
||||
[`MockClient`]: MockClient.md#class-mockclient
|
||||
[`MockInterceptor`]: MockPool.md#return-mockinterceptor
|
||||
[`MockScope`]: MockPool.md#return-mockscope
|
||||
[`Pool`]: Pool.md#class-pool
|
||||
[`dispatcher.dispatch(options, handlers)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`mockAgent.get(origin)`]: MockAgent.md#mockagentgetorigin
|
||||
+234
-42
@@ -1,87 +1,279 @@
|
||||
# Class: Pool
|
||||
# Pool
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v1.0.0-->
|
||||
|
||||
A pool of [Client](/docs/docs/api/Client.md) instances connected to the same upstream target.
|
||||
<!--type=module-->
|
||||
|
||||
Requests are not guaranteed to be dispatched in order of invocation.
|
||||
<!-- source_link=lib/dispatcher/pool.js -->
|
||||
|
||||
## `new Pool(url[, options])`
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Arguments:
|
||||
A pool of [`Client`][] instances connected to the same upstream origin. A `Pool`
|
||||
spreads requests across several connections to the same host, which allows more
|
||||
than one request to be in flight at a time without enabling pipelining on a
|
||||
single connection.
|
||||
|
||||
* **url** `URL | string` - It should only include the **protocol, hostname, and port**.
|
||||
* **options** `PoolOptions` (optional)
|
||||
`Pool` extends [`Dispatcher`][]. Each [`Client`][] in the pool is created lazily
|
||||
through a configurable `factory` and shares the same options. Requests are not
|
||||
guaranteed to be dispatched in the order in which they are invoked.
|
||||
|
||||
### Parameter: `PoolOptions`
|
||||
```mjs
|
||||
import { Pool } from 'undici'
|
||||
|
||||
Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions)
|
||||
const pool = new Pool('http://localhost:3000', { connections: 10 })
|
||||
const { statusCode, body } = await pool.request({ path: '/', method: 'GET' })
|
||||
console.log(statusCode, await body.text())
|
||||
await pool.close()
|
||||
```
|
||||
|
||||
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
|
||||
* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `Pool` instance will create an unlimited amount of `Client` instances.
|
||||
* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `Pool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age.
|
||||
## Class: `Pool`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Reworked as part of the balanced pool implementation.
|
||||
-->
|
||||
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
### `new Pool(url[, options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `url` {URL|string} The upstream origin. It should only include the protocol,
|
||||
hostname, and port.
|
||||
* `options` {PoolOptions} (optional) Extends {ClientOptions}, so every
|
||||
[`Client`][] option is also accepted and forwarded to each pooled client.
|
||||
* `factory` {Function} Creates the [`Client`][] (or other [`Dispatcher`][])
|
||||
instances used by the pool.
|
||||
**Default:** `(origin, opts) => new Client(origin, opts)`.
|
||||
* `origin` {URL} The parsed upstream origin.
|
||||
* `opts` {Object} The resolved client options.
|
||||
* Returns: {Dispatcher}
|
||||
* `connections` {number|null} The maximum number of [`Client`][] instances the
|
||||
pool may create. When set to `null` the pool creates an unlimited number of
|
||||
clients. **Default:** `null`.
|
||||
* `clientTtl` {number|null} The amount of time, in milliseconds, before an idle
|
||||
[`Client`][] is removed from the pool and closed. When set to `null` clients
|
||||
are never removed based on age. **Default:** `null`.
|
||||
|
||||
Creates a new `Pool` against `url`. If `connections` is provided it must be a
|
||||
finite, non-negative number, otherwise an [`InvalidArgumentError`][] is thrown.
|
||||
The `factory` must be a function, and `connect` (inherited from {ClientOptions})
|
||||
must be a function or an object when provided.
|
||||
|
||||
The connection-related {ClientOptions} (`connect`, `connectTimeout`, `tls`,
|
||||
`maxCachedSessions`, `socketPath`, `autoSelectFamily`,
|
||||
`autoSelectFamilyAttemptTimeout`, `allowH2`, and `useH2c`) are used to build the
|
||||
connector shared by every pooled client.
|
||||
|
||||
> [!NOTE]
|
||||
> `Pool` inherits all [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions), including `allowH2` (default `true`) and `maxConcurrentStreams` (default `100`). With the unlimited default of `connections`, `Pool` will open a new `Client` — and therefore a new TCP/TLS socket — per concurrent dispatch, which defeats HTTP/2 multiplexing on a shared session. To benefit from h2 multiplexing on a single session, cap `connections` (e.g. `connections: 1`) so that concurrent requests share a session up to `maxConcurrentStreams`.
|
||||
> `Pool` inherits all {ClientOptions}, including `allowH2` and
|
||||
> `maxConcurrentStreams`. With the default unlimited `connections`, the pool
|
||||
> opens a new client - and therefore a new TCP/TLS socket - per concurrent
|
||||
> dispatch, which defeats HTTP/2 multiplexing over a shared session. To benefit
|
||||
> from h2 multiplexing on a single session, cap `connections` (for example
|
||||
> `connections: 1`) so that concurrent requests share a session up to
|
||||
> `maxConcurrentStreams`.
|
||||
|
||||
## Instance Properties
|
||||
```mjs
|
||||
import { Pool } from 'undici'
|
||||
|
||||
### `Pool.closed`
|
||||
const pool = new Pool('http://localhost:3000', {
|
||||
connections: 128,
|
||||
pipelining: 1
|
||||
})
|
||||
```
|
||||
|
||||
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
|
||||
### `pool.closed`
|
||||
|
||||
### `Pool.destroyed`
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
|
||||
* Type: {boolean}
|
||||
|
||||
### `Pool.stats`
|
||||
`true` after [`pool.close()`][] has been called.
|
||||
|
||||
Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool.
|
||||
### `pool.destroyed`
|
||||
|
||||
## Instance Methods
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
changes:
|
||||
- version: v2.0.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/384
|
||||
description: Reflects the state set by `pool.close()`.
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Reworked as part of the balanced pool implementation.
|
||||
-->
|
||||
|
||||
### `Pool.close([callback])`
|
||||
* Type: {boolean}
|
||||
|
||||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
|
||||
`true` after [`pool.destroy()`][] has been called, or after [`pool.close()`][]
|
||||
has been called and the pool shutdown has completed.
|
||||
|
||||
### `Pool.destroy([error, callback])`
|
||||
### `pool.stats`
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
### `Pool.connect(options[, callback])`
|
||||
* Type: {PoolStats}
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
|
||||
Aggregate statistics for the pool. See [`PoolStats`][].
|
||||
|
||||
### `Pool.dispatch(options, handler)`
|
||||
### `pool.close([callback])`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
### `Pool.pipeline(options, handler)`
|
||||
Implements [`dispatcher.close([callback])`][]. Gracefully closes the pool and
|
||||
all of its clients, waiting for in-flight requests to complete. Resolves once
|
||||
every pooled client has closed.
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
|
||||
### `pool.destroy([error[, callback]])`
|
||||
|
||||
### `Pool.request(options[, callback])`
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
Implements [`dispatcher.destroy([error[, callback]])`][]. Forcefully destroys
|
||||
the pool and all of its clients, aborting any in-flight requests with `error`.
|
||||
|
||||
### `Pool.stream(options, factory[, callback])`
|
||||
### `pool.connect(options[, callback])`
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
### `Pool.upgrade(options[, callback])`
|
||||
See [`dispatcher.connect(options[, callback])`][]. Performs an HTTP `CONNECT`
|
||||
request through one of the pooled clients.
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
||||
### `pool.dispatch(options, handler)`
|
||||
|
||||
## Instance Events
|
||||
<!-- YAML
|
||||
added: v1.3.0
|
||||
changes:
|
||||
- version: v4.10.4
|
||||
pr-url: https://github.com/nodejs/undici/pull/1114
|
||||
description: Reworked as part of the balanced pool implementation.
|
||||
-->
|
||||
|
||||
Implements [`dispatcher.dispatch(options, handler)`][]. Selects an available
|
||||
client - creating a new one through `factory` when none is free and the
|
||||
`connections` limit has not been reached - and dispatches the request to it.
|
||||
|
||||
### `pool.pipeline(options, handler)`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
See [`dispatcher.pipeline(options, handler)`][]. Sends a request and returns a
|
||||
{Duplex} stream that pipes the request body to the response body through
|
||||
`handler`.
|
||||
|
||||
### `pool.request(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
See [`dispatcher.request(options[, callback])`][]. Performs a request through one
|
||||
of the pooled clients.
|
||||
|
||||
```mjs
|
||||
import { Pool } from 'undici'
|
||||
|
||||
const pool = new Pool('http://localhost:3000', { connections: 10 })
|
||||
const { statusCode, headers, body } = await pool.request({
|
||||
path: '/',
|
||||
method: 'GET'
|
||||
})
|
||||
console.log(statusCode, headers)
|
||||
console.log(await body.text())
|
||||
await pool.close()
|
||||
```
|
||||
|
||||
### `pool.stream(options, factory[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
See [`dispatcher.stream(options, factory[, callback])`][]. Performs a request and
|
||||
writes the response body into the {Writable} stream returned by `factory`.
|
||||
|
||||
### `pool.upgrade(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
See [`dispatcher.upgrade(options[, callback])`][]. Upgrades a connection
|
||||
(for example to a WebSocket) through one of the pooled clients.
|
||||
|
||||
### Event: `'connect'`
|
||||
|
||||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `origin` {URL}
|
||||
* `targets` {Array} The pool followed by the client that connected,
|
||||
`[pool, ...targets]`.
|
||||
|
||||
Emitted by a pooled client when a socket has connected. See
|
||||
[`Dispatcher` Event: `'connect'`][]. When `clientTtl` is set, the connecting
|
||||
client's time-to-live timer starts on this event.
|
||||
|
||||
### Event: `'disconnect'`
|
||||
|
||||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `origin` {URL}
|
||||
* `targets` {Array} The pool followed by the client that disconnected,
|
||||
`[pool, ...targets]`.
|
||||
* `error` {Error}
|
||||
|
||||
Emitted by a pooled client when a socket has disconnected. See
|
||||
[`Dispatcher` Event: `'disconnect'`][].
|
||||
|
||||
### Event: `'drain'`
|
||||
|
||||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
|
||||
<!-- YAML
|
||||
added: v1.0.0
|
||||
-->
|
||||
|
||||
* `origin` {URL}
|
||||
* `targets` {Array} The pool followed by the client that drained,
|
||||
`[pool, ...targets]`.
|
||||
|
||||
Emitted when a pooled client is no longer busy and can accept more requests. See
|
||||
[`Dispatcher` Event: `'drain'`][].
|
||||
|
||||
[`Client`]: Client.md#class-client
|
||||
[`Dispatcher` Event: `'connect'`]: Dispatcher.md#event-connect
|
||||
[`Dispatcher` Event: `'disconnect'`]: Dispatcher.md#event-disconnect
|
||||
[`Dispatcher` Event: `'drain'`]: Dispatcher.md#event-drain
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`InvalidArgumentError`]: Errors.md#class-invalidargumenterror
|
||||
[`PoolStats`]: PoolStats.md
|
||||
[`dispatcher.close([callback])`]: Dispatcher.md#dispatcherclosecallback
|
||||
[`dispatcher.connect(options[, callback])`]: Dispatcher.md#dispatcherconnectoptions-callback
|
||||
[`dispatcher.destroy([error[, callback]])`]: Dispatcher.md#dispatcherdestroyerror-callback
|
||||
[`dispatcher.dispatch(options, handler)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`dispatcher.pipeline(options, handler)`]: Dispatcher.md#dispatcherpipelineoptions-handler
|
||||
[`dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`dispatcher.stream(options, factory[, callback])`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
|
||||
[`dispatcher.upgrade(options[, callback])`]: Dispatcher.md#dispatcherupgradeoptions-callback
|
||||
[`pool.close()`]: #poolclosecallback
|
||||
[`pool.destroy()`]: #pooldestroyerror-callback
|
||||
+119
-13
@@ -1,35 +1,141 @@
|
||||
# Class: PoolStats
|
||||
# PoolStats
|
||||
|
||||
Aggregate stats for a [Pool](/docs/docs/api/Pool.md) or [BalancedPool](/docs/docs/api/BalancedPool.md).
|
||||
<!--introduced_in=v4.15.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/util/stats.js -->
|
||||
|
||||
## `new PoolStats(pool)`
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Arguments:
|
||||
`PoolStats` exposes a read-only snapshot of the aggregate connection and request
|
||||
counters for a [`Pool`][] or [`BalancedPool`][]. An instance is created lazily
|
||||
each time the `pool.stats` getter is accessed, so the values reflect the state of
|
||||
the pool at the moment of access.
|
||||
|
||||
* **pool** `Pool` - Pool or BalancedPool from which to return stats.
|
||||
`PoolStats` is not constructed directly in normal use; obtain an instance from
|
||||
the `stats` property of a pool:
|
||||
|
||||
## Instance Properties
|
||||
```mjs
|
||||
import { Pool } from 'undici'
|
||||
|
||||
### `PoolStats.connected`
|
||||
const pool = new Pool('http://localhost:3000')
|
||||
const { stats } = pool
|
||||
|
||||
console.log(stats.connected, stats.running, stats.size)
|
||||
```
|
||||
|
||||
## Class: `PoolStats`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
Each property is a plain number computed from the pool's internal state when the
|
||||
instance is created. The instance is a snapshot; read `pool.stats` again to
|
||||
observe later changes.
|
||||
|
||||
### `new PoolStats(pool)`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
* `pool` {Pool|BalancedPool} The pool to read counters from.
|
||||
|
||||
Creates a new `PoolStats` instance from the current state of `pool`. Prefer the
|
||||
`pool.stats` getter, which returns a fresh `PoolStats` on each access.
|
||||
|
||||
### `poolStats.connected`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of open socket connections in this pool.
|
||||
|
||||
### `PoolStats.free`
|
||||
### `poolStats.free`
|
||||
|
||||
Number of open socket connections in this pool that do not have an active request.
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
### `PoolStats.pending`
|
||||
* Type: {number}
|
||||
|
||||
Number of open socket connections in this pool that do not have an active
|
||||
request.
|
||||
|
||||
### `poolStats.pending`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of pending requests across all clients in this pool.
|
||||
|
||||
### `PoolStats.queued`
|
||||
### `poolStats.queued`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of queued requests across all clients in this pool.
|
||||
|
||||
### `PoolStats.running`
|
||||
### `poolStats.running`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of currently active requests across all clients in this pool.
|
||||
|
||||
### `PoolStats.size`
|
||||
### `poolStats.size`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.15.0
|
||||
changes:
|
||||
- version: v7.9.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4157
|
||||
description: Pool and client stats are exposed through `Agent`.
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
Number of active, pending, or queued requests across all clients in this pool.
|
||||
|
||||
[`BalancedPool`]: BalancedPool.md#class-balancedpool
|
||||
[`Pool`]: Pool.md#class-pool
|
||||
+175
-156
@@ -1,229 +1,248 @@
|
||||
# Class: ProxyAgent
|
||||
# ProxyAgent
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v4.8.2-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/dispatcher/proxy-agent.js -->
|
||||
|
||||
A Proxy Agent class that implements the Agent API. It allows the connection through proxy in a simple way.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new ProxyAgent([options])`
|
||||
A `ProxyAgent` routes every request through an HTTP, HTTPS, or SOCKS5 proxy
|
||||
server. It implements the [`Agent`][] API, so it can be used wherever a
|
||||
dispatcher is accepted, for example with [`request`][] or [`fetch`][], or
|
||||
installed globally with [`setGlobalDispatcher`][].
|
||||
|
||||
Arguments:
|
||||
For secure (`https:`) endpoints the proxy connection is established through an
|
||||
HTTP `CONNECT` tunnel. SOCKS5 proxies (`socks5:` or `socks:` URIs) are handled
|
||||
by delegating to a [`Socks5ProxyAgent`][].
|
||||
|
||||
* **options** `ProxyAgentOptions` (required) - It extends the `Agent` options.
|
||||
```mjs
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
Returns: `ProxyAgent`
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000')
|
||||
setGlobalDispatcher(proxyAgent)
|
||||
```
|
||||
|
||||
### Parameter: `ProxyAgentOptions`
|
||||
## Class: `ProxyAgent`
|
||||
|
||||
Extends: [`AgentOptions`](/docs/docs/api/Agent.md#parameter-agentoptions)
|
||||
> It ommits `AgentOptions#connect`.
|
||||
<!-- YAML
|
||||
added: v4.8.2
|
||||
-->
|
||||
|
||||
> **Note:** When `AgentOptions#connections` is set, and different from `0`, the non-standard [`proxy-connection` header](https://udger.com/resources/http-request-headers-detail?header=Proxy-Connection) will be set to `keep-alive` in the request.
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
* **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string.
|
||||
If the `uri` is provided as a string or `uri` is an object with an `uri` property of type string, then it will be parsed into a `URL` object according to the [WHATWG URL Specification](https://url.spec.whatwg.org).
|
||||
For detailed information on the parsing process and potential validation errors, please refer to the ["Writing" section](https://url.spec.whatwg.org/#writing) of the WHATWG URL Specification.
|
||||
* **token** `string` (optional) - It can be passed by a string of token for authentication.
|
||||
* **auth** `string` (**deprecated**) - Use token.
|
||||
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
|
||||
* **requestTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the request. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
||||
* **proxyTls** `BuildOptions` (optional) - Options object passed when creating the underlying socket via the connector builder for the proxy server. It extends from [`Client#ConnectOptions`](/docs/docs/api/Client.md#parameter-connectoptions).
|
||||
* **proxyTunnel** `boolean` (optional) - For connections involving secure protocols, Undici will always establish a tunnel via the HTTP2 CONNECT extension. If proxyTunnel is set to true, this will occur for unsecured proxy/endpoint connections as well. Currently, there is no way to facilitate HTTP1 IP tunneling as described in https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request. If proxyTunnel is set to false (the default), ProxyAgent connections where both the Proxy and Endpoint are unsecured will issue all requests to the Proxy, and prefix the endpoint request path with the endpoint origin address.
|
||||
`ProxyAgent` keeps an internal [`Agent`][] that creates per-origin dispatchers
|
||||
on demand. The proxy connection is set up lazily when the first request to an
|
||||
origin is dispatched, then reused for subsequent requests to the same origin.
|
||||
|
||||
Examples:
|
||||
### `new ProxyAgent(options)`
|
||||
|
||||
```js
|
||||
<!-- YAML
|
||||
added: v4.8.2
|
||||
-->
|
||||
|
||||
* `options` {ProxyAgentOptions|string|URL} The proxy URI, or an options object.
|
||||
Required. When a `string` or {URL} is passed it is used as the proxy `uri`.
|
||||
The options object extends {AgentOptions} (omitting `connect`).
|
||||
* `uri` {string} The URI of the proxy server. Required when `options` is an
|
||||
object. Parsed into a {URL} according to the
|
||||
[WHATWG URL Specification][].
|
||||
* `token` {string} A pre-formatted `proxy-authorization` header value sent to
|
||||
the proxy on every request, for example `` `Bearer ${apiKey}` ``.
|
||||
* `auth` {string} A base64-encoded `username:password` credential sent as a
|
||||
`Basic` `proxy-authorization` header. Cannot be combined with `token`.
|
||||
* `headers` {Object} Additional headers sent to the proxy server on the
|
||||
`CONNECT` request (or on every request for an untunneled HTTP proxy).
|
||||
**Default:** `{}`.
|
||||
* `requestTls` {BuildOptions} TLS options for the connection to the request
|
||||
endpoint, passed to the connector builder.
|
||||
* `proxyTls` {BuildOptions} TLS options for the connection to the proxy
|
||||
server, passed to the connector builder.
|
||||
* `clientFactory` {Function} Builds the {Dispatcher} used to talk to the proxy
|
||||
server itself. **Default:** `(origin, opts) => new Pool(origin, opts)`.
|
||||
* `origin` {URL} The proxy origin.
|
||||
* `opts` {Object} The resolved options for the dispatcher.
|
||||
* Returns: {Dispatcher}
|
||||
* `proxyTunnel` {boolean} Forces tunneling through the proxy. By default,
|
||||
Undici detects tunneling based on the request protocol. If the target
|
||||
endpoint uses HTTPS, Undici establishes a `CONNECT` tunnel through the proxy
|
||||
(after the TLS handshake to the proxy itself when the proxy URL is HTTPS).
|
||||
If the target endpoint uses plain HTTP, Undici forwards the request to the
|
||||
proxy using an HTTP/1.1 absolute-form request target (over TLS when the
|
||||
proxy URL is HTTPS), as required by
|
||||
[RFC 9112 §3.2.2](https://www.rfc-editor.org/rfc/rfc9112.html#name-absolute-form).
|
||||
This non-tunneled forwarding path does not negotiate HTTP/2 with the proxy.
|
||||
Set `proxyTunnel` to `true` to force tunneling for plain HTTP requests as
|
||||
well. Currently, there is no way to facilitate HTTP/1.1 IP tunneling as
|
||||
described in
|
||||
[RFC 9484](https://www.rfc-editor.org/rfc/rfc9484.html#name-http-11-request).
|
||||
|
||||
Throws an {InvalidArgumentError} when no proxy URI is provided, when
|
||||
`clientFactory` is not a function, or when both `auth` and `token` are supplied.
|
||||
|
||||
The `proxy-authorization` header is derived, in order of precedence, from
|
||||
`token`, then `auth`, then the `username` and `password` embedded in the proxy
|
||||
`uri`. Credentials in the URI are URL-decoded before being base64-encoded.
|
||||
|
||||
> [!NOTE]
|
||||
> Unless {AgentOptions} `connections` is set to `0`, the non-standard
|
||||
> `proxy-connection: keep-alive` header is added to the tunnel `CONNECT`
|
||||
> request. This includes the default case where `connections` is unset.
|
||||
|
||||
The `auth` option is deprecated; use `token` instead.
|
||||
|
||||
```mjs
|
||||
import { ProxyAgent } from 'undici'
|
||||
|
||||
const proxyAgent = new ProxyAgent('my.proxy.server')
|
||||
// or
|
||||
const proxyAgent = new ProxyAgent(new URL('my.proxy.server'))
|
||||
// or
|
||||
const proxyAgent = new ProxyAgent({ uri: 'my.proxy.server' })
|
||||
// or
|
||||
const proxyAgent = new ProxyAgent({
|
||||
uri: new URL('my.proxy.server'),
|
||||
// A string or URL is treated as the proxy uri.
|
||||
const a = new ProxyAgent('http://localhost:8000')
|
||||
const b = new ProxyAgent(new URL('http://localhost:8000'))
|
||||
|
||||
// An options object requires a `uri`.
|
||||
const c = new ProxyAgent({ uri: 'http://localhost:8000' })
|
||||
|
||||
// With proxy TLS settings.
|
||||
const d = new ProxyAgent({
|
||||
uri: 'https://secure.proxy.server',
|
||||
proxyTls: {
|
||||
signal: AbortSignal.timeout(1000)
|
||||
}
|
||||
signal: AbortSignal.timeout(1000),
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
#### Example - Basic ProxyAgent instantiation
|
||||
#### Example - proxy request with the global dispatcher
|
||||
|
||||
This will instantiate the ProxyAgent. It will not do anything until registered as the agent to use with requests.
|
||||
|
||||
```js
|
||||
import { ProxyAgent } from 'undici'
|
||||
|
||||
const proxyAgent = new ProxyAgent('my.proxy.server')
|
||||
```
|
||||
|
||||
#### Example - Basic Proxy Request with global agent dispatcher
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { setGlobalDispatcher, request, ProxyAgent } from 'undici'
|
||||
|
||||
const proxyAgent = new ProxyAgent('my.proxy.server')
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000')
|
||||
setGlobalDispatcher(proxyAgent)
|
||||
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
console.log('response received', statusCode)
|
||||
console.log('data', await body.text())
|
||||
```
|
||||
|
||||
#### Example - Basic Proxy Request with local agent dispatcher
|
||||
#### Example - proxy request with a local dispatcher
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { ProxyAgent, request } from 'undici'
|
||||
|
||||
const proxyAgent = new ProxyAgent('my.proxy.server')
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo', { dispatcher: proxyAgent })
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo', {
|
||||
dispatcher: proxyAgent,
|
||||
})
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
console.log('response received', statusCode)
|
||||
console.log('data', await body.text())
|
||||
```
|
||||
|
||||
#### Example - Basic Proxy Request with authentication
|
||||
#### Example - proxy request with authentication
|
||||
|
||||
```js
|
||||
import { setGlobalDispatcher, request, ProxyAgent } from 'undici';
|
||||
```mjs
|
||||
import { setGlobalDispatcher, request, ProxyAgent } from 'undici'
|
||||
|
||||
const proxyAgent = new ProxyAgent({
|
||||
uri: 'my.proxy.server',
|
||||
// token: 'Bearer xxxx'
|
||||
token: `Basic ${Buffer.from('username:password').toString('base64')}`
|
||||
});
|
||||
setGlobalDispatcher(proxyAgent);
|
||||
uri: 'http://localhost:8000',
|
||||
token: `Basic ${Buffer.from('username:password').toString('base64')}`,
|
||||
})
|
||||
setGlobalDispatcher(proxyAgent)
|
||||
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo');
|
||||
const { statusCode } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode); // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')); // data foo
|
||||
}
|
||||
console.log('response received', statusCode)
|
||||
```
|
||||
|
||||
### `ProxyAgent.close()`
|
||||
### `proxyAgent.dispatch(options, handler)`
|
||||
|
||||
Closes the proxy agent and waits for registered pools and clients to also close before resolving.
|
||||
<!-- YAML
|
||||
added: v4.8.2
|
||||
-->
|
||||
|
||||
Returns: `Promise<void>`
|
||||
* `options` {AgentDispatchOptions} Extends {DispatchOptions}.
|
||||
* `origin` {string|URL} The origin to dispatch the request against. Required.
|
||||
* `handler` {DispatchHandler}
|
||||
* Returns: {boolean} `false` if the dispatcher is busy and the caller should
|
||||
wait for the [`'drain'`][] event before dispatching again.
|
||||
|
||||
#### Example - clean up after tests are complete
|
||||
Dispatches the request through the proxy. Throws an {InvalidArgumentError} if
|
||||
the request headers contain a `proxy-authorization` header, which must instead
|
||||
be supplied through the constructor's `token` or `auth` option. A `host` header
|
||||
is derived from `options.origin` when one is not already present. Implements
|
||||
[`Dispatcher.dispatch()`][].
|
||||
|
||||
```js
|
||||
### `proxyAgent.close()`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.8.2
|
||||
changes:
|
||||
- version: v7.10.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/4180
|
||||
description: Untunneled HTTP-to-HTTP proxy connections match `curl` behavior.
|
||||
-->
|
||||
|
||||
* Returns: {Promise<void>}
|
||||
|
||||
Closes the proxy agent and waits for its internal agent and proxy client (when
|
||||
present) to close before resolving. Implements [`Dispatcher.close()`][].
|
||||
|
||||
```mjs
|
||||
import { ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const proxyAgent = new ProxyAgent('my.proxy.server')
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000')
|
||||
setGlobalDispatcher(proxyAgent)
|
||||
|
||||
await proxyAgent.close()
|
||||
```
|
||||
|
||||
### `ProxyAgent.dispatch(options, handlers)`
|
||||
### `proxyAgent.request(options[, callback])`
|
||||
|
||||
Implements [`Agent.dispatch(options, handlers)`](/docs/docs/api/Agent.md#parameter-agentdispatchoptions).
|
||||
<!-- YAML
|
||||
added: v5.0.0
|
||||
-->
|
||||
|
||||
### `ProxyAgent.request(options[, callback])`
|
||||
See [`Dispatcher.request()`][]. The request is routed through the proxy.
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
#### Example - proxy request with `fetch`
|
||||
|
||||
```mjs
|
||||
import { ProxyAgent, fetch } from 'undici'
|
||||
|
||||
#### Example - ProxyAgent with Fetch
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000')
|
||||
|
||||
This example demonstrates how to use `fetch` with a proxy via `ProxyAgent`. It is particularly useful for scenarios requiring proxy tunneling.
|
||||
|
||||
```javascript
|
||||
import { ProxyAgent, fetch } from 'undici';
|
||||
|
||||
// Define the ProxyAgent
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000');
|
||||
|
||||
// Make a GET request through the proxy
|
||||
const response = await fetch('http://localhost:3000/foo', {
|
||||
dispatcher: proxyAgent,
|
||||
method: 'GET',
|
||||
});
|
||||
})
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response data:', await response.text());
|
||||
console.log('response status', response.status)
|
||||
console.log('response data', await response.text())
|
||||
```
|
||||
|
||||
---
|
||||
#### Example - HTTPS tunneling
|
||||
|
||||
#### Example - ProxyAgent with a Custom Proxy Server
|
||||
```mjs
|
||||
import { ProxyAgent, fetch } from 'undici'
|
||||
|
||||
This example shows how to create a custom proxy server and use it with `ProxyAgent`.
|
||||
const proxyAgent = new ProxyAgent('https://secure.proxy.server')
|
||||
|
||||
```javascript
|
||||
import * as http from 'node:http';
|
||||
import { createProxy } from 'proxy';
|
||||
import { ProxyAgent, fetch } from 'undici';
|
||||
|
||||
// Create a proxy server
|
||||
const proxyServer = createProxy(http.createServer());
|
||||
proxyServer.listen(8000, () => {
|
||||
console.log('Proxy server running on port 8000');
|
||||
});
|
||||
|
||||
// Define and use the ProxyAgent
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000');
|
||||
|
||||
const response = await fetch('http://example.com', {
|
||||
dispatcher: proxyAgent,
|
||||
method: 'GET',
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response data:', await response.text());
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Example - ProxyAgent with HTTPS Tunneling
|
||||
|
||||
This example demonstrates how to perform HTTPS tunneling using a proxy.
|
||||
|
||||
```javascript
|
||||
import { ProxyAgent, fetch } from 'undici';
|
||||
|
||||
// Define a ProxyAgent for HTTPS proxy
|
||||
const proxyAgent = new ProxyAgent('https://secure.proxy.server');
|
||||
|
||||
// Make a request to an HTTPS endpoint via the proxy
|
||||
const response = await fetch('https://secure.endpoint.com/api/data', {
|
||||
dispatcher: proxyAgent,
|
||||
method: 'GET',
|
||||
});
|
||||
})
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response data:', await response.json());
|
||||
console.log('response status', response.status)
|
||||
console.log('response data', await response.json())
|
||||
```
|
||||
|
||||
#### Example - ProxyAgent as a Global Dispatcher
|
||||
|
||||
`ProxyAgent` can be configured as a global dispatcher, making it available for all requests without explicitly passing it. This simplifies code and is useful when a single proxy configuration applies to all requests.
|
||||
|
||||
```javascript
|
||||
import { ProxyAgent, setGlobalDispatcher, fetch } from 'undici';
|
||||
|
||||
// Define and configure the ProxyAgent
|
||||
const proxyAgent = new ProxyAgent('http://localhost:8000');
|
||||
setGlobalDispatcher(proxyAgent);
|
||||
|
||||
// Make requests without specifying the dispatcher
|
||||
const response = await fetch('http://example.com');
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response data:', await response.text());
|
||||
[WHATWG URL Specification]: https://url.spec.whatwg.org
|
||||
[`'drain'`]: Dispatcher.md#event-drain
|
||||
[`Agent`]: Agent.md#class-agent
|
||||
[`Dispatcher.close()`]: Dispatcher.md#dispatcherclosecallback-promise
|
||||
[`Dispatcher.dispatch()`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`Dispatcher.request()`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`Socks5ProxyAgent`]: Socks5ProxyAgent.md#class-socks5proxyagent
|
||||
[`fetch`]: Fetch.md
|
||||
[`request`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`setGlobalDispatcher`]: Dispatcher.md#setglobaldispatcherdispatcher
|
||||
+238
-63
@@ -1,101 +1,276 @@
|
||||
# Class: RedirectHandler
|
||||
# RedirectHandler
|
||||
|
||||
A class that handles redirection logic for HTTP requests.
|
||||
<!--introduced_in=v4.0.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/handler/redirect-handler.js -->
|
||||
|
||||
## `new RedirectHandler(dispatch, maxRedirections, opts, handler)`
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Arguments:
|
||||
A dispatch handler that follows HTTP redirects on behalf of another handler. It
|
||||
wraps a downstream `handler` and a `dispatch` function: when a redirectable
|
||||
response is received it transparently re-dispatches the request to the new
|
||||
location, forwarding the lifecycle callbacks to the wrapped `handler` only once a
|
||||
final (non-redirect) response is reached.
|
||||
|
||||
- **dispatch** `function` - The dispatch function to be called after every retry.
|
||||
- **maxRedirections** `number` - Maximum number of redirections allowed.
|
||||
- **opts** `object` - Options for handling redirection. Supports `throwOnMaxRedirect`, `stripHeadersOnRedirect`, and `stripHeadersOnCrossOriginRedirect`.
|
||||
- **handler** `object` - An object containing handlers for different stages of the request lifecycle.
|
||||
Redirects are followed for the `300`, `301`, `302`, `303`, `307`, and `308`
|
||||
status codes, up to `maxRedirections` hops. Following the relevant HTTP
|
||||
specifications, the method is downgraded to `GET` for `301`/`302` `POST`
|
||||
responses and for `303` responses with any method other than `HEAD`, request
|
||||
bodies that have already been consumed are not replayed, and headers that refer
|
||||
to the original URL (such as `host`) are stripped on each hop, with additional
|
||||
credential headers removed on cross-origin redirects.
|
||||
|
||||
Returns: `RedirectHandler`
|
||||
```mjs
|
||||
import { RedirectHandler } from 'undici'
|
||||
```
|
||||
|
||||
### Parameters
|
||||
```cjs
|
||||
const { RedirectHandler } = require('undici')
|
||||
```
|
||||
|
||||
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every redirection.
|
||||
- **maxRedirections** `number` (required) - Maximum number of redirections allowed.
|
||||
- **opts** `object` (required) - Options for handling redirection.
|
||||
- **throwOnMaxRedirect** `boolean` - Throw when the maximum number of redirections is reached.
|
||||
- **stripHeadersOnRedirect** `string[]` - Header names to remove from all redirected requests.
|
||||
- **stripHeadersOnCrossOriginRedirect** `string[]` - Header names to remove from cross-origin redirected requests.
|
||||
- **handler** `object` (required) - Handlers for different stages of the request lifecycle.
|
||||
Most users do not construct `RedirectHandler` directly. The `maxRedirections`
|
||||
option on [`Dispatcher`][] methods and the higher-level clients use it
|
||||
internally.
|
||||
|
||||
### Properties
|
||||
## Class: `RedirectHandler`
|
||||
|
||||
- **location** `string` - The current redirection location.
|
||||
- **abort** `function` - The abort function.
|
||||
- **opts** `object` - The options for handling redirection.
|
||||
- **maxRedirections** `number` - Maximum number of redirections allowed.
|
||||
- **handler** `object` - Handlers for different stages of the request lifecycle.
|
||||
- **history** `Array` - An array representing the history of URLs during redirection.
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
### Methods
|
||||
* Extends: {DispatchHandler}
|
||||
|
||||
#### `onRequestStart(controller, context)`
|
||||
Implements the [`DispatchHandler`][] interface, delegating every callback to the
|
||||
wrapped `handler` while intercepting redirect responses.
|
||||
|
||||
Called when the request starts.
|
||||
### `new RedirectHandler(dispatch, maxRedirections, opts, handler)`
|
||||
|
||||
Parameters:
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
- **controller** `DispatchController` - The request controller.
|
||||
- **context** `object` - The dispatch context.
|
||||
* `dispatch` {Function} The dispatch function used to issue the original request
|
||||
and every subsequent redirect. Called as `dispatch(options, handler)`.
|
||||
* `maxRedirections` {number|null} The maximum number of redirects to follow. Must
|
||||
be a non-negative integer or `null`. When `null` or `0`, no redirects are
|
||||
followed and redirect responses are passed through to the wrapped `handler`.
|
||||
* `opts` {DispatchOptions} The dispatch options for the request. In addition to
|
||||
the standard dispatch options, the following redirect-specific fields are
|
||||
recognized:
|
||||
* `throwOnMaxRedirect` {boolean} When `true`, an error is thrown once
|
||||
`maxRedirections` is reached instead of returning the last redirect response.
|
||||
**Default:** `false`.
|
||||
* `stripHeadersOnRedirect` {string[]} Header names to remove from the request on
|
||||
every redirect hop. **Default:** `null`.
|
||||
* `stripHeadersOnCrossOriginRedirect` {string[]} Additional header names to
|
||||
remove from the request when a redirect points to a different origin.
|
||||
**Default:** `null`.
|
||||
* `handler` {DispatchHandler} The downstream handler that receives the lifecycle
|
||||
callbacks for the final response.
|
||||
|
||||
#### `onRequestUpgrade(controller, statusCode, headers, socket)`
|
||||
Throws an [`InvalidArgumentError`][] when `maxRedirections` is not a non-negative
|
||||
integer, when `throwOnMaxRedirect` is not a boolean, or when either
|
||||
`stripHeadersOnRedirect` or `stripHeadersOnCrossOriginRedirect` is not an array of
|
||||
header-name strings.
|
||||
|
||||
Called when an upgrade is requested.
|
||||
```mjs
|
||||
import { Client, RedirectHandler } from 'undici'
|
||||
|
||||
Parameters:
|
||||
const client = new Client('http://example.com')
|
||||
const dispatch = client.dispatch.bind(client)
|
||||
|
||||
- **controller** `DispatchController` - The request controller.
|
||||
- **statusCode** `number` - The HTTP status code.
|
||||
- **headers** `object` - The headers received in the response.
|
||||
- **socket** `object` - The socket object.
|
||||
const handler = new RedirectHandler(dispatch, 5, {
|
||||
method: 'GET',
|
||||
path: '/',
|
||||
origin: 'http://example.com',
|
||||
}, {
|
||||
onRequestStart () {},
|
||||
onResponseStart (controller, statusCode, headers) {
|
||||
console.log(statusCode)
|
||||
},
|
||||
onResponseData (controller, chunk) {},
|
||||
onResponseEnd () {},
|
||||
})
|
||||
|
||||
#### `onResponseError(controller, error)`
|
||||
dispatch({ method: 'GET', path: '/', origin: 'http://example.com' }, handler)
|
||||
```
|
||||
|
||||
Called when an error occurs.
|
||||
### Static method: `RedirectHandler.buildDispatch(dispatcher, maxRedirections)`
|
||||
|
||||
Parameters:
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
- **controller** `DispatchController` - The request controller.
|
||||
- **error** `Error` - The error that occurred.
|
||||
* `dispatcher` {Dispatcher} The dispatcher whose `dispatch` method is wrapped.
|
||||
* `maxRedirections` {number|null} The maximum number of redirects to follow. Must
|
||||
be a non-negative integer or `null`.
|
||||
* Returns: {Function} A `dispatch(options, handler)` function that wraps each call
|
||||
in a `RedirectHandler` before delegating to `dispatcher`.
|
||||
|
||||
#### `onResponseStart(controller, statusCode, headers, statusText)`
|
||||
Builds a redirect-aware dispatch function from an existing dispatcher. The
|
||||
returned function has the same shape as `dispatcher.dispatch` but follows
|
||||
redirects automatically. Throws an [`InvalidArgumentError`][] when
|
||||
`maxRedirections` is not a non-negative integer.
|
||||
|
||||
Called when headers are received.
|
||||
```mjs
|
||||
import { Client, RedirectHandler } from 'undici'
|
||||
|
||||
Parameters:
|
||||
const client = new Client('http://example.com')
|
||||
const dispatch = RedirectHandler.buildDispatch(client, 5)
|
||||
|
||||
- **controller** `DispatchController` - The request controller.
|
||||
- **statusCode** `number` - The HTTP status code.
|
||||
- **headers** `object` - The headers received in the response.
|
||||
- **statusText** `string` - The status text.
|
||||
dispatch({ method: 'GET', path: '/', origin: 'http://example.com' }, {
|
||||
onRequestStart () {},
|
||||
onResponseStart (controller, statusCode) {
|
||||
console.log(statusCode)
|
||||
},
|
||||
onResponseData () {},
|
||||
onResponseEnd () {},
|
||||
})
|
||||
```
|
||||
|
||||
#### `onResponseData(controller, chunk)`
|
||||
### `redirectHandler.onRequestStart(controller, context)`
|
||||
|
||||
Called when data is received.
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
Parameters:
|
||||
* `controller` {DispatchController} The controller for the in-flight request.
|
||||
* `context` {Object} The dispatch context.
|
||||
|
||||
- **controller** `DispatchController` - The request controller.
|
||||
- **chunk** `Buffer` - The data chunk received.
|
||||
Forwards the request-start event to the wrapped `handler`, augmenting `context`
|
||||
with the current redirect `history`.
|
||||
|
||||
#### `onResponseEnd(controller, trailers)`
|
||||
### `redirectHandler.onRequestUpgrade(controller, statusCode, headers, socket)`
|
||||
|
||||
Called when the request is complete.
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
Parameters:
|
||||
* `controller` {DispatchController} The controller for the in-flight request.
|
||||
* `statusCode` {number} The HTTP status code of the upgrade response.
|
||||
* `headers` {Object} The response headers.
|
||||
* `socket` {Duplex} The upgraded socket.
|
||||
|
||||
- **controller** `DispatchController` - The request controller.
|
||||
- **trailers** `object` - The trailers received.
|
||||
Forwards a connection upgrade to the wrapped `handler`.
|
||||
|
||||
#### `onBodySent(chunk)`
|
||||
### `redirectHandler.onResponseStart(controller, statusCode, headers, statusMessage)`
|
||||
|
||||
Called when the request body is sent.
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
Parameters:
|
||||
* `controller` {DispatchController} The controller for the in-flight request.
|
||||
* `statusCode` {number} The HTTP status code of the response.
|
||||
* `headers` {Object} The response headers.
|
||||
* `statusMessage` {string} The HTTP status message.
|
||||
|
||||
- **chunk** `Buffer` - The chunk of the request body sent.
|
||||
Inspects the response headers to decide whether to follow a redirect. When the
|
||||
status code is redirectable, the `maxRedirections` limit has not been reached, and
|
||||
the request body has not been consumed, the `location` property is set, the method
|
||||
and headers are adjusted for the next hop, and the response is not forwarded.
|
||||
Otherwise the event is forwarded to the wrapped `handler`.
|
||||
|
||||
Throws when `throwOnMaxRedirect` is `true` and the number of recorded redirects
|
||||
has reached `maxRedirections`, and throws an [`InvalidArgumentError`][] if a
|
||||
redirect loop is detected (for example when a [`Client`][] or [`Pool`][] is used
|
||||
for a cross-origin redirect; use an [`Agent`][] instead).
|
||||
|
||||
### `redirectHandler.onResponseData(controller, chunk)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `controller` {DispatchController} The controller for the in-flight request.
|
||||
* `chunk` {Buffer} A chunk of the response body.
|
||||
|
||||
Forwards body data to the wrapped `handler`. While a redirect is pending
|
||||
(`location` is set), the `3xx` response body is ignored.
|
||||
|
||||
### `redirectHandler.onResponseEnd(controller, trailers)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `controller` {DispatchController} The controller for the in-flight request.
|
||||
* `trailers` {Object} The response trailers.
|
||||
|
||||
Completes the response. When a redirect is pending, the request is re-dispatched
|
||||
to the new `location`; otherwise the end event is forwarded to the wrapped
|
||||
`handler`.
|
||||
|
||||
### `redirectHandler.onResponseError(controller, error)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
* `controller` {DispatchController} The controller for the in-flight request.
|
||||
* `error` {Error} The error that occurred.
|
||||
|
||||
Forwards the error to the wrapped `handler`.
|
||||
|
||||
### `redirectHandler.location`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Type: {string|null}
|
||||
|
||||
The `Location` header value of the redirect currently being followed, or `null`
|
||||
when no redirect is pending and the response is being passed through to the
|
||||
wrapped `handler`.
|
||||
|
||||
### `redirectHandler.opts`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.12.0
|
||||
-->
|
||||
|
||||
* Type: {DispatchOptions}
|
||||
|
||||
A copy of the dispatch options for the next request. It excludes the redirect
|
||||
control fields (`maxRedirections`, `stripHeadersOnRedirect`, and
|
||||
`stripHeadersOnCrossOriginRedirect`) and is mutated on each hop to reflect the
|
||||
updated method, path, origin, and headers.
|
||||
|
||||
### `redirectHandler.maxRedirections`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Type: {number|null}
|
||||
|
||||
The maximum number of redirects this handler will follow.
|
||||
|
||||
### `redirectHandler.handler`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Type: {DispatchHandler}
|
||||
|
||||
The wrapped downstream handler that receives the lifecycle callbacks for the final
|
||||
response.
|
||||
|
||||
### `redirectHandler.history`
|
||||
|
||||
<!-- YAML
|
||||
added: v4.0.0
|
||||
-->
|
||||
|
||||
* Type: {URL[]}
|
||||
|
||||
The ordered list of URLs visited while following redirects. It is used to enforce
|
||||
`maxRedirections` and to detect redirect loops.
|
||||
|
||||
[`Agent`]: Agent.md#class-agent
|
||||
[`Client`]: Client.md#class-client
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`DispatchHandler`]: Dispatcher.md#parameter-dispatchhandler
|
||||
[`InvalidArgumentError`]: Errors.md#class-invalidargumenterror
|
||||
[`Pool`]: Pool.md#class-pool
|
||||
+113
-26
@@ -1,49 +1,136 @@
|
||||
# Class: RetryAgent
|
||||
# RetryAgent
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v6.7.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/dispatcher/retry-agent.js -->
|
||||
|
||||
A `undici.Dispatcher` that allows to automatically retry a request.
|
||||
Wraps a `undici.RetryHandler`.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new RetryAgent(dispatcher, [options])`
|
||||
A [`Dispatcher`][] that automatically retries failed requests by wrapping
|
||||
another dispatcher with the retry logic of [`RetryHandler`][]. Requests that fail
|
||||
with a retryable status code or transport error are re-dispatched according to
|
||||
the configured retry policy.
|
||||
|
||||
Arguments:
|
||||
```mjs
|
||||
import { RetryAgent } from 'undici'
|
||||
```
|
||||
|
||||
* **dispatcher** `undici.Dispatcher` (required) - the dispatcher to wrap
|
||||
* **options** `RetryHandlerOptions` (optional) - the options
|
||||
## Class: `RetryAgent`
|
||||
|
||||
Returns: `RetryAgent`
|
||||
<!-- YAML
|
||||
added: v6.7.0
|
||||
-->
|
||||
|
||||
### Parameter: `RetryHandlerOptions`
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
- **throwOnError** `boolean` (optional) - Disable to prevent throwing error on last retry attept, useful if you need the body on errors from server or if you have custom error handler. Default: `true`
|
||||
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
|
||||
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
|
||||
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
|
||||
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
|
||||
- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
|
||||
- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
|
||||
- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']`
|
||||
- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
|
||||
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'UND_ERR_SOCKET']`
|
||||
Wraps an existing dispatcher and applies retry behaviour to every request it
|
||||
dispatches. Lifecycle methods such as [`retryAgent.close()`][] and
|
||||
[`retryAgent.destroy()`][] are forwarded to the wrapped dispatcher.
|
||||
|
||||
**`RetryContext`**
|
||||
### `new RetryAgent(dispatcher[, options])`
|
||||
|
||||
- `state`: `RetryState` - Current retry state. It can be mutated.
|
||||
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
|
||||
<!-- YAML
|
||||
added: v6.7.0
|
||||
-->
|
||||
|
||||
Example:
|
||||
* `dispatcher` {Dispatcher} The dispatcher to wrap and add retry behaviour to.
|
||||
* `options` {RetryOptions} (optional)
|
||||
* `throwOnError` {boolean} When `true`, an error is thrown on the final retry
|
||||
attempt, preventing the following handlers from being called and destroying
|
||||
the socket. Disable it when you need the response body for error responses
|
||||
or when you provide a custom error handler. **Default:** `true`.
|
||||
* `retry` {Function} Callback invoked on every retry iteration. It receives the
|
||||
error, the current retry state, and a callback. Pass an error to the callback
|
||||
to stop retrying, or call it with no argument to perform the retry.
|
||||
* `err` {Error} The error that caused the retry attempt.
|
||||
* `context` {RetryContext} The current retry context.
|
||||
* `state` {RetryState} The mutable retry state.
|
||||
* `counter` {number} The number of retries performed so far.
|
||||
* `opts` {Object} The options passed to the retry handler, combining the
|
||||
dispatch options with `retryOptions`.
|
||||
* `callback` {Function} Called to continue. Pass an `Error` to stop retrying,
|
||||
or `null`/`undefined` to perform the next retry.
|
||||
* `maxRetries` {number} Maximum number of retries to allow. **Default:** `5`.
|
||||
* `maxTimeout` {number} Maximum number of milliseconds to wait between retries.
|
||||
**Default:** `30000` (30 seconds).
|
||||
* `minTimeout` {number} Initial number of milliseconds to wait before the first
|
||||
retry. **Default:** `500` (half a second).
|
||||
* `timeoutFactor` {number} Factor by which the timeout is multiplied between
|
||||
retries. **Default:** `2`.
|
||||
* `retryAfter` {boolean} When `true`, the delay between retries is inferred from
|
||||
the `Retry-After` response header when present. **Default:** `true`.
|
||||
* `methods` {string[]} HTTP methods that are eligible for retry.
|
||||
**Default:** `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']`.
|
||||
* `statusCodes` {number[]} HTTP status codes that trigger a retry.
|
||||
**Default:** `[500, 502, 503, 504, 429]`.
|
||||
* `errorCodes` {string[]} Transport error codes that trigger a retry.
|
||||
**Default:** `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'UND_ERR_SOCKET']`.
|
||||
|
||||
```js
|
||||
Creates a new `RetryAgent` that wraps `dispatcher`. The `options` are stored and
|
||||
used to construct a [`RetryHandler`][] for each dispatched request.
|
||||
|
||||
```mjs
|
||||
import { Agent, RetryAgent } from 'undici'
|
||||
|
||||
const agent = new RetryAgent(new Agent())
|
||||
const agent = new RetryAgent(new Agent(), {
|
||||
maxRetries: 3,
|
||||
minTimeout: 1000,
|
||||
timeoutFactor: 2,
|
||||
})
|
||||
|
||||
const res = await agent.request({
|
||||
method: 'GET',
|
||||
origin: 'http://example.com',
|
||||
path: '/',
|
||||
})
|
||||
|
||||
console.log(res.statusCode)
|
||||
console.log(await res.body.text())
|
||||
```
|
||||
|
||||
### `retryAgent.dispatch(options, handler)`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.7.0
|
||||
-->
|
||||
|
||||
* `options` {DispatchOptions} The dispatch options, identical to
|
||||
[`dispatcher.dispatch()`][].
|
||||
* `handler` {DispatchHandler} The dispatch handler.
|
||||
* Returns: {boolean}
|
||||
|
||||
Dispatches `options` through the wrapped dispatcher, installing a
|
||||
[`RetryHandler`][] that applies the configured retry policy before delegating to
|
||||
`handler`. Returns `false` if the dispatcher is busy and the caller should wait
|
||||
before dispatching again, otherwise `true`.
|
||||
|
||||
### `retryAgent.close()`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.7.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Resolves when the wrapped dispatcher has closed.
|
||||
|
||||
Gracefully closes the wrapped dispatcher, waiting for in-flight requests to
|
||||
complete. Equivalent to calling [`dispatcher.close()`][] on the wrapped
|
||||
dispatcher.
|
||||
|
||||
### `retryAgent.destroy()`
|
||||
|
||||
<!-- YAML
|
||||
added: v6.7.0
|
||||
-->
|
||||
|
||||
* Returns: {Promise} Resolves when the wrapped dispatcher has been destroyed.
|
||||
|
||||
Forcefully destroys the wrapped dispatcher, aborting all pending requests.
|
||||
Equivalent to calling [`dispatcher.destroy()`][] on the wrapped dispatcher.
|
||||
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`RetryHandler`]: RetryHandler.md#class-retryhandler
|
||||
[`dispatcher.close()`]: Dispatcher.md#dispatcherclosecallback
|
||||
[`dispatcher.destroy()`]: Dispatcher.md#dispatcherdestroyerror-callback
|
||||
[`dispatcher.dispatch()`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`retryAgent.close()`]: #retryagentclose
|
||||
[`retryAgent.destroy()`]: #retryagentdestroy
|
||||
+156
-72
@@ -1,116 +1,200 @@
|
||||
# Class: RetryHandler
|
||||
# RetryHandler
|
||||
|
||||
Extends: `undici.DispatcherHandlers`
|
||||
<!--introduced_in=v5.28.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/handler/retry-handler.js -->
|
||||
|
||||
A handler class that implements the retry logic for a request.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new RetryHandler(opts, { dispatch, handler })`
|
||||
A [`Dispatcher`][] handler that automatically retries a request when it fails
|
||||
with a recoverable network error or an eligible HTTP status code. It wraps an
|
||||
inner handler and re-dispatches the request, applying an exponential backoff and
|
||||
honouring the `Retry-After` response header. When a response is partially
|
||||
consumed before the failure, the handler resumes the download with a `Range`
|
||||
request guarded by the original `ETag`.
|
||||
|
||||
Arguments:
|
||||
The handler is most often used indirectly through [`RetryAgent`][], but it can
|
||||
also be supplied directly to [`dispatcher.dispatch()`][] for fine-grained
|
||||
control over the retry behaviour.
|
||||
|
||||
- **opts** `Dispatch.DispatchOptions & { retryOptions?: RetryOptions }` (required) - An intersection of `Dispatcher.DispatchOptions` and an optional `RetryOptions` object.
|
||||
- **{ dispatch, handler }** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle.
|
||||
```mjs
|
||||
import { RetryHandler } from 'undici'
|
||||
```
|
||||
|
||||
Returns: `retryHandler`
|
||||
## Class: `RetryHandler`
|
||||
|
||||
### Parameter: `Dispatch.DispatchOptions & RetryOptions`
|
||||
<!-- YAML
|
||||
added: v5.28.0
|
||||
-->
|
||||
|
||||
Extends: [`Dispatch.DispatchOptions`](/docs/docs/api/Dispatcher.md#parameter-dispatchoptions).
|
||||
* Extends: {DispatchHandler}
|
||||
|
||||
Implements the [`DispatchHandler`][] interface. An instance is consumed by a
|
||||
single dispatch call and forwards the dispatch lifecycle to the inner `handler`,
|
||||
re-issuing the request through the supplied `dispatch` function whenever a retry
|
||||
is warranted.
|
||||
|
||||
> **Note:** The `RetryHandler` does not retry over stateful bodies (for example
|
||||
> streams or `AsyncIterable`), because once consumed they cannot be replayed. In
|
||||
> these situations the body is identified as stateful and the request is rejected
|
||||
> with the `UND_ERR_REQ_RETRY` error instead of being retried.
|
||||
|
||||
### `new RetryHandler(options, retryHandlers)`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.28.0
|
||||
changes:
|
||||
- version: v7.0.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/3883
|
||||
description: Reimplemented on top of the new dispatch lifecycle hooks.
|
||||
-->
|
||||
|
||||
* `options` {DispatchOptions} The dispatch options for the request, extended with
|
||||
an optional `retryOptions` field. Type is
|
||||
`DispatchOptions & { retryOptions?: RetryOptions }`.
|
||||
* `retryOptions` {RetryOptions} (optional) Configuration controlling when and
|
||||
how the request is retried. See [`RetryOptions`](#retryoptions).
|
||||
* `retryHandlers` {RetryHandlers} The handlers used to drive the retry loop. See
|
||||
[`RetryHandlers`](#parameter-retryhandlers).
|
||||
* Returns: {RetryHandler}
|
||||
|
||||
#### `RetryOptions`
|
||||
|
||||
- **throwOnError** `boolean` (optional) - Disable to prevent throwing error on last retry attept, useful if you need the body on errors from server or if you have custom error handler.
|
||||
- **retry** `(err: Error, context: RetryContext, callback: (err?: Error | null) => void) => void` (optional) - Function to be called after every retry. It should pass error if no more retries should be performed.
|
||||
- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5`
|
||||
- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds)
|
||||
- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second)
|
||||
- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2`
|
||||
- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true`
|
||||
- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']`
|
||||
- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]`
|
||||
- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'UND_ERR_SOCKET']`
|
||||
* `throwOnError` {boolean} When `true`, an error is thrown on the last retry
|
||||
attempt and propagated to the inner handler; when `false`, the failing response
|
||||
is passed through instead, which is useful when the error body is needed or a
|
||||
custom error handler is in place. **Default:** `true`.
|
||||
* `retry` {Function} Callback invoked on every retry iteration to decide whether
|
||||
another attempt should be made. It receives the error, the retry context, and a
|
||||
callback. Call the callback with an `Error` to stop retrying, or with `null` to
|
||||
schedule another attempt. **Default:** the built-in retry strategy described
|
||||
below.
|
||||
* `err` {Error} The error that triggered the retry.
|
||||
* `context` {RetryContext} The current retry context. See
|
||||
[`RetryContext`](#retrycontext).
|
||||
* `callback` {Function} Signals the outcome of this iteration.
|
||||
* `result` {Error|null} (optional) An `Error` to abort retrying, or `null` to
|
||||
retry.
|
||||
* `maxRetries` {number} Maximum number of retries allowed. **Default:** `5`.
|
||||
* `maxTimeout` {number} Maximum number of milliseconds to wait between retries.
|
||||
**Default:** `30000` (30 seconds).
|
||||
* `minTimeout` {number} Initial number of milliseconds to wait before the first
|
||||
retry. **Default:** `500` (half a second).
|
||||
* `timeoutFactor` {number} Multiplier applied to the timeout between successive
|
||||
retries to produce an exponential backoff. **Default:** `2`.
|
||||
* `retryAfter` {boolean} When `true`, the delay before the next retry is inferred
|
||||
from the `Retry-After` response header when present. **Default:** `true`.
|
||||
* `methods` {string[]} HTTP methods that are eligible for retrying. **Default:**
|
||||
`['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE']`.
|
||||
* `statusCodes` {number[]} HTTP status codes that trigger a retry. **Default:**
|
||||
`[500, 502, 503, 504, 429]`.
|
||||
* `errorCodes` {string[]} Network error codes that trigger a retry. **Default:**
|
||||
`['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN', 'ENETUNREACH', 'EHOSTDOWN', 'EHOSTUNREACH', 'EPIPE', 'UND_ERR_SOCKET']`.
|
||||
|
||||
**`RetryContext`**
|
||||
The default `retry` strategy computes the delay before the next attempt as
|
||||
`minTimeout * timeoutFactor ** (counter - 1)`, capped at `maxTimeout`. When a
|
||||
`Retry-After` header is present it takes precedence (interpreted as seconds, or as
|
||||
an HTTP date), still capped at `maxTimeout`. The default strategy stops retrying
|
||||
once `counter` exceeds `maxRetries`, when the error code is not in `errorCodes`,
|
||||
when the method is not in `methods`, or when the response status code is not in
|
||||
`statusCodes`.
|
||||
|
||||
- `state`: `RetryState` - Current retry state. It can be mutated.
|
||||
- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler.
|
||||
#### `RetryContext`
|
||||
|
||||
**`RetryState`**
|
||||
* `state` {RetryState} The current retry state. See [`RetryState`](#retrystate).
|
||||
* `opts` {DispatchOptions} The dispatch options passed to the handler, including
|
||||
the resolved `retryOptions`. Type is
|
||||
`DispatchOptions & { retryOptions?: RetryOptions }`.
|
||||
|
||||
It represents the retry state for a given request.
|
||||
The context object passed as the second argument to the `retry` callback.
|
||||
|
||||
- `counter`: `number` - Current retry attempt.
|
||||
#### `RetryState`
|
||||
|
||||
### Parameter `RetryHandlers`
|
||||
* `counter` {number} The current retry attempt, starting at `1` for the first
|
||||
retry.
|
||||
|
||||
- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandler) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry.
|
||||
- **handler** Extends [`Dispatch.DispatchHandler`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted.
|
||||
Represents the retry state for a given request.
|
||||
|
||||
>__Note__: The `RetryHandler` does not retry over stateful bodies (e.g. streams, AsyncIterable) as those, once consumed, are left in a state that cannot be reutilized. For these situations the `RetryHandler` will identify
|
||||
>the body as stateful and will not retry the request rejecting with the error `UND_ERR_REQ_RETRY`.
|
||||
#### Parameter: `RetryHandlers`
|
||||
|
||||
Examples:
|
||||
* `dispatch` {Function} The dispatch function called to (re-)issue the request on
|
||||
every attempt. Type is `(options, handler) => boolean`.
|
||||
* `handler` {DispatchHandler} The inner handler invoked once the request succeeds
|
||||
or the retries are exhausted.
|
||||
|
||||
### Examples
|
||||
|
||||
```mjs
|
||||
import { Client, RetryHandler } from 'undici'
|
||||
|
||||
const client = new Client(`http://localhost:${server.address().port}`)
|
||||
const chunks = []
|
||||
|
||||
```js
|
||||
const client = new Client(`http://localhost:${server.address().port}`);
|
||||
const chunks = [];
|
||||
const handler = new RetryHandler(
|
||||
{
|
||||
...dispatchOptions,
|
||||
retryOptions: {
|
||||
// custom retry function
|
||||
retry: function (err, state, callback) {
|
||||
counter++;
|
||||
|
||||
if (err.code && err.code === "UND_ERR_DESTROYED") {
|
||||
callback(err);
|
||||
return;
|
||||
// Custom retry decision function.
|
||||
retry (err, { state, opts }, callback) {
|
||||
if (err.code === 'UND_ERR_DESTROYED') {
|
||||
callback(err)
|
||||
return
|
||||
}
|
||||
|
||||
if (err.statusCode === 206) {
|
||||
callback(err);
|
||||
return;
|
||||
callback(err)
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(() => callback(null), 1000);
|
||||
},
|
||||
},
|
||||
setTimeout(() => callback(null), 1000)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
dispatch: (...args) => {
|
||||
return client.dispatch(...args);
|
||||
dispatch (...args) {
|
||||
return client.dispatch(...args)
|
||||
},
|
||||
handler: {
|
||||
onRequestStart() {},
|
||||
onBodySent(chunk) {},
|
||||
onResponseStart(_controller, status, headers) {
|
||||
// do something with headers
|
||||
onRequestStart () {},
|
||||
onResponseStart (controller, status, headers) {
|
||||
// Do something with the response headers.
|
||||
},
|
||||
onResponseData(_controller, chunk) {
|
||||
chunks.push(chunk);
|
||||
onResponseData (controller, chunk) {
|
||||
chunks.push(chunk)
|
||||
},
|
||||
onResponseEnd() {},
|
||||
onResponseError(_controller, err) {
|
||||
// handle error properly
|
||||
},
|
||||
},
|
||||
onResponseEnd () {},
|
||||
onResponseError (controller, err) {
|
||||
// Handle the error.
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
client.dispatch(dispatchOptions, handler)
|
||||
```
|
||||
|
||||
#### Example - Basic RetryHandler with defaults
|
||||
A minimal handler that relies entirely on the default retry options:
|
||||
|
||||
```mjs
|
||||
import { Client, RetryHandler } from 'undici'
|
||||
|
||||
const client = new Client(`http://localhost:${server.address().port}`)
|
||||
|
||||
```js
|
||||
const client = new Client(`http://localhost:${server.address().port}`);
|
||||
const handler = new RetryHandler(dispatchOptions, {
|
||||
dispatch: client.dispatch.bind(client),
|
||||
handler: {
|
||||
onRequestStart() {},
|
||||
onBodySent(chunk) {},
|
||||
onResponseStart(_controller, status, headers) {},
|
||||
onResponseData(_controller, chunk) {},
|
||||
onResponseEnd() {},
|
||||
onResponseError(_controller, err) {},
|
||||
},
|
||||
});
|
||||
onRequestStart () {},
|
||||
onResponseStart (controller, status, headers) {},
|
||||
onResponseData (controller, chunk) {},
|
||||
onResponseEnd () {},
|
||||
onResponseError (controller, err) {}
|
||||
}
|
||||
})
|
||||
|
||||
client.dispatch(dispatchOptions, handler)
|
||||
```
|
||||
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`DispatchHandler`]: Dispatcher.md#parameter-dispatchhandler
|
||||
[`RetryAgent`]: RetryAgent.md#class-retryagent
|
||||
[`dispatcher.dispatch()`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
+166
-72
@@ -1,131 +1,213 @@
|
||||
# Class: RoundRobinPool
|
||||
# RoundRobinPool
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v7.17.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/dispatcher/round-robin-pool.js -->
|
||||
|
||||
A pool of [Client](/docs/docs/api/Client.md) instances connected to the same upstream target with round-robin client selection.
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Unlike [`Pool`](/docs/docs/api/Pool.md), which always selects the first available client, `RoundRobinPool` cycles through clients in a round-robin fashion. This ensures even distribution of requests across all connections, which is particularly useful when the upstream target is behind a load balancer that round-robins TCP connections across multiple backend servers (e.g., Kubernetes Services).
|
||||
A pool of [`Client`][] instances connected to the same upstream target that
|
||||
selects clients in a round-robin fashion.
|
||||
|
||||
Requests are not guaranteed to be dispatched in order of invocation.
|
||||
Unlike [`Pool`][], which always reuses the first available client,
|
||||
`RoundRobinPool` cycles through its clients so that requests are distributed
|
||||
evenly across every open connection. This is useful when the upstream target is
|
||||
fronted by a load balancer that distributes TCP connections across multiple
|
||||
backend servers (for example, a Kubernetes Service): each connection is pinned
|
||||
to a backend by the load balancer, and `RoundRobinPool` spreads requests across
|
||||
those connections so every backend receives a comparable share of traffic.
|
||||
|
||||
## `new RoundRobinPool(url[, options])`
|
||||
Requests are not guaranteed to be dispatched in the order they were invoked.
|
||||
|
||||
Arguments:
|
||||
```mjs
|
||||
import { RoundRobinPool } from 'undici'
|
||||
|
||||
* **url** `URL | string` - It should only include the **protocol, hostname, and port**.
|
||||
* **options** `RoundRobinPoolOptions` (optional)
|
||||
const pool = new RoundRobinPool('http://localhost:3000', { connections: 10 })
|
||||
```
|
||||
|
||||
### Parameter: `RoundRobinPoolOptions`
|
||||
`RoundRobinPool` distributes HTTP requests evenly across TCP connections, not
|
||||
across backend servers directly. Even backend distribution therefore depends on
|
||||
the load balancer assigning different connections to different backends (for
|
||||
example, round-robin, random, or least-connections without client affinity). If
|
||||
the load balancer pins all connections from one source to the same backend (for
|
||||
example, source-IP affinity or sticky sessions), consider [`BalancedPool`][]
|
||||
with the individual backend addresses instead.
|
||||
|
||||
Extends: [`ClientOptions`](/docs/docs/api/Client.md#parameter-clientoptions)
|
||||
## Class: `RoundRobinPool`
|
||||
|
||||
* **factory** `(origin: URL, opts: Object) => Dispatcher` - Default: `(origin, opts) => new Client(origin, opts)`
|
||||
* **connections** `number | null` (optional) - Default: `null` - The number of `Client` instances to create. When set to `null`, the `RoundRobinPool` instance will create an unlimited amount of `Client` instances.
|
||||
* **clientTtl** `number | null` (optional) - Default: `null` - The amount of time before a `Client` instance is removed from the `RoundRobinPool` and closed. When set to `null`, `Client` instances will not be removed or closed based on age.
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
## Use Case
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
`RoundRobinPool` is designed for scenarios where:
|
||||
### `new RoundRobinPool(url[, options])`
|
||||
|
||||
1. You connect to a single origin (e.g., `http://my-service.namespace.svc`)
|
||||
2. That origin is backed by a load balancer distributing TCP connections across multiple servers
|
||||
3. You want requests evenly distributed across all backend servers
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
**Example**: In Kubernetes, when using a Service DNS name with multiple Pod replicas, kube-proxy load balances TCP connections. `RoundRobinPool` ensures each connection (and thus each Pod) receives an equal share of requests.
|
||||
* `url` {URL|string} The upstream target. It should only include the
|
||||
**protocol, hostname, and port**.
|
||||
* `options` {RoundRobinPoolOptions} (optional)
|
||||
|
||||
### Important: Backend Distribution Considerations
|
||||
#### Parameter: `RoundRobinPoolOptions`
|
||||
|
||||
`RoundRobinPool` distributes **HTTP requests** evenly across **TCP connections**. Whether this translates to even backend server distribution depends on the load balancer's behavior:
|
||||
Extends: {ClientOptions}
|
||||
|
||||
**✓ Works when the load balancer**:
|
||||
- Assigns different backends to different TCP connections from the same client
|
||||
- Uses algorithms like: round-robin, random, least-connections (without client affinity)
|
||||
- Example: Default Kubernetes Services without `sessionAffinity`
|
||||
* `factory` {Function} A function used to create the underlying {Client}
|
||||
instances. **Default:** `(origin, opts) => new Client(origin, opts)`.
|
||||
* `origin` {URL}
|
||||
* `opts` {Object}
|
||||
* Returns: {Dispatcher}
|
||||
* `connections` {number|null} (optional) The maximum number of {Client}
|
||||
instances to create. When set to `null`, the `RoundRobinPool` instance creates
|
||||
an unlimited number of {Client} instances. **Default:** `null`.
|
||||
* `clientTtl` {number|null} (optional) The amount of time, in milliseconds,
|
||||
before a {Client} instance is removed from the `RoundRobinPool` and closed.
|
||||
When set to `null`, {Client} instances are not removed or closed based on age.
|
||||
**Default:** `null`.
|
||||
|
||||
**✗ Does NOT work when**:
|
||||
- Load balancer has client/source IP affinity (all connections from one IP → same backend)
|
||||
- Load balancer uses source-IP-hash or sticky sessions
|
||||
`RoundRobinPool` inherits all [`Client`][] options. A [`Client`][] instance is
|
||||
created lazily on the first dispatch and additional instances are created on
|
||||
demand, up to `connections`, when every existing client is busy.
|
||||
|
||||
**How it works:**
|
||||
1. `RoundRobinPool` creates N TCP connections to the load balancer endpoint
|
||||
2. Load balancer assigns each TCP connection to a backend (per its algorithm)
|
||||
3. `RoundRobinPool` cycles HTTP requests across those N connections
|
||||
4. Result: Requests distributed proportionally to how the LB distributed the connections
|
||||
### `roundRobinPool.closed`
|
||||
|
||||
If the load balancer assigns all connections to the same backend (e.g., due to session affinity), `RoundRobinPool` cannot overcome this. In such cases, consider using [`BalancedPool`](/docs/docs/api/BalancedPool.md) with direct backend addresses (e.g., individual pod IPs) instead of a load-balanced endpoint.
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
## Instance Properties
|
||||
* Type: {boolean}
|
||||
|
||||
### `RoundRobinPool.closed`
|
||||
`true` after `roundRobinPool.close()` has been called.
|
||||
|
||||
Implements [Client.closed](/docs/docs/api/Client.md#clientclosed)
|
||||
### `roundRobinPool.destroyed`
|
||||
|
||||
### `RoundRobinPool.destroyed`
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
Implements [Client.destroyed](/docs/docs/api/Client.md#clientdestroyed)
|
||||
* Type: {boolean}
|
||||
|
||||
### `RoundRobinPool.stats`
|
||||
`true` after `roundRobinPool.destroy()` has been called, or after
|
||||
`roundRobinPool.close()` has been called and the pool shutdown has completed.
|
||||
|
||||
Returns [`PoolStats`](/docs/docs/api/PoolStats.md) instance for this pool.
|
||||
### `roundRobinPool.stats`
|
||||
|
||||
## Instance Methods
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
### `RoundRobinPool.close([callback])`
|
||||
* Type: {PoolStats}
|
||||
|
||||
Implements [`Dispatcher.close([callback])`](/docs/docs/api/Dispatcher.md#dispatcherclosecallback-promise).
|
||||
Aggregate connection statistics for the pool. See [`PoolStats`][].
|
||||
|
||||
### `RoundRobinPool.destroy([error, callback])`
|
||||
### `roundRobinPool.close([callback])`
|
||||
|
||||
Implements [`Dispatcher.destroy([error, callback])`](/docs/docs/api/Dispatcher.md#dispatcherdestroyerror-callback-promise).
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
### `RoundRobinPool.connect(options[, callback])`
|
||||
Closes the pool and gracefully waits for enqueued requests to complete before
|
||||
resolving. Implements [`dispatcher.close([callback])`][].
|
||||
|
||||
See [`Dispatcher.connect(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback).
|
||||
### `roundRobinPool.destroy([error[, callback]])`
|
||||
|
||||
### `RoundRobinPool.dispatch(options, handler)`
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handler).
|
||||
Destroys the pool abruptly. All pending and running requests are aborted with
|
||||
the given `error`. Implements [`dispatcher.destroy([error[, callback]])`][].
|
||||
|
||||
### `RoundRobinPool.pipeline(options, handler)`
|
||||
### `roundRobinPool.connect(options[, callback])`
|
||||
|
||||
See [`Dispatcher.pipeline(options, handler)`](/docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler).
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
### `RoundRobinPool.request(options[, callback])`
|
||||
Starts two-way communications with the requested resource using
|
||||
[HTTP CONNECT][]. See [`dispatcher.connect(options[, callback])`][].
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
### `roundRobinPool.dispatch(options, handler)`
|
||||
|
||||
### `RoundRobinPool.stream(options, factory[, callback])`
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
See [`Dispatcher.stream(options, factory[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback).
|
||||
Dispatches a request through the next client selected in round-robin order.
|
||||
Implements [`dispatcher.dispatch(options, handler)`][].
|
||||
|
||||
### `RoundRobinPool.upgrade(options[, callback])`
|
||||
### `roundRobinPool.pipeline(options, handler)`
|
||||
|
||||
See [`Dispatcher.upgrade(options[, callback])`](/docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback).
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
## Instance Events
|
||||
For easy use with [`stream.pipeline`][]. See
|
||||
[`dispatcher.pipeline(options, handler)`][].
|
||||
|
||||
### `roundRobinPool.request(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
Performs an HTTP request. See [`dispatcher.request(options[, callback])`][].
|
||||
|
||||
### `roundRobinPool.stream(options, factory[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
A faster version of [`roundRobinPool.request()`][]. See
|
||||
[`dispatcher.stream(options, factory[, callback])`][].
|
||||
|
||||
### `roundRobinPool.upgrade(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
Upgrades a connection to a different protocol. See
|
||||
[`dispatcher.upgrade(options[, callback])`][].
|
||||
|
||||
### Event: `'connect'`
|
||||
|
||||
See [Dispatcher Event: `'connect'`](/docs/docs/api/Dispatcher.md#event-connect).
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'connect'`][].
|
||||
|
||||
### Event: `'disconnect'`
|
||||
|
||||
See [Dispatcher Event: `'disconnect'`](/docs/docs/api/Dispatcher.md#event-disconnect).
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'disconnect'`][].
|
||||
|
||||
### Event: `'drain'`
|
||||
|
||||
See [Dispatcher Event: `'drain'`](/docs/docs/api/Dispatcher.md#event-drain).
|
||||
<!-- YAML
|
||||
added: v7.17.0
|
||||
-->
|
||||
|
||||
See [Dispatcher Event: `'drain'`][].
|
||||
|
||||
## Example
|
||||
|
||||
```javascript
|
||||
```mjs
|
||||
import { RoundRobinPool } from 'undici'
|
||||
|
||||
const pool = new RoundRobinPool('http://my-service.default.svc.cluster.local', {
|
||||
const pool = new RoundRobinPool('http://localhost:3000', {
|
||||
connections: 10
|
||||
})
|
||||
|
||||
// Requests will be distributed evenly across all 10 connections
|
||||
// Requests are distributed evenly across all 10 connections.
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const { body } = await pool.request({
|
||||
path: '/api/data',
|
||||
@@ -137,9 +219,21 @@ for (let i = 0; i < 100; i++) {
|
||||
await pool.close()
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- [Pool](/docs/docs/api/Pool.md) - Connection pool without round-robin
|
||||
- [BalancedPool](/docs/docs/api/BalancedPool.md) - Load balancing across multiple origins
|
||||
- [Issue #3648](https://github.com/nodejs/undici/issues/3648) - Original issue describing uneven distribution
|
||||
|
||||
[HTTP CONNECT]: https://developer.mozilla.org/docs/Web/HTTP/Methods/CONNECT
|
||||
[`BalancedPool`]: BalancedPool.md#class-balancedpool
|
||||
[`Client`]: Client.md#class-client
|
||||
[`Pool`]: Pool.md#class-pool
|
||||
[`PoolStats`]: PoolStats.md#class-poolstats
|
||||
[`dispatcher.close([callback])`]: Dispatcher.md#dispatcherclosecallback-promise
|
||||
[`dispatcher.connect(options[, callback])`]: Dispatcher.md#dispatcherconnectoptions-callback
|
||||
[`dispatcher.destroy([error[, callback]])`]: Dispatcher.md#dispatcherdestroyerror-callback-promise
|
||||
[`dispatcher.dispatch(options, handler)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`dispatcher.pipeline(options, handler)`]: Dispatcher.md#dispatcherpipelineoptions-handler
|
||||
[`dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`dispatcher.stream(options, factory[, callback])`]: Dispatcher.md#dispatcherstreamoptions-factory-callback
|
||||
[`dispatcher.upgrade(options[, callback])`]: Dispatcher.md#dispatcherupgradeoptions-callback
|
||||
[`roundRobinPool.request()`]: #roundrobinpoolrequestoptions-callback
|
||||
[`stream.pipeline`]: https://nodejs.org/api/stream.html#streampipelinesource-transforms-destination-callback
|
||||
[Dispatcher Event: `'connect'`]: Dispatcher.md#event-connect
|
||||
[Dispatcher Event: `'disconnect'`]: Dispatcher.md#event-disconnect
|
||||
[Dispatcher Event: `'drain'`]: Dispatcher.md#event-drain
|
||||
+265
-543
@@ -1,639 +1,361 @@
|
||||
# SnapshotAgent
|
||||
|
||||
The `SnapshotAgent` provides a powerful way to record and replay HTTP requests for testing purposes. It extends `MockAgent` to enable automatic snapshot testing, eliminating the need to manually define mock responses.
|
||||
<!--introduced_in=v7.13.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/mock/snapshot-agent.js -->
|
||||
|
||||
## Use Cases
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
- **Integration Testing**: Record real API interactions and replay them in tests
|
||||
- **Offline Development**: Work with APIs without network connectivity
|
||||
- **Consistent Test Data**: Ensure tests use the same responses across runs
|
||||
- **API Contract Testing**: Capture and validate API behavior over time
|
||||
`SnapshotAgent` records real HTTP responses and replays them on later requests,
|
||||
allowing tests to run against deterministic, captured data instead of a live
|
||||
network. It extends [`MockAgent`][], so it can be installed with
|
||||
[`setGlobalDispatcher()`][] or passed explicitly through the `dispatcher`
|
||||
option, and it works with every undici API (`fetch`, `request`, `stream`,
|
||||
`pipeline`, and so on).
|
||||
|
||||
## Constructor
|
||||
A `SnapshotAgent` operates in one of three modes. In `'record'` mode it performs
|
||||
real requests and writes the responses to a snapshot file. In `'playback'` mode
|
||||
it serves responses from the snapshot file without touching the network. In
|
||||
`'update'` mode it replays existing snapshots and records any request that has no
|
||||
matching snapshot.
|
||||
|
||||
```javascript
|
||||
new SnapshotAgent([options])
|
||||
```
|
||||
```mjs
|
||||
import { SnapshotAgent, setGlobalDispatcher, fetch } from 'undici'
|
||||
|
||||
### Parameters
|
||||
|
||||
- **options** `Object` (optional)
|
||||
- **mode** `String` - The snapshot mode: `'record'`, `'playback'`, or `'update'`. Default: `'record'`
|
||||
- **snapshotPath** `String` - Path to the snapshot file for loading/saving
|
||||
- **maxSnapshots** `Number` - Maximum number of snapshots to keep in memory. Default: `Infinity`
|
||||
- **autoFlush** `Boolean` - Whether to automatically save snapshots to disk. Default: `false`
|
||||
- **flushInterval** `Number` - Interval in milliseconds for auto-flush. Default: `30000`
|
||||
- **matchHeaders** `Array<String>` - Specific headers to include in request matching. Default: all headers
|
||||
- **ignoreHeaders** `Array<String>` - Headers to ignore during request matching
|
||||
- **excludeHeaders** `Array<String>` - Headers to exclude from snapshots (for security)
|
||||
- **matchBody** `Boolean` - Whether to include request body in matching. Default: `true`
|
||||
- **normalizeBody** `Function` - Optional function `(body) => string` to normalize the request body before matching (e.g. strip volatile fields like timestamps). Only used when `matchBody` is `true`.
|
||||
- **matchQuery** `Boolean` - Whether to include query parameters in matching. Default: `true`
|
||||
- **normalizeQuery** `Function` - Optional function `(query: URLSearchParams) => string` to normalize query parameters before matching (e.g. strip volatile params like cache-busters). Only used when `matchQuery` is `true`.
|
||||
- **caseSensitive** `Boolean` - Whether header matching is case-sensitive. Default: `false`
|
||||
- **shouldRecord** `Function` - Callback to determine if a request should be recorded
|
||||
- **shouldPlayback** `Function` - Callback to determine if a request should be played back
|
||||
- **excludeUrls** `Array` - URL patterns (strings or RegExp) to exclude from recording/playback
|
||||
- All other options from `MockAgent` are supported
|
||||
|
||||
### Modes
|
||||
|
||||
#### Record Mode (`'record'`)
|
||||
Makes real HTTP requests and saves the responses to snapshots.
|
||||
|
||||
```javascript
|
||||
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './test/snapshots/api-calls.json'
|
||||
snapshotPath: './snapshots/api.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// Makes real requests and records them
|
||||
const response = await fetch('https://api.example.com/users')
|
||||
const users = await response.json()
|
||||
|
||||
// Save recorded snapshots
|
||||
await agent.saveSnapshots()
|
||||
await agent.close()
|
||||
```
|
||||
|
||||
#### Playback Mode (`'playback'`)
|
||||
Replays recorded responses without making real HTTP requests.
|
||||
Constructing a `SnapshotAgent` emits an `ExperimentalWarning` once per process.
|
||||
|
||||
```javascript
|
||||
## Class: `SnapshotAgent`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
* Extends: {MockAgent}
|
||||
|
||||
The captured interactions are managed by an internal `SnapshotRecorder`, which
|
||||
is accessible through [`agent.getRecorder()`][]. Because `SnapshotAgent` extends
|
||||
[`MockAgent`][], all `MockAgent` and [`Dispatcher`][] methods and events are also
|
||||
available.
|
||||
|
||||
### `new SnapshotAgent([options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
* `options` {Object} (optional) Accepts all [`MockAgent`][] options plus the
|
||||
following snapshot-specific options.
|
||||
* `mode` {string} The snapshot mode. One of `'record'`, `'playback'`, or
|
||||
`'update'`. **Default:** `'record'`.
|
||||
* `snapshotPath` {string} Path to the snapshot file used for loading and
|
||||
saving. Required when `mode` is `'playback'` or `'update'`. **Default:**
|
||||
`null`.
|
||||
* `maxSnapshots` {number} Maximum number of snapshots to keep in memory.
|
||||
**Default:** `Infinity`.
|
||||
* `autoFlush` {boolean} When `true`, snapshots are written to `snapshotPath`
|
||||
automatically as they are recorded. **Default:** `false`.
|
||||
* `flushInterval` {number} Interval, in milliseconds, between automatic flushes
|
||||
when `autoFlush` is enabled. **Default:** `30000`.
|
||||
* `matchHeaders` {string[]} Names of the only headers to include when matching
|
||||
a request against a snapshot. When omitted, all headers are matched.
|
||||
* `ignoreHeaders` {string[]} Header names to ignore when matching a request.
|
||||
* `excludeHeaders` {string[]} Header names to strip from snapshots entirely,
|
||||
so that sensitive values are never written to disk.
|
||||
* `matchBody` {boolean} When `true`, the request body is included in request
|
||||
matching. **Default:** `true`.
|
||||
* `normalizeBody` {Function} Normalizes the request body before it is matched
|
||||
or stored, for example to strip volatile fields such as timestamps. Only
|
||||
applied when `matchBody` is `true`.
|
||||
* `body` {string|Buffer|null|undefined} The raw request body.
|
||||
* Returns: {string} The normalized body used for matching.
|
||||
* `matchQuery` {boolean} When `true`, query parameters are included in request
|
||||
matching. **Default:** `true`.
|
||||
* `normalizeQuery` {Function} Normalizes the query string before it is matched,
|
||||
for example to strip cache-busting parameters. Only applied when `matchQuery`
|
||||
is `true`.
|
||||
* `query` {URLSearchParams} The parsed query parameters.
|
||||
* Returns: {string} The normalized query string used for matching.
|
||||
* `caseSensitive` {boolean} When `true`, header matching is case-sensitive.
|
||||
**Default:** `false`.
|
||||
* `shouldRecord` {Function} Decides whether a given request is recorded. Only
|
||||
used in `'record'` and `'update'` modes.
|
||||
* `requestOpts` {Object} The dispatch options of the request.
|
||||
* Returns: {boolean} `true` to record the request.
|
||||
* `shouldPlayback` {Function} Decides whether a given request is served from a
|
||||
snapshot. Only used in `'playback'` and `'update'` modes.
|
||||
* `requestOpts` {Object} The dispatch options of the request.
|
||||
* Returns: {boolean} `true` to play the request back from a snapshot.
|
||||
* `excludeUrls` {Array<string|RegExp>} URL patterns that are excluded from
|
||||
recording and playback. String patterns match case-insensitively as
|
||||
substrings of the URL; `RegExp` patterns are tested against the URL. Matching
|
||||
requests are passed through to a real [`Agent`][] instead.
|
||||
|
||||
Creates a new `SnapshotAgent`. Throws an [`InvalidArgumentError`][] when `mode`
|
||||
is `'playback'` or `'update'` and `snapshotPath` is not provided, and throws an
|
||||
`InvalidArgumentError` when `mode` is not one of the supported values.
|
||||
|
||||
When `mode` is `'playback'` or `'update'` and `snapshotPath` is set, snapshots
|
||||
are loaded from the file automatically; a missing file is ignored until the first
|
||||
request requires it.
|
||||
|
||||
```mjs
|
||||
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/api-calls.json'
|
||||
snapshotPath: './snapshots/api.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// Uses recorded response instead of real request
|
||||
const response = await fetch('https://api.example.com/users')
|
||||
```
|
||||
|
||||
#### Update Mode (`'update'`)
|
||||
Uses existing snapshots when available, but records new ones for missing requests.
|
||||
The mode controls how requests are handled:
|
||||
|
||||
```javascript
|
||||
import { SnapshotAgent, setGlobalDispatcher } from 'undici'
|
||||
* `'record'` performs the real request and stores the response.
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'update',
|
||||
snapshotPath: './test/snapshots/api-calls.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
```mjs
|
||||
import { SnapshotAgent, setGlobalDispatcher, fetch } from 'undici'
|
||||
|
||||
// Uses snapshot if exists, otherwise makes real request and records it
|
||||
const response = await fetch('https://api.example.com/new-endpoint')
|
||||
```
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots/api.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
## Instance Methods
|
||||
await fetch('https://api.example.com/users')
|
||||
await agent.saveSnapshots()
|
||||
```
|
||||
|
||||
### `agent.saveSnapshots([filePath])`
|
||||
* `'playback'` serves the recorded response without any network access. A
|
||||
request with no matching snapshot rejects with an [`UndiciError`][] whose
|
||||
message begins with `No snapshot found`.
|
||||
|
||||
Saves all recorded snapshots to a file.
|
||||
```mjs
|
||||
import { SnapshotAgent, setGlobalDispatcher, fetch } from 'undici'
|
||||
|
||||
#### Parameters
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './snapshots/api.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
- **filePath** `String` (optional) - Path to save snapshots. Uses constructor `snapshotPath` if not provided.
|
||||
const response = await fetch('https://api.example.com/users')
|
||||
```
|
||||
|
||||
#### Returns
|
||||
* `'update'` replays an existing snapshot when one is found and otherwise records
|
||||
the request like `'record'` mode.
|
||||
|
||||
`Promise<void>`
|
||||
```mjs
|
||||
import { SnapshotAgent, setGlobalDispatcher, fetch } from 'undici'
|
||||
|
||||
```javascript
|
||||
await agent.saveSnapshots('./custom-snapshots.json')
|
||||
```
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'update',
|
||||
snapshotPath: './snapshots/api.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
## Advanced Configuration
|
||||
|
||||
### Body Matching
|
||||
|
||||
By default (`matchBody: true`) the full request body string is included in the snapshot key. Set it to `false` to ignore the body entirely, or use `normalizeBody` to strip volatile fields (like timestamps) before matching:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Match on everything except the timestamp field
|
||||
normalizeBody: (body) => {
|
||||
if (!body) return ''
|
||||
const parsed = JSON.parse(String(body))
|
||||
delete parsed.timestamp
|
||||
return JSON.stringify(parsed)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
`normalizeBody` receives the raw body (`string | Buffer | null | undefined`) and must return a `string`. It runs at both record and playback time so the hash is consistent. Two requests match the same snapshot whenever their normalized strings are identical.
|
||||
|
||||
### Header Filtering
|
||||
|
||||
Control which headers are used for request matching and what gets stored in snapshots:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Only match these specific headers
|
||||
matchHeaders: ['content-type', 'accept'],
|
||||
|
||||
// Ignore these headers during matching (but still store them)
|
||||
ignoreHeaders: ['user-agent', 'date'],
|
||||
|
||||
// Exclude sensitive headers from snapshots entirely
|
||||
excludeHeaders: ['authorization', 'x-api-key', 'cookie']
|
||||
})
|
||||
```
|
||||
|
||||
### Custom Request/Response Filtering
|
||||
|
||||
Use callback functions to determine what gets recorded or played back:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Only record GET requests to specific endpoints
|
||||
shouldRecord: (requestOpts) => {
|
||||
const url = new URL(requestOpts.path, requestOpts.origin)
|
||||
return requestOpts.method === 'GET' && url.pathname.startsWith('/api/v1/')
|
||||
},
|
||||
|
||||
// Skip authentication endpoints during playback
|
||||
shouldPlayback: (requestOpts) => {
|
||||
const url = new URL(requestOpts.path, requestOpts.origin)
|
||||
return !url.pathname.includes('/auth/')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### URL Pattern Exclusion
|
||||
|
||||
Exclude specific URLs from recording/playback using patterns:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
excludeUrls: [
|
||||
'https://analytics.example.com', // String match
|
||||
/\/api\/v\d+\/health/, // Regex pattern
|
||||
'telemetry' // Substring match
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Memory Management
|
||||
|
||||
Configure automatic memory and disk management:
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Keep only 1000 snapshots in memory
|
||||
maxSnapshots: 1000,
|
||||
|
||||
// Automatically save to disk every 30 seconds
|
||||
autoFlush: true,
|
||||
flushInterval: 30000
|
||||
})
|
||||
```
|
||||
|
||||
### Sequential Response Handling
|
||||
|
||||
Handle multiple responses for the same request (similar to nock):
|
||||
|
||||
```javascript
|
||||
// In record mode, multiple identical requests get recorded as separate responses
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './sequential.json' })
|
||||
|
||||
// First call returns response A
|
||||
await fetch('https://api.example.com/random')
|
||||
|
||||
// Second call returns response B
|
||||
await fetch('https://api.example.com/random')
|
||||
|
||||
await agent.saveSnapshots()
|
||||
|
||||
// In playback mode, calls return responses in sequence
|
||||
const playbackAgent = new SnapshotAgent({ mode: 'playback', snapshotPath: './sequential.json' })
|
||||
|
||||
// Returns response A
|
||||
const first = await fetch('https://api.example.com/random')
|
||||
|
||||
// Returns response B
|
||||
const second = await fetch('https://api.example.com/random')
|
||||
|
||||
// Third call repeats the last response (B)
|
||||
const third = await fetch('https://api.example.com/random')
|
||||
```
|
||||
|
||||
## Managing Snapshots
|
||||
|
||||
### Replacing Existing Snapshots
|
||||
|
||||
```javascript
|
||||
// Load existing snapshots
|
||||
await agent.loadSnapshots('./old-snapshots.json')
|
||||
|
||||
// Get snapshot data
|
||||
const recorder = agent.getRecorder()
|
||||
const snapshots = recorder.getSnapshots()
|
||||
|
||||
// Modify or filter snapshots
|
||||
const filteredSnapshots = snapshots.filter(s =>
|
||||
!s.request.url.includes('deprecated')
|
||||
)
|
||||
|
||||
// Replace all snapshots
|
||||
agent.replaceSnapshots(filteredSnapshots.map((snapshot, index) => ({
|
||||
hash: `new-hash-${index}`,
|
||||
snapshot
|
||||
})))
|
||||
|
||||
// Save updated snapshots
|
||||
await agent.saveSnapshots('./updated-snapshots.json')
|
||||
```
|
||||
await fetch('https://api.example.com/new-endpoint')
|
||||
```
|
||||
|
||||
### `agent.loadSnapshots([filePath])`
|
||||
|
||||
Loads snapshots from a file.
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
#### Parameters
|
||||
* `filePath` {string} (optional) Path to read snapshots from. **Default:** the
|
||||
`snapshotPath` given to the constructor.
|
||||
* Returns: {Promise<void>} Resolves when the snapshots have been loaded.
|
||||
|
||||
- **filePath** `String` (optional) - Path to load snapshots from. Uses constructor `snapshotPath` if not provided.
|
||||
Loads snapshots from disk into memory. In `'playback'` mode this also installs
|
||||
the corresponding [`MockAgent`][] interceptors so that subsequent requests are
|
||||
matched against the loaded snapshots.
|
||||
|
||||
#### Returns
|
||||
```mjs
|
||||
await agent.loadSnapshots('./snapshots/api.json')
|
||||
```
|
||||
|
||||
`Promise<void>`
|
||||
### `agent.saveSnapshots([filePath])`
|
||||
|
||||
```javascript
|
||||
await agent.loadSnapshots('./existing-snapshots.json')
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
* `filePath` {string} (optional) Path to write snapshots to. **Default:** the
|
||||
`snapshotPath` given to the constructor.
|
||||
* Returns: {Promise<void>} Resolves when the snapshots have been written.
|
||||
|
||||
Writes all recorded snapshots to disk.
|
||||
|
||||
```mjs
|
||||
await agent.saveSnapshots('./snapshots/api.json')
|
||||
```
|
||||
|
||||
### `agent.getRecorder()`
|
||||
|
||||
Gets the underlying `SnapshotRecorder` instance.
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
#### Returns
|
||||
* Returns: {SnapshotRecorder} The internal recorder that stores the captured
|
||||
interactions.
|
||||
|
||||
`SnapshotRecorder`
|
||||
Returns the underlying `SnapshotRecorder`. The recorder exposes lower-level
|
||||
operations over the captured data, including `record()`, `findSnapshot()`,
|
||||
`getSnapshots()`, `size()`, and `clear()`. The recorder is an internal type
|
||||
returned for inspection; it is not exported from the package root.
|
||||
|
||||
```javascript
|
||||
```mjs
|
||||
const recorder = agent.getRecorder()
|
||||
console.log(`Recorded ${recorder.size()} interactions`)
|
||||
```
|
||||
|
||||
### `agent.getMode()`
|
||||
|
||||
Gets the current snapshot mode.
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
#### Returns
|
||||
* Returns: {string} The current mode: `'record'`, `'playback'`, or `'update'`.
|
||||
|
||||
`String` - The current mode (`'record'`, `'playback'`, or `'update'`)
|
||||
Returns the mode the agent was constructed with.
|
||||
|
||||
### `agent.clearSnapshots()`
|
||||
|
||||
Clears all recorded snapshots from memory.
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
```javascript
|
||||
* Returns: {undefined}
|
||||
|
||||
Removes all snapshots from memory.
|
||||
|
||||
```mjs
|
||||
agent.clearSnapshots()
|
||||
```
|
||||
|
||||
## Working with Different Request Types
|
||||
### `agent.resetCallCounts()`
|
||||
|
||||
### GET Requests
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
```javascript
|
||||
// Record mode
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './get-snapshots.json' })
|
||||
setGlobalDispatcher(agent)
|
||||
* Returns: {undefined}
|
||||
|
||||
const response = await fetch('https://jsonplaceholder.typicode.com/posts/1')
|
||||
const post = await response.json()
|
||||
Resets the call count of every snapshot to zero. Useful between tests when the
|
||||
same agent is reused, since matching a snapshot in `'playback'` mode increments
|
||||
its call count.
|
||||
|
||||
await agent.saveSnapshots()
|
||||
```mjs
|
||||
agent.resetCallCounts()
|
||||
```
|
||||
|
||||
### POST Requests with Body
|
||||
### `agent.deleteSnapshot(requestOpts)`
|
||||
|
||||
```javascript
|
||||
// Record mode
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './post-snapshots.json' })
|
||||
setGlobalDispatcher(agent)
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
const response = await fetch('https://jsonplaceholder.typicode.com/posts', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ title: 'Test Post', body: 'Content' })
|
||||
})
|
||||
* `requestOpts` {Object} Dispatch options identifying the snapshot to remove.
|
||||
* Returns: {boolean} `true` if a matching snapshot was deleted, `false` if none
|
||||
was found.
|
||||
|
||||
await agent.saveSnapshots()
|
||||
```
|
||||
Deletes a single snapshot that matches the given request options.
|
||||
|
||||
### Using with `undici.request`
|
||||
|
||||
SnapshotAgent works with all undici APIs, not just fetch:
|
||||
|
||||
```javascript
|
||||
import { SnapshotAgent, request, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './request-snapshots.json' })
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
const { statusCode, headers, body } = await request('https://api.example.com/data')
|
||||
const data = await body.json()
|
||||
|
||||
await agent.saveSnapshots()
|
||||
```
|
||||
|
||||
## Test Integration
|
||||
|
||||
### Basic Test Setup
|
||||
|
||||
```javascript
|
||||
import { test } from 'node:test'
|
||||
import { SnapshotAgent, setGlobalDispatcher, getGlobalDispatcher } from 'undici'
|
||||
|
||||
test('API integration test', async (t) => {
|
||||
const originalDispatcher = getGlobalDispatcher()
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/api-test.json'
|
||||
})
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
t.after(() => setGlobalDispatcher(originalDispatcher))
|
||||
|
||||
// This will use recorded data
|
||||
const response = await fetch('https://api.example.com/users')
|
||||
const users = await response.json()
|
||||
|
||||
assert(Array.isArray(users))
|
||||
assert(users.length > 0)
|
||||
```mjs
|
||||
const deleted = agent.deleteSnapshot({
|
||||
method: 'GET',
|
||||
origin: 'https://api.example.com',
|
||||
path: '/users'
|
||||
})
|
||||
```
|
||||
|
||||
### Environment-Based Mode Selection
|
||||
### `agent.getSnapshotInfo(requestOpts)`
|
||||
|
||||
```javascript
|
||||
const mode = process.env.SNAPSHOT_MODE || 'playback'
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
const agent = new SnapshotAgent({
|
||||
mode,
|
||||
snapshotPath: './test/snapshots/integration.json'
|
||||
})
|
||||
* `requestOpts` {Object} Dispatch options identifying the snapshot to inspect.
|
||||
* Returns: {Object|null} Information about the matching snapshot, or `null` when
|
||||
none is found.
|
||||
* `hash` {string} The hash used to key the snapshot.
|
||||
* `request` {Object} The recorded request.
|
||||
* `method` {string} The request method.
|
||||
* `url` {string} The request URL.
|
||||
* `headers` {Record<string, string>} The recorded request headers.
|
||||
* `body` {string} The recorded request body, when present.
|
||||
* `responseCount` {number} The number of recorded responses for the request.
|
||||
* `callCount` {number} The number of times the snapshot has been matched.
|
||||
* `timestamp` {string} The ISO timestamp at which the snapshot was recorded.
|
||||
|
||||
// Run with: SNAPSHOT_MODE=record npm test (to record)
|
||||
// Run with: npm test (to playback)
|
||||
```
|
||||
Returns metadata about a snapshot without returning the response payload.
|
||||
|
||||
### Test Helper Function
|
||||
|
||||
```javascript
|
||||
function createSnapshotAgent(testName, mode = 'playback') {
|
||||
return new SnapshotAgent({
|
||||
mode,
|
||||
snapshotPath: `./test/snapshots/${testName}.json`
|
||||
})
|
||||
}
|
||||
|
||||
test('user API test', async (t) => {
|
||||
const agent = createSnapshotAgent('user-api')
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// Test implementation...
|
||||
```mjs
|
||||
const info = agent.getSnapshotInfo({
|
||||
method: 'GET',
|
||||
origin: 'https://api.example.com',
|
||||
path: '/users'
|
||||
})
|
||||
```
|
||||
|
||||
## Snapshot File Format
|
||||
### `agent.replaceSnapshots(snapshotData)`
|
||||
|
||||
Snapshots are stored as JSON with the following structure:
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"hash": "dGVzdC1oYXNo...",
|
||||
"snapshot": {
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://api.example.com/users",
|
||||
"headers": {
|
||||
"authorization": "Bearer token"
|
||||
},
|
||||
"body": undefined
|
||||
},
|
||||
"response": {
|
||||
"statusCode": 200,
|
||||
"headers": {
|
||||
"content-type": "application/json"
|
||||
},
|
||||
"body": "eyJkYXRhIjoidGVzdCJ9", // base64 encoded
|
||||
"trailers": {}
|
||||
},
|
||||
"timestamp": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
* `snapshotData` {Array} The snapshot data to load, replacing any existing
|
||||
snapshots.
|
||||
* `hash` {string} The hash used to key the snapshot.
|
||||
* `snapshot` {Object} The snapshot entry.
|
||||
* Returns: {undefined}
|
||||
|
||||
Replaces all in-memory snapshots with the provided data.
|
||||
|
||||
```mjs
|
||||
const recorder = agent.getRecorder()
|
||||
const snapshots = recorder.getSnapshots()
|
||||
|
||||
agent.replaceSnapshots(snapshots.map((snapshot, index) => ({
|
||||
hash: `snapshot-${index}`,
|
||||
snapshot
|
||||
})))
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
### `agent.close()`
|
||||
|
||||
### Sensitive Data in Snapshots
|
||||
<!-- YAML
|
||||
added: v7.13.0
|
||||
-->
|
||||
|
||||
By default, SnapshotAgent records all headers and request/response data. For production use, always exclude sensitive information:
|
||||
* Returns: {Promise<void>} Resolves once the agent and its resources are closed.
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'record',
|
||||
snapshotPath: './snapshots.json',
|
||||
|
||||
// Exclude sensitive headers from snapshots
|
||||
excludeHeaders: [
|
||||
'authorization',
|
||||
'x-api-key',
|
||||
'cookie',
|
||||
'set-cookie',
|
||||
'x-auth-token',
|
||||
'x-csrf-token'
|
||||
],
|
||||
|
||||
// Filter out requests with sensitive data
|
||||
shouldRecord: (requestOpts) => {
|
||||
const url = new URL(requestOpts.path, requestOpts.origin)
|
||||
|
||||
// Don't record authentication endpoints
|
||||
if (url.pathname.includes('/auth/') || url.pathname.includes('/login')) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Don't record if request contains sensitive body data
|
||||
if (requestOpts.body && typeof requestOpts.body === 'string') {
|
||||
const body = requestOpts.body.toLowerCase()
|
||||
if (body.includes('password') || body.includes('secret')) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
})
|
||||
Closes the agent and releases its resources. In `'record'` and `'update'` modes
|
||||
the recorder is flushed to disk first. In `'playback'` mode nothing is written,
|
||||
because matching a snapshot mutates its call count and saving would needlessly
|
||||
rewrite the snapshot file. The real [`Agent`][], when one was created, and the
|
||||
underlying [`MockAgent`][] are closed as well.
|
||||
|
||||
```mjs
|
||||
await agent.close()
|
||||
```
|
||||
|
||||
### Snapshot File Security
|
||||
|
||||
**Important**: Snapshot files may contain sensitive data. Handle them securely:
|
||||
|
||||
- ✅ Add snapshot files to `.gitignore` if they contain real API data
|
||||
- ✅ Use environment-specific snapshots (dev/staging/prod)
|
||||
- ✅ Regularly review snapshot contents for sensitive information
|
||||
- ✅ Use the `excludeHeaders` option for production snapshots
|
||||
- ❌ Never commit snapshots with real authentication tokens
|
||||
- ❌ Don't share snapshot files containing personal data
|
||||
|
||||
```gitignore
|
||||
# Exclude snapshots with real data
|
||||
/test/snapshots/production-*.json
|
||||
/test/snapshots/*-real-data.json
|
||||
|
||||
# Include sanitized test snapshots
|
||||
!/test/snapshots/mock-*.json
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Missing Snapshots in Playback Mode
|
||||
|
||||
```javascript
|
||||
try {
|
||||
const response = await fetch('https://api.example.com/nonexistent')
|
||||
} catch (error) {
|
||||
if (error.message.includes('No snapshot found')) {
|
||||
// Handle missing snapshot
|
||||
console.log('Snapshot not found for this request')
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Handling Network Errors in Record Mode
|
||||
|
||||
```javascript
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
|
||||
|
||||
try {
|
||||
const response = await fetch('https://nonexistent-api.example.com/data')
|
||||
} catch (error) {
|
||||
// Network errors are not recorded as snapshots
|
||||
console.log('Network error:', error.message)
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. Organize Snapshots by Test Suite
|
||||
|
||||
```javascript
|
||||
// Use descriptive snapshot file names
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: `./test/snapshots/${testSuiteName}-${testName}.json`
|
||||
})
|
||||
```
|
||||
|
||||
### 2. Version Control Snapshots
|
||||
|
||||
Add snapshot files to version control to ensure consistent test behavior across environments:
|
||||
|
||||
```gitignore
|
||||
# Include snapshots in version control
|
||||
!/test/snapshots/*.json
|
||||
```
|
||||
|
||||
### 3. Clean Up Test Data
|
||||
|
||||
```javascript
|
||||
test('API test', async (t) => {
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/temp-test.json'
|
||||
})
|
||||
|
||||
// Clean up after test
|
||||
t.after(() => {
|
||||
agent.clearSnapshots()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### 4. Snapshot Validation
|
||||
|
||||
```javascript
|
||||
test('validate snapshot contents', async (t) => {
|
||||
const agent = new SnapshotAgent({
|
||||
mode: 'playback',
|
||||
snapshotPath: './test/snapshots/validation.json'
|
||||
})
|
||||
|
||||
const recorder = agent.getRecorder()
|
||||
const snapshots = recorder.getSnapshots()
|
||||
|
||||
// Validate snapshot structure
|
||||
assert(snapshots.length > 0, 'Should have recorded snapshots')
|
||||
assert(snapshots[0].request.url.startsWith('https://'), 'Should use HTTPS')
|
||||
})
|
||||
```
|
||||
|
||||
## Comparison with Other Tools
|
||||
|
||||
### vs Manual MockAgent Setup
|
||||
|
||||
**Manual MockAgent:**
|
||||
```javascript
|
||||
const mockAgent = new MockAgent()
|
||||
const mockPool = mockAgent.get('https://api.example.com')
|
||||
|
||||
mockPool.intercept({
|
||||
path: '/users',
|
||||
method: 'GET'
|
||||
}).reply(200, [
|
||||
{ id: 1, name: 'User 1' },
|
||||
{ id: 2, name: 'User 2' }
|
||||
])
|
||||
```
|
||||
|
||||
**SnapshotAgent:**
|
||||
```javascript
|
||||
// Record once
|
||||
const agent = new SnapshotAgent({ mode: 'record', snapshotPath: './snapshots.json' })
|
||||
// Real API call gets recorded automatically
|
||||
|
||||
// Use in tests
|
||||
const agent = new SnapshotAgent({ mode: 'playback', snapshotPath: './snapshots.json' })
|
||||
// Automatically replays recorded response
|
||||
```
|
||||
|
||||
### vs nock
|
||||
|
||||
SnapshotAgent provides similar functionality to nock but is specifically designed for undici:
|
||||
|
||||
- ✅ Works with all undici APIs (`request`, `stream`, `pipeline`, etc.)
|
||||
- ✅ Supports undici-specific features (RetryAgent, connection pooling)
|
||||
- ✅ Better TypeScript integration
|
||||
- ✅ More efficient for high-performance scenarios
|
||||
|
||||
## See Also
|
||||
|
||||
- [MockAgent](/docs/docs/api/MockAgent.md) - Manual mocking for more control
|
||||
- [MockCallHistory](/docs/docs/api/MockCallHistory.md) - Inspecting request history
|
||||
- [Testing Best Practices](/docs/docs/best-practices/writing-tests.md) - General testing guidance
|
||||
[`Agent`]: Agent.md#class-agent
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`InvalidArgumentError`]: Errors.md#undicierrorsundiciinvalidargumenterror
|
||||
[`MockAgent`]: MockAgent.md#class-mockagent
|
||||
[`UndiciError`]: Errors.md#undicierrorsundicierror
|
||||
[`agent.getRecorder()`]: #agentgetrecorder
|
||||
[`setGlobalDispatcher()`]: Dispatcher.md#setglobaldispatcherdispatcher
|
||||
+198
-223
@@ -1,275 +1,250 @@
|
||||
# Class: Socks5ProxyAgent
|
||||
# Socks5ProxyAgent
|
||||
|
||||
Extends: `undici.Dispatcher`
|
||||
<!--introduced_in=v7.23.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/dispatcher/socks5-proxy-agent.js -->
|
||||
|
||||
A SOCKS5 proxy wrapper class that implements the Dispatcher API. It enables HTTP requests to be routed through a SOCKS5 proxy server, providing connection tunneling and authentication support.
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
## `new Socks5ProxyAgent(proxyUrl[, options])`
|
||||
A [`Dispatcher`][] implementation that routes requests through a [SOCKS5][]
|
||||
proxy server. It tunnels both HTTP and HTTPS traffic over the proxy, supports
|
||||
optional username/password authentication, and pools connections per origin so
|
||||
that subsequent requests to the same host reuse the established tunnel.
|
||||
|
||||
Arguments:
|
||||
DNS resolution is delegated to the proxy: target host names are sent to the
|
||||
proxy as domain names rather than being resolved locally. For HTTPS targets the
|
||||
TLS session is negotiated end-to-end through the tunnel, so the proxy only sees
|
||||
encrypted bytes.
|
||||
|
||||
* **proxyUrl** `string | URL` (required) - The SOCKS5 proxy server URL. Must use `socks5://` or `socks://` protocol.
|
||||
* **options** `Socks5ProxyAgent.Options` (optional) - Additional configuration options.
|
||||
Import it from `'undici'`:
|
||||
|
||||
Returns: `Socks5ProxyAgent`
|
||||
```mjs
|
||||
import { Socks5ProxyAgent } from 'undici'
|
||||
```
|
||||
|
||||
### Parameter: `Socks5ProxyAgent.Options`
|
||||
Constructing the agent emits a one-time `ExperimentalWarning`, reflecting the
|
||||
experimental status of SOCKS5 support.
|
||||
|
||||
Extends: [`PoolOptions`](/docs/docs/api/Pool.md#parameter-pooloptions)
|
||||
## Class: `Socks5ProxyAgent`
|
||||
|
||||
* **headers** `IncomingHttpHeaders` (optional) - Additional headers to send with proxy connections.
|
||||
* **username** `string` (optional) - SOCKS5 proxy username for authentication. Can also be provided in the proxy URL.
|
||||
* **password** `string` (optional) - SOCKS5 proxy password for authentication. Can also be provided in the proxy URL.
|
||||
* **connect** `Function` (optional) - Custom connector function for the proxy connection.
|
||||
* **proxyTls** `BuildOptions` (optional) - TLS options for the proxy connection (when using SOCKS5 over TLS).
|
||||
* **requestTls** `BuildOptions` (optional) - TLS options applied to the HTTPS connection to the target server through the SOCKS5 tunnel. Use this to configure `ca`, `cert`, `key`, `rejectUnauthorized`, `servername`, etc. for the target HTTPS endpoint.
|
||||
<!-- YAML
|
||||
added: v7.23.0
|
||||
-->
|
||||
|
||||
Examples:
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
```js
|
||||
* Extends: {Dispatcher}
|
||||
|
||||
Routes dispatched requests through a SOCKS5 proxy. Register an instance as the
|
||||
global dispatcher with [`setGlobalDispatcher()`][], or pass it per request via
|
||||
the `dispatcher` option.
|
||||
|
||||
### `new Socks5ProxyAgent(proxyUrl[, options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.23.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `proxyUrl` {string|URL} The SOCKS5 proxy server URL. Must use the `socks5:` or
|
||||
`socks:` protocol. Credentials may be embedded in the URL userinfo (for
|
||||
example `socks5://user:pass@host:1080`).
|
||||
* `options` {Object} (optional) Extends {PoolOptions}; the pool options are
|
||||
applied to the per-origin pools created behind the tunnel.
|
||||
* `headers` {Object} Additional headers to send with the proxy connection.
|
||||
**Default:** `{}`.
|
||||
* `username` {string} Username for SOCKS5 authentication. Takes precedence over
|
||||
a username embedded in `proxyUrl`. **Default:** the URL username, if any.
|
||||
* `password` {string} Password for SOCKS5 authentication. Takes precedence over
|
||||
a password embedded in `proxyUrl`. **Default:** the URL password, if any.
|
||||
* `connect` {Function} Custom connector used to open the socket to the proxy.
|
||||
**Default:** a connector built from `proxyTls`.
|
||||
* `proxyTls` {BuildOptions} TLS options for the connection to the proxy itself
|
||||
(SOCKS5 over TLS). When set, the proxy connection is established over TLS and
|
||||
`servername` defaults to the proxy host name.
|
||||
* `requestTls` {BuildOptions} TLS options applied to the end-to-end connection
|
||||
to an HTTPS target through the tunnel, such as `ca`, `cert`, `key`,
|
||||
`rejectUnauthorized`, and `servername`. `servername` defaults to the target
|
||||
host name.
|
||||
|
||||
Throws an `InvalidArgumentError` if `proxyUrl` is missing or does not use the
|
||||
`socks5:` or `socks:` protocol.
|
||||
|
||||
```mjs
|
||||
import { Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
// or with authentication
|
||||
const socks5ProxyWithAuth = new Socks5ProxyAgent('socks5://user:pass@localhost:1080')
|
||||
// or with options
|
||||
const socks5ProxyWithOptions = new Socks5ProxyAgent('socks5://localhost:1080', {
|
||||
// Without authentication.
|
||||
const agent = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
|
||||
// With credentials in the URL.
|
||||
const authAgent = new Socks5ProxyAgent('socks5://user:pass@localhost:1080')
|
||||
|
||||
// With credentials and pool options.
|
||||
const configuredAgent = new Socks5ProxyAgent('socks5://localhost:1080', {
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
connections: 10
|
||||
})
|
||||
```
|
||||
|
||||
#### Example - Basic SOCKS5 Proxy instantiation
|
||||
Using the agent as the global dispatcher:
|
||||
|
||||
This will instantiate the Socks5ProxyAgent. It will not do anything until registered as the dispatcher to use with requests.
|
||||
|
||||
```js
|
||||
import { Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
```
|
||||
|
||||
#### Example - Basic SOCKS5 Proxy Request with global dispatcher
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { setGlobalDispatcher, request, Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
setGlobalDispatcher(socks5Proxy)
|
||||
const agent = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
console.log('response received', statusCode)
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
console.log('data', data.toString('utf8'))
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - Basic SOCKS5 Proxy Request with local dispatcher
|
||||
Using the agent per request:
|
||||
|
||||
```js
|
||||
import { Socks5ProxyAgent, request } from 'undici'
|
||||
```mjs
|
||||
import { request, Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
const agent = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo', { dispatcher: socks5Proxy })
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - SOCKS5 Proxy Request with authentication
|
||||
|
||||
```js
|
||||
import { setGlobalDispatcher, request, Socks5ProxyAgent } from 'undici'
|
||||
|
||||
// Authentication via URL
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://username:password@localhost:1080')
|
||||
|
||||
// Or authentication via options
|
||||
// const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080', {
|
||||
// username: 'username',
|
||||
// password: 'password'
|
||||
// })
|
||||
|
||||
setGlobalDispatcher(socks5Proxy)
|
||||
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode) // response received 200
|
||||
|
||||
for await (const data of body) {
|
||||
console.log('data', data.toString('utf8')) // data foo
|
||||
}
|
||||
```
|
||||
|
||||
#### Example - SOCKS5 Proxy with HTTPS requests
|
||||
|
||||
SOCKS5 proxy supports both HTTP and HTTPS requests through tunneling:
|
||||
|
||||
```js
|
||||
import { Socks5ProxyAgent, request } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
|
||||
const response = await request('https://api.example.com/data', {
|
||||
dispatcher: socks5Proxy,
|
||||
method: 'GET'
|
||||
const { statusCode, body } = await request('http://localhost:3000/foo', {
|
||||
dispatcher: agent
|
||||
})
|
||||
|
||||
console.log('Response status:', response.statusCode)
|
||||
console.log('Response data:', await response.body.json())
|
||||
console.log('response received', statusCode)
|
||||
```
|
||||
|
||||
#### Example - SOCKS5 Proxy with Fetch
|
||||
HTTPS targets are tunnelled and encrypted end-to-end:
|
||||
|
||||
```js
|
||||
import { Socks5ProxyAgent, fetch } from 'undici'
|
||||
```mjs
|
||||
import { request, Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
const agent = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
|
||||
const { statusCode, body } = await request('https://api.example.com/data', {
|
||||
dispatcher: agent
|
||||
})
|
||||
|
||||
console.log('Response status:', statusCode)
|
||||
console.log('Response data:', await body.json())
|
||||
```
|
||||
|
||||
### `socks5ProxyAgent.close()`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.23.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Returns: {Promise<void>} Resolves once every per-origin pool has been closed.
|
||||
|
||||
Gracefully closes the agent, waiting for all underlying pools and their
|
||||
connections to drain and close before resolving.
|
||||
|
||||
```mjs
|
||||
import { setGlobalDispatcher, Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const agent = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
setGlobalDispatcher(agent)
|
||||
|
||||
// ... make requests ...
|
||||
|
||||
await agent.close()
|
||||
```
|
||||
|
||||
### `socks5ProxyAgent.destroy([err])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.23.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `err` {Error} (optional) The error to reject in-flight requests with.
|
||||
* Returns: {Promise<void>} Resolves once every per-origin pool has been
|
||||
destroyed.
|
||||
|
||||
Forcibly destroys the agent and all underlying connections immediately, without
|
||||
waiting for in-flight requests to complete.
|
||||
|
||||
```mjs
|
||||
import { Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const agent = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
|
||||
await agent.destroy()
|
||||
```
|
||||
|
||||
### `socks5ProxyAgent.dispatch(options, handlers)`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.23.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `options` {DispatchOptions}
|
||||
* `handlers` {DispatchHandler}
|
||||
* Returns: {boolean} `false` if the dispatcher is busy and the request should be
|
||||
retried later, otherwise `true`.
|
||||
|
||||
Routes a request through the SOCKS5 tunnel. The agent maintains a [`Pool`][] per
|
||||
target origin; the first request to a given origin establishes the tunnel and
|
||||
upgrades to TLS when the target is HTTPS, and later requests reuse the pool.
|
||||
|
||||
This is the lower-level entry point implementing
|
||||
[`Dispatcher.dispatch(options, handlers)`][]. Prefer
|
||||
[`socks5ProxyAgent.request()`][] for most use cases.
|
||||
|
||||
### `socks5ProxyAgent.request(options[, callback])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.23.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `options` {DispatchOptions}
|
||||
* `callback` {Function} (optional)
|
||||
* Returns: {Promise|void} A promise resolving to the response when `callback` is
|
||||
omitted.
|
||||
|
||||
Performs a request through the SOCKS5 proxy. Inherited from
|
||||
[`Dispatcher.request(options[, callback])`][]; see that method for the full set
|
||||
of options and the shape of the resolved response.
|
||||
|
||||
```mjs
|
||||
import { fetch, Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const agent = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
|
||||
const response = await fetch('http://localhost:3000/api/users', {
|
||||
dispatcher: socks5Proxy,
|
||||
method: 'GET'
|
||||
dispatcher: agent
|
||||
})
|
||||
|
||||
console.log('Response status:', response.status)
|
||||
console.log('Response data:', await response.text())
|
||||
```
|
||||
|
||||
#### Example - Connection Pooling
|
||||
|
||||
SOCKS5ProxyWrapper automatically manages connection pooling for better performance:
|
||||
|
||||
```js
|
||||
import { Socks5ProxyAgent, request } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080', {
|
||||
connections: 10, // Allow up to 10 concurrent connections
|
||||
pipelining: 1 // Enable HTTP/1.1 pipelining
|
||||
})
|
||||
|
||||
// Multiple requests will reuse connections through the SOCKS5 tunnel
|
||||
const responses = await Promise.all([
|
||||
request('http://api.example.com/endpoint1', { dispatcher: socks5Proxy }),
|
||||
request('http://api.example.com/endpoint2', { dispatcher: socks5Proxy }),
|
||||
request('http://api.example.com/endpoint3', { dispatcher: socks5Proxy })
|
||||
])
|
||||
|
||||
console.log('All requests completed through the same SOCKS5 proxy')
|
||||
```
|
||||
|
||||
### `Socks5ProxyAgent.close()`
|
||||
|
||||
Closes the SOCKS5 proxy wrapper and waits for all underlying pools and connections to close before resolving.
|
||||
|
||||
Returns: `Promise<void>`
|
||||
|
||||
#### Example - clean up after tests are complete
|
||||
|
||||
```js
|
||||
import { Socks5ProxyAgent, setGlobalDispatcher } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
setGlobalDispatcher(socks5Proxy)
|
||||
|
||||
// ... make requests
|
||||
|
||||
await socks5Proxy.close()
|
||||
```
|
||||
|
||||
### `Socks5ProxyAgent.destroy([err])`
|
||||
|
||||
Destroys the SOCKS5 proxy wrapper and all underlying connections immediately.
|
||||
|
||||
Arguments:
|
||||
* **err** `Error` (optional) - The error that caused the destruction.
|
||||
|
||||
Returns: `Promise<void>`
|
||||
|
||||
#### Example - force close all connections
|
||||
|
||||
```js
|
||||
import { Socks5ProxyAgent } from 'undici'
|
||||
|
||||
const socks5Proxy = new Socks5ProxyAgent('socks5://localhost:1080')
|
||||
|
||||
// Force close all connections
|
||||
await socks5Proxy.destroy()
|
||||
```
|
||||
|
||||
### `Socks5ProxyAgent.dispatch(options, handlers)`
|
||||
|
||||
Implements [`Dispatcher.dispatch(options, handlers)`](/docs/docs/api/Dispatcher.md#dispatcherdispatchoptions-handlers).
|
||||
|
||||
### `Socks5ProxyAgent.request(options[, callback])`
|
||||
|
||||
See [`Dispatcher.request(options [, callback])`](/docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback).
|
||||
|
||||
## Debugging
|
||||
|
||||
SOCKS5 proxy connections can be debugged using Node.js diagnostics:
|
||||
SOCKS5 proxy activity can be traced with the `undici:socks5-proxy` debug scope:
|
||||
|
||||
```sh
|
||||
NODE_DEBUG=undici:socks5 node script.js
|
||||
```bash
|
||||
NODE_DEBUG=undici:socks5-proxy node script.js
|
||||
```
|
||||
|
||||
This will output detailed information about the SOCKS5 handshake, authentication, and connection establishment.
|
||||
This logs the SOCKS5 handshake, authentication, and tunnel establishment steps.
|
||||
|
||||
## SOCKS5 Protocol Support
|
||||
|
||||
The Socks5ProxyAgent supports the following SOCKS5 features:
|
||||
|
||||
### Authentication Methods
|
||||
|
||||
- **No Authentication** (`0x00`) - For public or internal proxies
|
||||
- **Username/Password** (`0x02`) - RFC 1929 authentication
|
||||
|
||||
### Address Types
|
||||
|
||||
- **IPv4** (`0x01`) - Standard IPv4 addresses
|
||||
- **Domain Name** (`0x03`) - Domain names (recommended for flexibility)
|
||||
- **IPv6** (`0x04`) - IPv6 addresses (full support for standard and compressed notation)
|
||||
|
||||
### Commands
|
||||
|
||||
- **CONNECT** (`0x01`) - Establish TCP connection (primary use case for HTTP)
|
||||
|
||||
### Error Handling
|
||||
|
||||
The wrapper handles various SOCKS5 error conditions:
|
||||
|
||||
- Connection refused by proxy
|
||||
- Authentication failures
|
||||
- Network unreachable
|
||||
- Host unreachable
|
||||
- Unsupported address types or commands
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
- **Connection Pooling**: Automatically pools connections through the SOCKS5 tunnel for better performance
|
||||
- **HTTP/1.1 Pipelining**: Supports pipelining when enabled
|
||||
- **DNS Resolution**: Domain names are resolved by the SOCKS5 proxy, reducing local DNS queries
|
||||
- **TLS Termination**: HTTPS connections are encrypted end-to-end, with the SOCKS5 proxy only handling the TCP tunnel
|
||||
|
||||
## Security Notes
|
||||
|
||||
1. **Authentication**: Credentials are sent to the SOCKS5 proxy in plaintext unless using SOCKS5 over TLS
|
||||
2. **DNS Leaks**: All DNS resolution happens on the proxy server, preventing DNS leaks
|
||||
3. **End-to-end Encryption**: HTTPS traffic remains encrypted between client and final destination
|
||||
4. **Connection Security**: Consider using authenticated proxies and secure networks
|
||||
|
||||
## Compatibility
|
||||
|
||||
- **Protocol**: SOCKS5 (RFC 1928) with Username/Password Authentication (RFC 1929)
|
||||
- **Transport**: TCP only (UDP support not implemented)
|
||||
- **Node.js**: Compatible with all supported Node.js versions
|
||||
- **HTTP Versions**: Works with HTTP/1.1 and HTTP/2 over the tunnel
|
||||
[SOCKS5]: https://www.rfc-editor.org/rfc/rfc1928
|
||||
[`Dispatcher.dispatch(options, handlers)`]: Dispatcher.md#dispatcherdispatchoptions-handler
|
||||
[`Dispatcher.request(options[, callback])`]: Dispatcher.md#dispatcherrequestoptions-callback
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`Pool`]: Pool.md#class-pool
|
||||
[`setGlobalDispatcher()`]: Dispatcher.md#setglobaldispatcherdispatcher
|
||||
[`socks5ProxyAgent.request()`]: #socks5proxyagentrequestoptions-callback
|
||||
+68
-11
@@ -1,25 +1,82 @@
|
||||
# Util
|
||||
|
||||
Utility API for third-party implementations of the dispatcher API.
|
||||
<!--introduced_in=v1.2.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/core/util.js -->
|
||||
|
||||
## `parseHeaders(headers, [obj])`
|
||||
> Stability: 2 - Stable
|
||||
|
||||
Receives a header object and returns the parsed value.
|
||||
A small set of utility helpers exposed for third-party implementations of the
|
||||
[`Dispatcher`][] API. They cover the header-handling primitives undici uses
|
||||
internally, so that custom dispatchers and handlers can normalize header names
|
||||
and parse raw header lists exactly the way undici does.
|
||||
|
||||
Arguments:
|
||||
```mjs
|
||||
import { util } from 'undici'
|
||||
|
||||
- **headers** `(Buffer | string | (Buffer | string)[])[]` (required) - Header object.
|
||||
const { parseHeaders, headerNameToString } = util
|
||||
```
|
||||
|
||||
- **obj** `Record<string, string | string[]>` (optional) - Object to specify a proxy object. The parsed value is assigned to this object. But, if **headers** is an object, it is not used.
|
||||
```cjs
|
||||
const { util } = require('undici')
|
||||
|
||||
Returns: `Record<string, string | string[]>` If **obj** is specified, it is equivalent to **obj**.
|
||||
const { parseHeaders, headerNameToString } = util
|
||||
```
|
||||
|
||||
## `parseHeaders(headers[, obj])`
|
||||
|
||||
<!-- YAML
|
||||
added: v1.2.0
|
||||
changes:
|
||||
- version: v6.1.0
|
||||
pr-url: https://github.com/nodejs/undici/pull/2501
|
||||
description: Header names supplied as a `Buffer` are accepted and normalized.
|
||||
-->
|
||||
|
||||
* `headers` {Array} A flat list of raw header entries where even indices are
|
||||
header names and odd indices are the corresponding values. Each entry is a
|
||||
{string}, {Buffer}, or an array of {string}/{Buffer}.
|
||||
* `obj` {Record<string, string|string[]>} An object to assign the parsed values
|
||||
to. When omitted, a new object is created. **Default:** `{}`.
|
||||
* Returns: {Record<string, string|string[]>} The object the parsed headers were
|
||||
assigned to. When `obj` is provided, the same reference is returned.
|
||||
|
||||
Receives a flat list of raw header name/value pairs and returns them as an
|
||||
object keyed by lowercased header name. Header names are normalized with
|
||||
[`headerNameToString()`][], and `Buffer` values are decoded as `latin1`. When a
|
||||
header name appears more than once, its values are collected into an array.
|
||||
|
||||
```mjs
|
||||
import { util } from 'undici'
|
||||
|
||||
const raw = ['Content-Type', 'text/plain', 'Set-Cookie', 'a=1', 'Set-Cookie', 'b=2']
|
||||
|
||||
console.log(util.parseHeaders(raw))
|
||||
// { 'content-type': 'text/plain', 'set-cookie': [ 'a=1', 'b=2' ] }
|
||||
```
|
||||
|
||||
## `headerNameToString(value)`
|
||||
|
||||
Retrieves a header name and returns its lowercase value.
|
||||
<!-- YAML
|
||||
added: v6.1.0
|
||||
-->
|
||||
|
||||
Arguments:
|
||||
* `value` {string|Buffer} The header name to normalize.
|
||||
* Returns: {string} The lowercased header name.
|
||||
|
||||
- **value** `string | Buffer` (required) - Header name.
|
||||
Returns the lowercased form of a header name. The `value` may be provided as a
|
||||
{string} or as a {Buffer}, in which case it is decoded as `latin1`. Common
|
||||
header names are resolved through an internal lookup table for performance.
|
||||
|
||||
Returns: `string`
|
||||
```mjs
|
||||
import { util } from 'undici'
|
||||
|
||||
console.log(util.headerNameToString('Content-Type'))
|
||||
// 'content-type'
|
||||
|
||||
console.log(util.headerNameToString(Buffer.from('X-Custom-Header')))
|
||||
// 'x-custom-header'
|
||||
```
|
||||
|
||||
[`Dispatcher`]: Dispatcher.md#class-dispatcher
|
||||
[`headerNameToString()`]: #headernametostringvalue
|
||||
+492
-97
@@ -1,141 +1,536 @@
|
||||
# Class: WebSocket
|
||||
# WebSocket
|
||||
|
||||
Extends: [`EventTarget`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget)
|
||||
<!--introduced_in=v5.15.0-->
|
||||
<!--type=module-->
|
||||
<!-- source_link=lib/web/websocket/websocket.js -->
|
||||
|
||||
The WebSocket object provides a way to manage a WebSocket connection to a server, allowing bidirectional communication. The API follows the [WebSocket spec](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) and [RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455).
|
||||
> Stability: 2 - Stable
|
||||
|
||||
## `new WebSocket(url[, protocol])`
|
||||
The `WebSocket` API provides a way to manage a WebSocket connection to a
|
||||
server, allowing bidirectional communication. It follows the
|
||||
[WHATWG WebSocket specification][] and [RFC 6455][], and is intended to be
|
||||
interchangeable with the global `WebSocket` available in browsers.
|
||||
|
||||
Arguments:
|
||||
```mjs
|
||||
import { WebSocket } from 'undici'
|
||||
|
||||
* **url** `URL | string`
|
||||
* **protocol** `string | string[] | WebSocketInit` (optional) - Subprotocol(s) to request the server use, or a [`Dispatcher`](/docs/docs/api/Dispatcher.md).
|
||||
const ws = new WebSocket('wss://echo.websocket.events')
|
||||
|
||||
### WebSocketInit
|
||||
ws.addEventListener('open', () => {
|
||||
ws.send('Hello, world!')
|
||||
})
|
||||
|
||||
When passing an object as the second argument, the following options are available:
|
||||
|
||||
* **protocols** `string | string[]` (optional) - Subprotocol(s) to request the server use.
|
||||
* **dispatcher** `Dispatcher` (optional) - A custom [`Dispatcher`](/docs/docs/api/Dispatcher.md) to use for the connection.
|
||||
* **headers** `HeadersInit` (optional) - Custom headers to include in the WebSocket handshake request.
|
||||
|
||||
### Example:
|
||||
|
||||
This example will not work in browsers or other platforms that don't allow passing an object.
|
||||
|
||||
```js
|
||||
import { WebSocket, ProxyAgent } from 'undici'
|
||||
|
||||
const proxyAgent = new ProxyAgent('my.proxy.server')
|
||||
|
||||
const ws = new WebSocket('wss://echo.websocket.events', {
|
||||
dispatcher: proxyAgent,
|
||||
protocols: ['echo', 'chat']
|
||||
ws.addEventListener('message', (event) => {
|
||||
console.log('received:', event.data)
|
||||
})
|
||||
```
|
||||
|
||||
If you do not need a custom Dispatcher, it's recommended to use the following pattern:
|
||||
In addition to the standard `WebSocket` interface, this module exports the
|
||||
streams-based [`WebSocketStream`][] and [`WebSocketError`][] APIs, as well as a
|
||||
[`ping()`][] helper for sending ping frames.
|
||||
|
||||
```js
|
||||
## Class: `WebSocket`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Extends: {EventTarget}
|
||||
|
||||
The `WebSocket` object represents a single WebSocket connection. Constructing a
|
||||
`WebSocket` immediately begins the opening handshake; the connection cannot be
|
||||
reused once it has been closed.
|
||||
|
||||
### `new WebSocket(url[, protocols])`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* `url` {URL|string} The URL of the server to connect to. Must use the `ws:`
|
||||
or `wss:` scheme.
|
||||
* `protocols` {string|string[]|WebSocketInit} A single subprotocol, a list of
|
||||
subprotocols to negotiate with the server, or a `WebSocketInit` object.
|
||||
**Default:** `[]`.
|
||||
* `protocols` {string|string[]} Subprotocol(s) to request the server use.
|
||||
**Default:** `[]`.
|
||||
* `dispatcher` {Dispatcher} The dispatcher used to open the connection.
|
||||
**Default:** the global dispatcher.
|
||||
* `headers` {HeadersInit|null} Additional headers to send with the handshake
|
||||
request. (optional)
|
||||
|
||||
Creates a new `WebSocket` and begins connecting to `url`. The second argument
|
||||
may be a string or array of subprotocols, or a `WebSocketInit` object. The
|
||||
object form is an undici extension and is not available in browsers; it allows a
|
||||
custom `dispatcher` and additional `headers` to be supplied.
|
||||
|
||||
If a value in `protocols` is repeated or is not a valid subprotocol name, the
|
||||
constructor throws a `SyntaxError` `DOMException`.
|
||||
|
||||
```mjs
|
||||
import { WebSocket } from 'undici'
|
||||
|
||||
const ws = new WebSocket('wss://echo.websocket.events', ['echo', 'chat'])
|
||||
```
|
||||
|
||||
### Example with HTTP/2:
|
||||
To route the connection through a custom dispatcher, such as a
|
||||
[`ProxyAgent`][], pass a `WebSocketInit` object:
|
||||
|
||||
> ⚠️ Warning: WebSocket over HTTP/2 is experimental, it is likely to change in the future.
|
||||
```mjs
|
||||
import { WebSocket, ProxyAgent } from 'undici'
|
||||
|
||||
> 🗒️ Note: WebSocket over HTTP/2 may be enabled by default in a future version,
|
||||
> this will happen by enabling HTTP/2 connections as the default behavior of Undici's Agent as well the global dispatcher.
|
||||
> Stay tuned to the changelog for more information.
|
||||
|
||||
This example will not work in browsers or other platforms that don't allow passing an object.
|
||||
|
||||
```js
|
||||
import { Agent } from 'undici'
|
||||
|
||||
const agent = new Agent({ allowH2: true })
|
||||
const dispatcher = new ProxyAgent('http://localhost:8080')
|
||||
|
||||
const ws = new WebSocket('wss://echo.websocket.events', {
|
||||
dispatcher: agent,
|
||||
dispatcher,
|
||||
protocols: ['echo', 'chat']
|
||||
})
|
||||
```
|
||||
|
||||
# Class: WebSocketStream
|
||||
WebSocket over HTTP/2 is experimental and may change in the future. It can be
|
||||
enabled by passing a dispatcher configured with `allowH2`:
|
||||
|
||||
> ⚠️ Warning: the WebSocketStream API has not been finalized and is likely to change.
|
||||
```mjs
|
||||
import { WebSocket, Agent } from 'undici'
|
||||
|
||||
See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocketStream) for more information.
|
||||
|
||||
## `new WebSocketStream(url[, protocol])`
|
||||
|
||||
Arguments:
|
||||
|
||||
* **url** `URL | string`
|
||||
* **options** `WebSocketStreamOptions` (optional)
|
||||
|
||||
### WebSocketStream Example
|
||||
|
||||
```js
|
||||
const stream = new WebSocketStream('https://echo.websocket.org/')
|
||||
const { readable, writable } = await stream.opened
|
||||
|
||||
async function read () {
|
||||
/** @type {ReadableStreamReader} */
|
||||
const reader = readable.getReader()
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
// do something with value
|
||||
}
|
||||
}
|
||||
|
||||
async function write () {
|
||||
/** @type {WritableStreamDefaultWriter} */
|
||||
const writer = writable.getWriter()
|
||||
writer.write('Hello, world!')
|
||||
writer.releaseLock()
|
||||
}
|
||||
|
||||
read()
|
||||
|
||||
setInterval(() => write(), 5000)
|
||||
const dispatcher = new Agent({ allowH2: true })
|
||||
|
||||
const ws = new WebSocket('wss://echo.websocket.events', {
|
||||
dispatcher,
|
||||
protocols: ['echo', 'chat']
|
||||
})
|
||||
```
|
||||
|
||||
## ping(websocket, payload)
|
||||
Arguments:
|
||||
### `websocket.close([code[, reason]])`
|
||||
|
||||
* **websocket** `WebSocket` - The WebSocket instance to send the ping frame on
|
||||
* **payload** `Buffer|undefined` (optional) - Optional payload data to include with the ping frame. Must not exceed 125 bytes.
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
Sends a ping frame to the WebSocket server. The server must respond with a pong frame containing the same payload data. This can be used for keepalive purposes or to verify that the connection is still active.
|
||||
* `code` {number} The status code explaining why the connection is being
|
||||
closed. **Default:** `null`.
|
||||
* `reason` {string} A human-readable string explaining why the connection is
|
||||
being closed. **Default:** `''`.
|
||||
|
||||
### Example:
|
||||
Starts the closing handshake. The `code` is clamped to an unsigned 16-bit
|
||||
integer; when omitted, no status code is sent. The `reason` is encoded as
|
||||
UTF-8 and must not exceed 123 bytes.
|
||||
|
||||
```js
|
||||
```mjs
|
||||
ws.close(1000, 'Done')
|
||||
```
|
||||
|
||||
### `websocket.send(data)`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* `data` {string|ArrayBuffer|TypedArray|DataView|Blob} The data to enqueue for
|
||||
transmission.
|
||||
|
||||
Enqueues `data` to be transmitted to the server. String data is sent as a text
|
||||
frame; `ArrayBuffer`, any `TypedArray`, `DataView`, and `Blob` values are sent
|
||||
as binary frames. The number of bytes queued is added to
|
||||
[`websocket.bufferedAmount`][].
|
||||
|
||||
If [`websocket.readyState`][] is `CONNECTING`, an `InvalidStateError`
|
||||
`DOMException` is thrown. If the connection is closing or already closed, the
|
||||
call is silently ignored.
|
||||
|
||||
```mjs
|
||||
ws.send('a text message')
|
||||
ws.send(new Uint8Array([1, 2, 3]))
|
||||
```
|
||||
|
||||
### `websocket.readyState`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
The current state of the connection. One of the numeric constants
|
||||
`WebSocket.CONNECTING` (`0`), `WebSocket.OPEN` (`1`),
|
||||
`WebSocket.CLOSING` (`2`), or `WebSocket.CLOSED` (`3`). This property is
|
||||
read-only.
|
||||
|
||||
### `websocket.bufferedAmount`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {number}
|
||||
|
||||
The number of bytes of data that have been queued using
|
||||
[`websocket.send()`][] but not yet transmitted to the network. This value
|
||||
resets to `0` once all queued data is sent. This property is read-only.
|
||||
|
||||
### `websocket.url`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The absolute URL of the WebSocket as resolved during construction. This
|
||||
property is read-only.
|
||||
|
||||
### `websocket.extensions`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The extensions negotiated by the server, taken from the
|
||||
`Sec-WebSocket-Extensions` response header. Empty until the connection is
|
||||
established. This property is read-only.
|
||||
|
||||
### `websocket.protocol`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The subprotocol selected by the server, taken from the
|
||||
`Sec-WebSocket-Protocol` response header. Empty until the connection is
|
||||
established and only set when a subprotocol was negotiated. This property is
|
||||
read-only.
|
||||
|
||||
### `websocket.binaryType`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {string} **Default:** `'blob'`.
|
||||
|
||||
Controls the type used to represent binary data delivered by the
|
||||
[`'message'`][] event. Must be either `'blob'` (binary frames are exposed as a
|
||||
`Blob`) or `'arraybuffer'` (binary frames are exposed as an `ArrayBuffer`).
|
||||
Assigning any other value resets it to `'blob'`.
|
||||
|
||||
### `websocket.onopen`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {Function|null} **Default:** `null`.
|
||||
|
||||
An event handler for the [`'open'`][] event. Setting this property replaces any
|
||||
previously registered `onopen` handler.
|
||||
|
||||
### `websocket.onerror`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {Function|null} **Default:** `null`.
|
||||
|
||||
An event handler for the [`'error'`][] event. Setting this property replaces any
|
||||
previously registered `onerror` handler.
|
||||
|
||||
### `websocket.onclose`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {Function|null} **Default:** `null`.
|
||||
|
||||
An event handler for the [`'close'`][] event. Setting this property replaces any
|
||||
previously registered `onclose` handler.
|
||||
|
||||
### `websocket.onmessage`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
* Type: {Function|null} **Default:** `null`.
|
||||
|
||||
An event handler for the [`'message'`][] event. Setting this property replaces
|
||||
any previously registered `onmessage` handler.
|
||||
|
||||
### Event: `'open'`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.21.0
|
||||
-->
|
||||
|
||||
Emitted when the WebSocket connection is established. After this event,
|
||||
[`websocket.readyState`][] is `OPEN` and data may be sent with
|
||||
[`websocket.send()`][]. The listener receives an `Event`.
|
||||
|
||||
### Event: `'message'`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
Emitted when a message is received from the server. The listener receives a
|
||||
`MessageEvent` whose `data` property is a `string` for text frames, and either a
|
||||
`Blob` or an `ArrayBuffer` for binary frames depending on
|
||||
[`websocket.binaryType`][]. The event's `origin` is the origin of
|
||||
[`websocket.url`][].
|
||||
|
||||
### Event: `'error'`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
Emitted when the connection is terminated abnormally, for example when no close
|
||||
frame was received before the underlying transport was lost. The listener
|
||||
receives an `ErrorEvent`.
|
||||
|
||||
### Event: `'close'`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
Emitted when the connection is closed. The listener receives a `CloseEvent`
|
||||
whose `code` and `reason` describe why the connection closed, and whose
|
||||
`wasClean` property is `true` only when the closing handshake completed in both
|
||||
directions.
|
||||
|
||||
## `ping(websocket[, payload])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.12.0
|
||||
-->
|
||||
|
||||
* `websocket` {WebSocket} The WebSocket to send the ping frame on.
|
||||
* `payload` {Buffer} Optional payload to include in the ping frame. Must not
|
||||
exceed 125 bytes. **Default:** `undefined`.
|
||||
|
||||
Sends a ping control frame to the server. A conforming server replies with a
|
||||
pong frame containing the same payload, which can be used to keep the connection
|
||||
alive or to verify that it is still active.
|
||||
|
||||
The frame is only sent when the connection is open; if it is connecting,
|
||||
closing, or closed, the call is a no-op. A `TypeError` is thrown if `payload`
|
||||
is not a `Buffer` or is larger than 125 bytes.
|
||||
|
||||
```mjs
|
||||
import { WebSocket, ping } from 'undici'
|
||||
|
||||
const ws = new WebSocket('wss://echo.websocket.events')
|
||||
|
||||
ws.addEventListener('open', () => {
|
||||
// Send ping with no payload
|
||||
ping(ws)
|
||||
|
||||
// Send ping with payload
|
||||
const payload = Buffer.from('hello')
|
||||
ping(ws, payload)
|
||||
ping(ws, Buffer.from('hello'))
|
||||
})
|
||||
```
|
||||
|
||||
**Note**: A ping frame cannot have a payload larger than 125 bytes. The ping will only be sent if the WebSocket connection is in the OPEN state.
|
||||
## Class: `WebSocketStream`
|
||||
|
||||
## Read More
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
- [MDN - WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
|
||||
- [The WebSocket Specification](https://www.rfc-editor.org/rfc/rfc6455)
|
||||
- [The WHATWG WebSocket Specification](https://websockets.spec.whatwg.org/)
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
`WebSocketStream` is a streams-based alternative to the event-based
|
||||
[`WebSocket`][] interface, exposing a connection as a pair of `ReadableStream`
|
||||
and `WritableStream` objects. See [MDN][MDN WebSocketStream] for background.
|
||||
|
||||
Constructing a `WebSocketStream` for the first time emits an
|
||||
`ExperimentalWarning` with the code `UNDICI-WSS`.
|
||||
|
||||
### `new WebSocketStream(url[, options])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `url` {URL|string} The URL of the server to connect to.
|
||||
* `options` {WebSocketStreamOptions}
|
||||
* `protocols` {string|string[]} Subprotocol(s) to request the server use.
|
||||
**Default:** `[]`.
|
||||
* `signal` {AbortSignal|null} An `AbortSignal` that aborts the opening
|
||||
handshake and rejects [`webSocketStream.opened`][] and
|
||||
[`webSocketStream.closed`][] when triggered. **Default:** `null`.
|
||||
|
||||
Creates a new `WebSocketStream` and begins connecting to `url`. As with
|
||||
[`new WebSocket()`][], a repeated or invalid subprotocol causes a `SyntaxError`
|
||||
`DOMException` to be thrown.
|
||||
|
||||
```mjs
|
||||
import { WebSocketStream } from 'undici'
|
||||
|
||||
const stream = new WebSocketStream('wss://echo.websocket.events')
|
||||
const { readable, writable } = await stream.opened
|
||||
|
||||
const writer = writable.getWriter()
|
||||
await writer.write('Hello, world!')
|
||||
writer.releaseLock()
|
||||
|
||||
const reader = readable.getReader()
|
||||
const { value } = await reader.read()
|
||||
console.log('received:', value)
|
||||
```
|
||||
|
||||
### `webSocketStream.url`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The serialized URL of the WebSocket. This property is read-only.
|
||||
|
||||
### `webSocketStream.opened`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {Promise} Fulfills once the connection is established.
|
||||
|
||||
A promise that resolves with an object describing the open connection, or
|
||||
rejects if the connection could not be established. The fulfillment value has
|
||||
the following shape:
|
||||
|
||||
* `extensions` {string} The negotiated extensions.
|
||||
* `protocol` {string} The negotiated subprotocol.
|
||||
* `readable` {ReadableStream} A stream of incoming messages. Text frames are
|
||||
delivered as strings and binary frames as `Uint8Array` chunks.
|
||||
* `writable` {WritableStream} A stream for sending messages. Writing a string
|
||||
sends a text frame; writing a `BufferSource` sends a binary frame.
|
||||
|
||||
### `webSocketStream.closed`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {Promise} Fulfills once the connection is closed cleanly.
|
||||
|
||||
A promise that resolves with a `WebSocketCloseInfo` object when the connection
|
||||
closes cleanly, or rejects with a [`WebSocketError`][] when it closes
|
||||
abnormally. The fulfillment value has the following shape:
|
||||
|
||||
* `closeCode` {number} The status code the connection was closed with.
|
||||
* `reason` {string} The reason the connection was closed with.
|
||||
|
||||
### `webSocketStream.close([closeInfo])`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `closeInfo` {Object}
|
||||
* `closeCode` {number} The status code to close with.
|
||||
* `reason` {string} The reason to close with. **Default:** `''`.
|
||||
|
||||
Starts the closing handshake. When `closeInfo` is omitted, the connection is
|
||||
closed without a status code.
|
||||
|
||||
## Class: `WebSocketError`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Extends: {DOMException}
|
||||
|
||||
An error type used by [`WebSocketStream`][] to report close information. It is
|
||||
used to reject [`webSocketStream.closed`][] when the connection closes
|
||||
abnormally, and may be passed to a stream's abort or cancel operations to close
|
||||
the connection with a specific code and reason.
|
||||
|
||||
### `new WebSocketError([message[, init]])`
|
||||
|
||||
<!-- YAML
|
||||
added: v7.0.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* `message` {string} A human-readable description of the error.
|
||||
**Default:** `''`.
|
||||
* `init` {Object}
|
||||
* `closeCode` {number} The status code to associate with the error.
|
||||
* `reason` {string} The reason to associate with the error.
|
||||
**Default:** `''`.
|
||||
|
||||
Creates a new `WebSocketError`. If `reason` is non-empty but no `closeCode` is
|
||||
given, `closeCode` defaults to `1000` ("Normal Closure"). The `closeCode` and
|
||||
`reason` are validated, so an out-of-range code or an over-long reason throws.
|
||||
|
||||
### `webSocketError.closeCode`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {number|null}
|
||||
|
||||
The status code associated with the error, or `null` if none was set. This
|
||||
property is read-only.
|
||||
|
||||
### `webSocketError.reason`
|
||||
|
||||
<!-- YAML
|
||||
added: v5.15.0
|
||||
-->
|
||||
|
||||
> Stability: 1 - Experimental
|
||||
|
||||
* Type: {string}
|
||||
|
||||
The reason associated with the error. This property is read-only.
|
||||
|
||||
## Read more
|
||||
|
||||
* [MDN: WebSocket][]
|
||||
* [RFC 6455][]
|
||||
* [WHATWG WebSocket specification][]
|
||||
|
||||
[MDN WebSocketStream]: https://developer.mozilla.org/docs/Web/API/WebSocketStream
|
||||
[MDN: WebSocket]: https://developer.mozilla.org/docs/Web/API/WebSocket
|
||||
[RFC 6455]: https://www.rfc-editor.org/rfc/rfc6455
|
||||
[WHATWG WebSocket specification]: https://websockets.spec.whatwg.org/
|
||||
[`'close'`]: #event-close
|
||||
[`'error'`]: #event-error
|
||||
[`'message'`]: #event-message
|
||||
[`'open'`]: #event-open
|
||||
[`ProxyAgent`]: ProxyAgent.md#class-proxyagent
|
||||
[`WebSocket`]: #class-websocket
|
||||
[`WebSocketError`]: #class-websocketerror
|
||||
[`WebSocketStream`]: #class-websocketstream
|
||||
[`new WebSocket()`]: #new-websocketurl-protocols
|
||||
[`ping()`]: #pingwebsocket-payload
|
||||
[`webSocketStream.closed`]: #websocketstreamclosed
|
||||
[`webSocketStream.opened`]: #websocketstreamopened
|
||||
[`websocket.bufferedAmount`]: #websocketbufferedamount
|
||||
[`websocket.binaryType`]: #websocketbinarytype
|
||||
[`websocket.readyState`]: #websocketreadystate
|
||||
[`websocket.send()`]: #websocketsenddata
|
||||
[`websocket.url`]: #websocketurl
|
||||
+96
-28
@@ -1,27 +1,49 @@
|
||||
# Client Lifecycle
|
||||
|
||||
An Undici [Client](/docs/docs/api/Client.md) can be best described as a state machine. The following list is a summary of the various state transitions the `Client` will go through in its lifecycle. This document also contains detailed breakdowns of each state.
|
||||
<!--type=misc-->
|
||||
|
||||
> This diagram is not a perfect representation of the undici Client. Since the Client class is not actually implemented as a state-machine, actual execution may deviate slightly from what is described below. Consider this as a general resource for understanding the inner workings of the Undici client rather than some kind of formal specification.
|
||||
A {Client} can be understood as a state machine. As requests are dispatched,
|
||||
processed, and drained, the client moves through a well-defined set of states
|
||||
until it is eventually destroyed. This document describes those states and the
|
||||
transitions between them.
|
||||
|
||||
## State Transition Overview
|
||||
The {Client} class is not literally implemented as a state machine, so the
|
||||
actual execution may deviate slightly from what is described here. Treat this
|
||||
guide as a conceptual model for reasoning about a client's behavior rather than
|
||||
as a formal specification.
|
||||
|
||||
* A `Client` begins in the **idle** state with no socket connection and no requests in queue.
|
||||
* The *connect* event transitions the `Client` to the **pending** state where requests can be queued prior to processing.
|
||||
* The *close* and *destroy* events transition the `Client` to the **destroyed** state. Since there are no requests in the queue, the *close* event immediately transitions to the **destroyed** state.
|
||||
* The **pending** state indicates the underlying socket connection has been successfully established and requests are queueing.
|
||||
* The *process* event transitions the `Client` to the **processing** state where requests are processed.
|
||||
* If requests are queued, the *close* event transitions to the **processing** state; otherwise, it transitions to the **destroyed** state.
|
||||
* The *destroy* event transitions to the **destroyed** state.
|
||||
* The **processing** state initializes to the **processing.running** state.
|
||||
* If the current request requires draining, the *needDrain* event transitions the `Client` into the **processing.busy** state which will return to the **processing.running** state with the *drainComplete* event.
|
||||
* After all queued requests are completed, the *keepalive* event transitions the `Client` back to the **pending** state. If no requests are queued during the timeout, the **close** event transitions the `Client` to the **destroyed** state.
|
||||
* If the *close* event is fired while the `Client` still has queued requests, the `Client` transitions to the **process.closing** state where it will complete all existing requests before firing the *done* event.
|
||||
* The *done* event gracefully transitions the `Client` to the **destroyed** state.
|
||||
* At any point in time, the *destroy* event will transition the `Client` from the **processing** state to the **destroyed** state, destroying any queued requests.
|
||||
* The **destroyed** state is a final state and the `Client` is no longer functional.
|
||||
## State transition overview
|
||||
|
||||
A state diagram representing an Undici Client instance:
|
||||
* A {Client} begins in the **idle** state with no socket connection and no
|
||||
queued requests.
|
||||
* The *connect* transition moves the {Client} to the **pending** state, where
|
||||
requests can be queued before they are processed.
|
||||
* Calling [`client.close()`][] or [`client.destroy()`][] moves the {Client}
|
||||
to the **destroyed** state. Because there are no queued requests in this
|
||||
state, *close* transitions straight to **destroyed**.
|
||||
* The **pending** state indicates that the underlying socket connection has been
|
||||
established and that requests are queueing.
|
||||
* The *process* transition moves the {Client} to the **processing** state,
|
||||
where requests are processed.
|
||||
* If requests are queued, *close* transitions to **processing**; otherwise it
|
||||
transitions to **destroyed**.
|
||||
* The *destroy* transition moves the {Client} to **destroyed**.
|
||||
* The **processing** state initializes to the **processing.running** sub-state.
|
||||
* If the current request body requires draining, *needDrain* moves the
|
||||
{Client} into the **processing.busy** sub-state, which returns to
|
||||
**processing.running** once *drainComplete* fires.
|
||||
* After all queued requests complete, *keepalive* moves the {Client} back to
|
||||
the **pending** state. If no requests are queued before the socket times
|
||||
out, the {Client} transitions to **idle**.
|
||||
* If *close* is fired while the {Client} still has queued requests, the
|
||||
{Client} transitions to **processing.closing**, where it completes all
|
||||
outstanding requests before firing *done*.
|
||||
* The *done* transition moves the {Client} gracefully to **destroyed**.
|
||||
* At any time, *destroy* moves the {Client} from **processing** to
|
||||
**destroyed**, aborting any queued requests.
|
||||
* The **destroyed** state is final; the {Client} is no longer functional.
|
||||
|
||||
A state diagram representing a {Client} instance:
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
@@ -54,38 +76,84 @@ stateDiagram-v2
|
||||
closing --> [*] : done
|
||||
}
|
||||
```
|
||||
|
||||
## State details
|
||||
|
||||
### idle
|
||||
|
||||
The **idle** state is the initial state of a `Client` instance. While an `origin` is required for instantiating a `Client` instance, the underlying socket connection will not be established until a request is queued using [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers). By calling `Client.dispatch()` directly or using one of the multiple implementations ([`Client.connect()`](/docs/docs/api/Client.md#clientconnectoptions-callback), [`Client.pipeline()`](/docs/docs/api/Client.md#clientpipelineoptions-handler), [`Client.request()`](/docs/docs/api/Client.md#clientrequestoptions-callback), [`Client.stream()`](/docs/docs/api/Client.md#clientstreamoptions-factory-callback), and [`Client.upgrade()`](/docs/docs/api/Client.md#clientupgradeoptions-callback)), the `Client` instance will transition from **idle** to [**pending**](/docs/docs/api/Client.md#pending) and then most likely directly to [**processing**](/docs/docs/api/Client.md#processing).
|
||||
**idle** is the initial state of a {Client} instance. Although an `origin` is
|
||||
required to construct a {Client}, the underlying socket connection is not
|
||||
established until a request is queued through [`client.dispatch()`][]. Calling
|
||||
`client.dispatch()` directly, or through one of its higher-level wrappers
|
||||
([`client.connect()`][], [`client.pipeline()`][], [`client.request()`][],
|
||||
[`client.stream()`][], or [`client.upgrade()`][]), moves the {Client} from
|
||||
**idle** to **pending**, and then usually directly on to **processing**.
|
||||
|
||||
Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) or [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state since the `Client` instance will have no queued requests in this state.
|
||||
Calling [`client.close()`][] or [`client.destroy()`][] in this state moves the
|
||||
{Client} straight to **destroyed**, since there are no queued requests.
|
||||
|
||||
### pending
|
||||
|
||||
The **pending** state signifies a non-processing `Client`. Upon entering this state, the `Client` establishes a socket connection and emits the [`'connect'`](/docs/docs/api/Client.md#event-connect) event signalling a connection was successfully established with the `origin` provided during `Client` instantiation. The internal queue is initially empty, and requests can start queueing.
|
||||
**pending** signifies a connected but non-processing {Client}. On entering this
|
||||
state, the {Client} establishes a socket connection and emits the
|
||||
[`'connect'`][] event, signalling that a connection to the `origin` supplied at
|
||||
construction time has been established. The internal queue starts empty, and
|
||||
requests can begin queueing.
|
||||
|
||||
Calling [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) with queued requests, transitions the `Client` to the [**processing**](/docs/docs/api/Client.md#processing) state. Without queued requests, it transitions to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state.
|
||||
Calling [`client.close()`][] with queued requests moves the {Client} to the
|
||||
**processing** state; without queued requests it moves to **destroyed**.
|
||||
|
||||
Calling [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) transitions directly to the [**destroyed**](/docs/docs/api/Client.md#destroyed) state regardless of existing requests.
|
||||
Calling [`client.destroy()`][] moves the {Client} directly to **destroyed**,
|
||||
regardless of any queued requests.
|
||||
|
||||
### processing
|
||||
|
||||
The **processing** state is a state machine within itself. It initializes to the [**processing.running**](/docs/docs/api/Client.md#running) state. The [`Client.dispatch()`](/docs/docs/api/Client.md#clientdispatchoptions-handlers), [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback), and [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) can be called at any time while the `Client` is in this state. `Client.dispatch()` will add more requests to the queue while existing requests continue to be processed. `Client.close()` will transition to the [**processing.closing**](/docs/docs/api/Client.md#closing) state. And `Client.destroy()` will transition to [**destroyed**](/docs/docs/api/Client.md#destroyed).
|
||||
**processing** is itself a sub-state machine that initializes to the
|
||||
**processing.running** sub-state. [`client.dispatch()`][], [`client.close()`][],
|
||||
and [`client.destroy()`][] can all be called while the {Client} is in this
|
||||
state. `client.dispatch()` queues additional requests while existing ones
|
||||
continue to be processed, `client.close()` moves the {Client} to the
|
||||
**processing.closing** sub-state, and `client.destroy()` moves it to
|
||||
**destroyed**.
|
||||
|
||||
#### running
|
||||
|
||||
In the **processing.running** sub-state, queued requests are being processed in a FIFO order. If a request body requires draining, the *needDrain* event transitions to the [**processing.busy**](/docs/docs/api/Client.md#busy) sub-state. The *close* event transitions the Client to the [**process.closing**](/docs/docs/api/Client.md#closing) sub-state. If all queued requests are processed and neither [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) nor [`Client.destroy()`](/docs/docs/api/Client.md#clientdestroyerror-callback) are called, then the [**processing**](/docs/docs/api/Client.md#processing) machine will trigger a *keepalive* event transitioning the `Client` back to the [**pending**](/docs/docs/api/Client.md#pending) state. During this time, the `Client` is waiting for the socket connection to timeout, and once it does, it triggers the *timeout* event and transitions to the [**idle**](/docs/docs/api/Client.md#idle) state.
|
||||
In **processing.running**, queued requests are processed in FIFO order. If a
|
||||
request body requires draining, *needDrain* moves the {Client} to the
|
||||
**processing.busy** sub-state. The *close* transition moves the {Client} to the
|
||||
**processing.closing** sub-state. When all queued requests have completed and
|
||||
neither [`client.close()`][] nor [`client.destroy()`][] has been called, the
|
||||
*keepalive* transition moves the {Client} back to the **pending** state. There
|
||||
the {Client} waits for the socket connection to time out; once it does, the
|
||||
{Client} returns to the **idle** state.
|
||||
|
||||
#### busy
|
||||
|
||||
This sub-state is only entered when a request body is an instance of [Stream](https://nodejs.org/api/stream.html) and requires draining. The `Client` cannot process additional requests while in this state and must wait until the currently processing request body is completely drained before transitioning back to [**processing.running**](/docs/docs/api/Client.md#running).
|
||||
This sub-state is entered only when a request body is a {stream.Readable} that
|
||||
requires draining. The {Client} cannot process additional requests while in
|
||||
this state and waits until the current request body has been fully drained
|
||||
before returning to **processing.running**.
|
||||
|
||||
#### closing
|
||||
|
||||
This sub-state is only entered when a `Client` instance has queued requests and the [`Client.close()`](/docs/docs/api/Client.md#clientclosecallback) method is called. In this state, the `Client` instance continues to process requests as usual, with the one exception that no additional requests can be queued. Once all of the queued requests are processed, the `Client` will trigger the *done* event gracefully entering the [**destroyed**](/docs/docs/api/Client.md#destroyed) state without an error.
|
||||
This sub-state is entered only when a {Client} has queued requests and
|
||||
[`client.close()`][] is called. The {Client} continues to process requests as
|
||||
usual, except that no additional requests may be queued. Once all queued
|
||||
requests have been processed, *done* fires and the {Client} moves gracefully to
|
||||
**destroyed** without an error.
|
||||
|
||||
### destroyed
|
||||
|
||||
The **destroyed** state is a final state for the `Client` instance. Once in this state, a `Client` is nonfunctional. Calling any other `Client` methods will result in an `ClientDestroyedError`.
|
||||
**destroyed** is the final state of a {Client} instance. Once in this state, the
|
||||
{Client} is no longer functional, and calling any further {Client} method
|
||||
rejects or fails with a {ClientDestroyedError}.
|
||||
|
||||
[`'connect'`]: Client.md#event-connect
|
||||
[`client.close()`]: Client.md#clientclosecallback
|
||||
[`client.connect()`]: Client.md#clientconnectoptions-callback
|
||||
[`client.destroy()`]: Client.md#clientdestroyerror-callback
|
||||
[`client.dispatch()`]: Client.md#clientdispatchoptions-handlers
|
||||
[`client.pipeline()`]: Client.md#clientpipelineoptions-handler
|
||||
[`client.request()`]: Client.md#clientrequestoptions-callback
|
||||
[`client.stream()`]: Client.md#clientstreamoptions-factory-callback
|
||||
[`client.upgrade()`]: Client.md#clientupgradeoptions-callback
|
||||
@@ -1,5 +1,7 @@
|
||||
# Client certificate
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
Client certificate authentication can be configured with the `Client`, the required options are passed along through the `connect` option.
|
||||
|
||||
The client certificates must be signed by a trusted CA. The Node.js default is to trust the well-known CAs curated by Mozilla.
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
# Crawling
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
[RFC 9309](https://datatracker.ietf.org/doc/html/rfc9309) defines crawlers as automated clients.
|
||||
|
||||
Some web servers may reject requests that omit the `User-Agent` header or that use common defaults such as `'curl/7.79.1'`.
|
||||
@@ -10,7 +12,7 @@ It is recommended to specify a **custom `User-Agent` header** when implementing
|
||||
|
||||
A user agent string should include sufficient detail to identify the crawler and provide contact information. For example:
|
||||
|
||||
```
|
||||
```text
|
||||
AcmeCo Crawler - acme.co - contact@acme.co
|
||||
```
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Migrating from Undici 7 to 8
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
This guide covers the changes you are most likely to hit when upgrading an
|
||||
application or library from Undici v7 to v8.
|
||||
|
||||
@@ -225,7 +227,7 @@ After moving to Undici v8, it is worth checking these paths in your test suite:
|
||||
|
||||
## Related documentation
|
||||
|
||||
- [Dispatcher](/docs/api/Dispatcher.md)
|
||||
- [Client](/docs/api/Client.md)
|
||||
- [Global Installation](/docs/api/GlobalInstallation.md)
|
||||
- [Undici Module vs. Node.js Built-in Fetch](/docs/best-practices/undici-vs-builtin-fetch.md)
|
||||
- [Dispatcher](../api/Dispatcher.md)
|
||||
- [Client](../api/Client.md)
|
||||
- [Global Installation](../api/GlobalInstallation.md)
|
||||
- [Undici Module vs. Node.js Built-in Fetch](./undici-vs-builtin-fetch.md)
|
||||
@@ -1,6 +1,8 @@
|
||||
# Mocking Request
|
||||
|
||||
Undici has its own mocking [utility](/docs/docs/api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
|
||||
<!--type=misc-->
|
||||
|
||||
Undici has its own mocking [utility](../api/MockAgent.md). It allow us to intercept undici HTTP requests and return mocked values instead. It can be useful for testing purposes.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -73,7 +75,7 @@ const badRequest = await bankTransfer('1234567890', '100')
|
||||
assert.deepEqual(badRequest, { message: 'bank account not found' })
|
||||
```
|
||||
|
||||
Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
|
||||
Explore other MockAgent functionality [here](../api/MockAgent.md)
|
||||
|
||||
## Access agent call history
|
||||
|
||||
@@ -100,7 +102,7 @@ setGlobalDispatcher(mockAgent)
|
||||
// this call is made (not intercepted)
|
||||
await fetch(`http://localhost:3000/endpoint?query='hello'`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' }
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ data: '' })
|
||||
})
|
||||
|
||||
@@ -123,11 +125,11 @@ assert.ok(mockAgent.getCallHistory()?.calls().length === 0)
|
||||
|
||||
Calling `mockAgent.close()` will automatically clear and delete every call history for you.
|
||||
|
||||
Explore other MockAgent functionality [here](/docs/docs/api/MockAgent.md)
|
||||
Explore other MockAgent functionality [here](../api/MockAgent.md)
|
||||
|
||||
Explore other MockCallHistory functionality [here](/docs/docs/api/MockCallHistory.md)
|
||||
Explore other MockCallHistory functionality [here](../api/MockCallHistory.md)
|
||||
|
||||
Explore other MockCallHistoryLog functionality [here](/docs/docs/api/MockCallHistoryLog.md)
|
||||
Explore other MockCallHistoryLog functionality [here](../api/MockCallHistoryLog.md)
|
||||
|
||||
## Debug Mock Value
|
||||
|
||||
@@ -179,7 +181,7 @@ mockPool.intercept({
|
||||
|
||||
in this case opts will be
|
||||
|
||||
```
|
||||
```js
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'X-TOKEN-SECRET': 'SuperSecretToken' },
|
||||
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
# Connecting through a proxy
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
Connecting through a proxy is possible by:
|
||||
|
||||
- Using [ProxyAgent](/docs/docs/api/ProxyAgent.md).
|
||||
- Using {ProxyAgent}.
|
||||
- Configuring `Client` or `Pool` constructor.
|
||||
|
||||
The proxy url should be passed to the `Client` or `Pool` constructor, while the upstream server url
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# Undici Module vs. Node.js Built-in Fetch
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
Node.js has shipped a built-in `fetch()` implementation powered by undici since
|
||||
Node.js v18. This guide explains the relationship between the `undici` npm
|
||||
package and the built-in `fetch`, and when you should install one versus relying
|
||||
@@ -57,7 +59,7 @@ await fetch('https://example.com', {
|
||||
### `undici.install()` globals
|
||||
|
||||
If you want the installed `undici` package to provide the globals, call
|
||||
[`install()`](/docs/api/GlobalInstallation.md):
|
||||
[`install()`](../api/GlobalInstallation.md):
|
||||
|
||||
```js
|
||||
import { install } from 'undici'
|
||||
@@ -207,7 +209,7 @@ You can always check the exact bundled version at runtime with
|
||||
|
||||
Installing undici from npm does not replace the built-in globals. If you want
|
||||
your installed version to replace the global `fetch` and related classes, use
|
||||
[`install()`](/docs/api/GlobalInstallation.md). Otherwise, import `fetch`
|
||||
[`install()`](../api/GlobalInstallation.md). Otherwise, import `fetch`
|
||||
directly from `'undici'`:
|
||||
|
||||
```js
|
||||
@@ -216,9 +218,9 @@ import { fetch } from 'undici' // uses your installed version, not the built-in
|
||||
|
||||
## Further reading
|
||||
|
||||
- [API Reference: Fetch](/docs/api/Fetch.md)
|
||||
- [API Reference: Client](/docs/api/Client.md)
|
||||
- [API Reference: Pool](/docs/api/Pool.md)
|
||||
- [API Reference: ProxyAgent](/docs/api/ProxyAgent.md)
|
||||
- [API Reference: MockAgent](/docs/api/MockAgent.md)
|
||||
- [API Reference: Global Installation](/docs/api/GlobalInstallation.md)
|
||||
- [API Reference: Fetch](../api/Fetch.md)
|
||||
- [API Reference: Client](../api/Client.md)
|
||||
- [API Reference: Pool](../api/Pool.md)
|
||||
- [API Reference: ProxyAgent](../api/ProxyAgent.md)
|
||||
- [API Reference: MockAgent](../api/MockAgent.md)
|
||||
- [API Reference: Global Installation](../api/GlobalInstallation.md)
|
||||
@@ -1,5 +1,7 @@
|
||||
# Writing tests
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
Undici is tuned for a production use case and its default will keep
|
||||
a socket open for a few seconds after an HTTP request is completed to
|
||||
remove the overhead of opening up a new socket. These settings that makes
|
||||
|
||||
+22
-19
@@ -1,5 +1,7 @@
|
||||
# Getting Started
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
@@ -12,7 +14,7 @@ The quickest way to get started is with `fetch`, which follows the
|
||||
[Fetch Standard](https://fetch.spec.whatwg.org/) and works the same way as
|
||||
the browser API:
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { fetch } from 'undici'
|
||||
|
||||
const res = await fetch('https://example.com')
|
||||
@@ -24,7 +26,7 @@ console.log(data)
|
||||
|
||||
undici also exports a `Request` class that follows the Fetch Standard:
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { fetch, Request } from 'undici'
|
||||
|
||||
const req = new Request('https://example.com', {
|
||||
@@ -41,7 +43,7 @@ console.log(res.status)
|
||||
`res.body` is a web `ReadableStream`. Use `pipeline` from
|
||||
`node:stream/promises` to stream it to a file:
|
||||
|
||||
```js
|
||||
```mjs
|
||||
import { fetch } from 'undici'
|
||||
import { pipeline } from 'node:stream/promises'
|
||||
import { createWriteStream } from 'node:fs'
|
||||
@@ -53,10 +55,10 @@ await pipeline(res.body, createWriteStream('./file.zip'))
|
||||
> Always consume or cancel the response body. In Node.js, garbage collection
|
||||
> is not aggressive enough to release connections promptly, so leaving a body
|
||||
> unread can cause connection leaks and stalled requests. See
|
||||
> [Specification Compliance - Garbage Collection](/docs/#garbage-collection)
|
||||
> [Specification Compliance - Garbage Collection](/#garbage-collection)
|
||||
> for details.
|
||||
|
||||
For more on `fetch`, see [API Reference: Fetch](/docs/docs/api/Fetch.md).
|
||||
For more on `fetch`, see [API Reference: Fetch](api/Fetch.md).
|
||||
|
||||
## Dispatchers: Connection reuse and pooling
|
||||
|
||||
@@ -111,7 +113,8 @@ pool.close()
|
||||
### `Client` — for a single connection
|
||||
|
||||
`Client` maps to a single TCP connection. It supports pipelining (sending
|
||||
multiple requests before responses arrive):
|
||||
multiple requests before responses arrive), which should only be enabled for
|
||||
trusted remote servers:
|
||||
|
||||
```js
|
||||
import { Client } from 'undici'
|
||||
@@ -127,9 +130,9 @@ client.close()
|
||||
```
|
||||
|
||||
For more on dispatcher options and lifecycle, see:
|
||||
- [API Reference: Agent](/docs/docs/api/Agent.md)
|
||||
- [API Reference: Pool](/docs/docs/api/Pool.md)
|
||||
- [API Reference: Client](/docs/docs/api/Client.md)
|
||||
- [API Reference: Agent](api/Agent.md)
|
||||
- [API Reference: Pool](api/Pool.md)
|
||||
- [API Reference: Client](api/Client.md)
|
||||
|
||||
## Timeouts
|
||||
|
||||
@@ -152,7 +155,7 @@ setGlobalDispatcher(agent)
|
||||
```
|
||||
|
||||
Timeout errors are thrown as `HeadersTimeoutError` and `BodyTimeoutError`.
|
||||
See [API Reference: Errors](/docs/docs/api/Errors.md) for the full list.
|
||||
See [API Reference: Errors](api/Errors.md) for the full list.
|
||||
|
||||
## Error handling
|
||||
|
||||
@@ -217,8 +220,8 @@ const proxy = new ProxyAgent('http://proxy.internal:8080')
|
||||
setGlobalDispatcher(proxy)
|
||||
```
|
||||
|
||||
See [Best Practices: Proxy](/docs/docs/best-practices/proxy.md) and
|
||||
[API Reference: ProxyAgent](/docs/docs/api/ProxyAgent.md).
|
||||
See [Best Practices: Proxy](best-practices/proxy.md) and
|
||||
[API Reference: ProxyAgent](api/ProxyAgent.md).
|
||||
|
||||
### Mocking in tests
|
||||
|
||||
@@ -235,8 +238,8 @@ const { body } = await request('https://api.example.com/users')
|
||||
console.log(await body.json())
|
||||
```
|
||||
|
||||
See [Best Practices: Mocking Request](/docs/docs/best-practices/mocking-request.md)
|
||||
and [API Reference: MockAgent](/docs/docs/api/MockAgent.md).
|
||||
See [Best Practices: Mocking Request](best-practices/mocking-request.md)
|
||||
and [API Reference: MockAgent](api/MockAgent.md).
|
||||
|
||||
### Testing with undici
|
||||
|
||||
@@ -252,7 +255,7 @@ const agent = new Agent({
|
||||
setGlobalDispatcher(agent)
|
||||
```
|
||||
|
||||
See [Best Practices: Writing Tests](/docs/docs/best-practices/writing-tests.md).
|
||||
See [Best Practices: Writing Tests](best-practices/writing-tests.md).
|
||||
|
||||
### Customizing the global fetch
|
||||
|
||||
@@ -268,11 +271,11 @@ install()
|
||||
const res = await fetch('https://example.com')
|
||||
```
|
||||
|
||||
See [API Reference: Global Installation](/docs/docs/api/GlobalInstallation.md).
|
||||
See [API Reference: Global Installation](api/GlobalInstallation.md).
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Undici vs. Built-in Fetch](/docs/docs/best-practices/undici-vs-builtin-fetch.md) —
|
||||
- [Undici vs. Built-in Fetch](best-practices/undici-vs-builtin-fetch.md) —
|
||||
when to install undici vs using Node.js built-in fetch
|
||||
- [API Reference](/docs/docs/api/Dispatcher.md) — full dispatcher API documentation
|
||||
- [Examples](/docs/examples/) — runnable code examples
|
||||
- [API Reference](api/Dispatcher.md) — full dispatcher API documentation
|
||||
- [Examples](/examples/) — runnable code examples
|
||||
Vendored
+779
@@ -0,0 +1,779 @@
|
||||
# undici
|
||||
|
||||
<!--type=misc-->
|
||||
|
||||
[](https://github.com/nodejs/undici/actions/workflows/nodejs.yml) [](https://github.com/neostandard/neostandard) [](https://badge.fury.io/js/undici) [](https://codecov.io/gh/nodejs/undici)
|
||||
|
||||
An HTTP/1.1 client, written from scratch for Node.js.
|
||||
|
||||
> Undici means eleven in Italian. 1.1 -> 11 -> Eleven -> Undici.
|
||||
It is also a Stranger Things reference.
|
||||
|
||||
## How to get involved
|
||||
|
||||
Have a question about using Undici? Open a [Q&A Discussion](https://github.com/nodejs/undici/discussions/new) or join our official OpenJS [Slack](https://openjs-foundation.slack.com/archives/C01QF9Q31QD) channel.
|
||||
|
||||
Looking to contribute? Start by reading the [contributing guide](./CONTRIBUTING.md)
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
npm i undici
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
The benchmark is a simple getting data [example](https://github.com/nodejs/undici/blob/main/benchmarks/benchmark.js) using
|
||||
50 TCP connections with a pipelining depth of 10 running on Node 24.14.1.
|
||||
|
||||
### HTTP/1.1
|
||||
|
||||
```text
|
||||
┌────────────────────────┬─────────┬────────────────────┬────────────┬─────────────────────────┐
|
||||
│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │
|
||||
├────────────────────────┼─────────┼────────────────────┼────────────┼─────────────────────────┤
|
||||
│ 'node-fetch' │ 50 │ '4711.86 req/sec' │ '± 2.92 %' │ '-' │
|
||||
│ 'undici - fetch' │ 75 │ '5438.50 req/sec' │ '± 2.97 %' │ '+ 15.42 %' │
|
||||
│ 'axios' │ 45 │ '5448.08 req/sec' │ '± 2.98 %' │ '+ 15.62 %' │
|
||||
│ 'request' │ 65 │ '5809.63 req/sec' │ '± 2.90 %' │ '+ 23.30 %' │
|
||||
│ 'http - no keepalive' │ 35 │ '5910.77 req/sec' │ '± 2.87 %' │ '+ 25.44 %' │
|
||||
│ 'got' │ 50 │ '6047.80 req/sec' │ '± 2.91 %' │ '+ 28.35 %' │
|
||||
│ 'superagent' │ 60 │ '7534.53 req/sec' │ '± 2.97 %' │ '+ 59.91 %' │
|
||||
│ 'http - keepalive' │ 75 │ '9343.41 req/sec' │ '± 2.90 %' │ '+ 98.30 %' │
|
||||
│ 'undici - pipeline' │ 65 │ '13470.70 req/sec' │ '± 2.93 %' │ '+ 185.89 %' │
|
||||
│ 'undici - request' │ 80 │ '16850.87 req/sec' │ '± 2.93 %' │ '+ 257.63 %' │
|
||||
│ 'undici - stream' │ 101 │ '18488.56 req/sec' │ '± 3.81 %' │ '+ 292.38 %' │
|
||||
│ 'undici - dispatch' │ 101 │ '20786.44 req/sec' │ '± 3.08 %' │ '+ 341.15 %' │
|
||||
└────────────────────────┴─────────┴────────────────────┴────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### HTTP/1.1 over HTTPS
|
||||
|
||||
Using [benchmark-https.js](https://github.com/nodejs/undici/blob/main/benchmarks/benchmark-https.js) against an h1-over-TLS server (50 connections, pipelining depth 10, Node 24.14.1).
|
||||
|
||||
```text
|
||||
┌────────────────────────┬─────────┬───────────────────┬────────────┬─────────────────────────┐
|
||||
│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │
|
||||
├────────────────────────┼─────────┼───────────────────┼────────────┼─────────────────────────┤
|
||||
│ 'https - no keepalive'│ 10 │ '1358.40 req/sec' │ '± 1.99 %' │ '-' │
|
||||
│ 'undici - fetch' │ 30 │ '3721.76 req/sec' │ '± 2.97 %' │ '+ 173.98 %' │
|
||||
│ 'https - keepalive' │ 35 │ '5633.91 req/sec' │ '± 2.84 %' │ '+ 314.75 %' │
|
||||
│ 'undici - pipeline' │ 15 │ '6254.05 req/sec' │ '± 2.80 %' │ '+ 360.40 %' │
|
||||
│ 'undici - request' │ 25 │ '6669.80 req/sec' │ '± 2.73 %' │ '+ 391.01 %' │
|
||||
│ 'undici - stream' │ 25 │ '7019.04 req/sec' │ '± 2.77 %' │ '+ 416.71 %' │
|
||||
│ 'undici - dispatch' │ 20 │ '7361.85 req/sec' │ '± 2.90 %' │ '+ 441.95 %' │
|
||||
└────────────────────────┴─────────┴───────────────────┴────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### HTTP/2
|
||||
|
||||
Using [benchmark-http2.js](https://github.com/nodejs/undici/blob/main/benchmarks/benchmark-http2.js) against an h2 server (50 connections, pipelining depth 10, Node 24.14.1).
|
||||
|
||||
```text
|
||||
┌────────────────────────┬─────────┬───────────────────┬────────────┬─────────────────────────┐
|
||||
│ Tests │ Samples │ Result │ Tolerance │ Difference with slowest │
|
||||
├────────────────────────┼─────────┼───────────────────┼────────────┼─────────────────────────┤
|
||||
│ 'undici - fetch' │ 45 │ '3499.03 req/sec' │ '± 2.93 %' │ '-' │
|
||||
│ 'native - http2' │ 25 │ '4904.58 req/sec' │ '± 2.81 %' │ '+ 40.17 %' │
|
||||
│ 'undici - pipeline' │ 60 │ '5836.82 req/sec' │ '± 2.99 %' │ '+ 66.81 %' │
|
||||
│ 'undici - request' │ 65 │ '6831.25 req/sec' │ '± 2.83 %' │ '+ 95.23 %' │
|
||||
│ 'undici - stream' │ 55 │ '6874.30 req/sec' │ '± 2.91 %' │ '+ 96.46 %' │
|
||||
│ 'undici - dispatch' │ 55 │ '7791.23 req/sec' │ '± 2.96 %' │ '+ 122.67 %' │
|
||||
└────────────────────────┴─────────┴───────────────────┴────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
## Undici vs. Fetch
|
||||
|
||||
### Overview
|
||||
|
||||
Node.js includes a built-in `fetch()` implementation powered by undici starting from Node.js v18. However, there are important differences between using the built-in fetch and installing undici as a separate module.
|
||||
|
||||
### Built-in Fetch (Node.js v18+)
|
||||
|
||||
Node.js's built-in fetch is powered by a bundled version of undici:
|
||||
|
||||
```js
|
||||
// Available globally in Node.js v18+
|
||||
const response = await fetch('https://api.example.com/data');
|
||||
const data = await response.json();
|
||||
|
||||
// Check the bundled undici version
|
||||
console.log(process.versions.undici); // e.g., "5.28.4"
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- No additional dependencies required
|
||||
- Works across different JavaScript runtimes
|
||||
- Automatic compression handling (gzip, deflate, br)
|
||||
- Built-in caching support (in development)
|
||||
|
||||
**Cons:**
|
||||
- Limited to the undici version bundled with your Node.js version
|
||||
- Less control over connection pooling and advanced features
|
||||
- Error handling follows Web API standards (errors wrapped in `TypeError`)
|
||||
- Performance overhead due to Web Streams implementation
|
||||
|
||||
### Undici Module
|
||||
|
||||
Installing undici as a separate module gives you access to the latest features and APIs:
|
||||
|
||||
```bash
|
||||
npm install undici
|
||||
```
|
||||
|
||||
```js
|
||||
import { request, fetch, Agent, setGlobalDispatcher } from 'undici';
|
||||
|
||||
// Use undici.request for maximum performance
|
||||
const { statusCode, headers, body } = await request('https://api.example.com/data');
|
||||
const data = await body.json();
|
||||
|
||||
// Or use undici.fetch with custom configuration
|
||||
const agent = new Agent({ keepAliveTimeout: 10000 });
|
||||
setGlobalDispatcher(agent);
|
||||
const response = await fetch('https://api.example.com/data');
|
||||
```
|
||||
|
||||
**Pros:**
|
||||
- Latest undici features and bug fixes
|
||||
- Access to advanced APIs (`request`, `stream`, `pipeline`)
|
||||
- Fine-grained control over connection pooling
|
||||
- Better error handling with clearer error messages
|
||||
- Superior performance, especially with `undici.request`
|
||||
- HTTP/1.1 pipelining support
|
||||
- Custom interceptors and middleware
|
||||
- Advanced features like `ProxyAgent`, `Socks5Agent`, `MockAgent`
|
||||
|
||||
**Cons:**
|
||||
- Additional dependency to manage
|
||||
- Larger bundle size
|
||||
|
||||
### When to Use Each
|
||||
|
||||
#### Use Built-in Fetch When:
|
||||
- You want zero dependencies
|
||||
- Building isomorphic code that runs in browsers and Node.js
|
||||
- Publishing to npm and want to maximize compatibility with JS runtimes
|
||||
- Simple HTTP requests without advanced configuration
|
||||
- You're publishing to npm and you want to maximize compatiblity
|
||||
- You don't depend on features from a specific version of undici
|
||||
|
||||
#### Use Undici Module When:
|
||||
- You need the latest undici features and performance improvements
|
||||
- You require advanced connection pooling configuration
|
||||
- You need APIs not available in the built-in fetch (`ProxyAgent`, `Socks5Agent`, `MockAgent`, etc.)
|
||||
- Performance is critical (use `undici.request` for maximum speed)
|
||||
- You want better error handling and debugging capabilities
|
||||
- You need HTTP/1.1 pipelining or advanced interceptors
|
||||
- You prefer decoupled protocol and API interfaces
|
||||
|
||||
### Performance Comparison
|
||||
|
||||
Based on benchmarks, here's the typical performance hierarchy:
|
||||
|
||||
1. **`undici.request()`** - Fastest, most efficient
|
||||
2. **`undici.fetch()`** - Good performance, standard compliance
|
||||
3. **Node.js `http`/`https`** - Baseline performance
|
||||
|
||||
### Migration Guide
|
||||
|
||||
If you're currently using built-in fetch and want to migrate to undici:
|
||||
|
||||
```js
|
||||
// Before: Built-in fetch
|
||||
const response = await fetch('https://api.example.com/data');
|
||||
|
||||
// After: Undici fetch (drop-in replacement)
|
||||
import { fetch } from 'undici';
|
||||
const response = await fetch('https://api.example.com/data');
|
||||
|
||||
// Or: Undici request (better performance)
|
||||
import { request } from 'undici';
|
||||
const { statusCode, body } = await request('https://api.example.com/data');
|
||||
const data = await body.json();
|
||||
```
|
||||
|
||||
### Keep `fetch` and `FormData` together
|
||||
|
||||
When you send a `FormData` body, keep `fetch` and `FormData` from the same
|
||||
implementation.
|
||||
|
||||
Use one of these patterns:
|
||||
|
||||
```js
|
||||
// Built-in globals
|
||||
const body = new FormData()
|
||||
body.set('name', 'some')
|
||||
await fetch('https://example.com', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
```
|
||||
|
||||
```js
|
||||
// undici module imports
|
||||
import { fetch, FormData } from 'undici'
|
||||
|
||||
const body = new FormData()
|
||||
body.set('name', 'some')
|
||||
await fetch('https://example.com', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
```
|
||||
|
||||
If you want the installed `undici` package to provide the globals, call
|
||||
`install()` first:
|
||||
|
||||
```js
|
||||
import { install } from 'undici'
|
||||
|
||||
install()
|
||||
|
||||
const body = new FormData()
|
||||
body.set('name', 'some')
|
||||
await fetch('https://example.com', {
|
||||
method: 'POST',
|
||||
body
|
||||
})
|
||||
```
|
||||
|
||||
`install()` replaces the global `fetch`, `Headers`, `Response`, `Request`, and
|
||||
`FormData` implementations with undici's versions, so they all match. It also
|
||||
installs undici's `WebSocket`, `CloseEvent`, `ErrorEvent`, `MessageEvent`, and
|
||||
`EventSource` globals.
|
||||
|
||||
Avoid mixing a global `FormData` with `undici.fetch()`, or `undici.FormData`
|
||||
with the built-in global `fetch()`.
|
||||
|
||||
### Version Compatibility
|
||||
|
||||
You can check which version of undici is bundled with your Node.js version:
|
||||
|
||||
```js
|
||||
console.log(process.versions.undici);
|
||||
```
|
||||
|
||||
Installing undici as a module allows you to use a newer version than what's bundled with Node.js, giving you access to the latest features and performance improvements.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Basic Request
|
||||
|
||||
```js
|
||||
import { request } from 'undici'
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
headers,
|
||||
trailers,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode)
|
||||
console.log('headers', headers)
|
||||
|
||||
for await (const data of body) { console.log('data', data) }
|
||||
|
||||
console.log('trailers', trailers)
|
||||
```
|
||||
|
||||
### Using Cache Interceptor
|
||||
|
||||
Undici provides a powerful HTTP caching interceptor that follows HTTP caching best practices. Here's how to use it:
|
||||
|
||||
```js
|
||||
import { fetch, Agent, interceptors, cacheStores } from 'undici';
|
||||
|
||||
// Create a client with cache interceptor
|
||||
const client = new Agent().compose(interceptors.cache({
|
||||
// Optional: Configure cache store (defaults to MemoryCacheStore)
|
||||
store: new cacheStores.MemoryCacheStore({
|
||||
maxSize: 100 * 1024 * 1024, // 100MB
|
||||
maxCount: 1000,
|
||||
maxEntrySize: 5 * 1024 * 1024 // 5MB
|
||||
}),
|
||||
|
||||
// Optional: Specify which HTTP methods to cache (default: ['GET', 'HEAD'])
|
||||
methods: ['GET', 'HEAD']
|
||||
}));
|
||||
|
||||
// Set the global dispatcher to use our caching client
|
||||
setGlobalDispatcher(client);
|
||||
|
||||
// Now all fetch requests will use the cache
|
||||
async function getData() {
|
||||
const response = await fetch('https://api.example.com/data');
|
||||
// The server should set appropriate Cache-Control headers in the response
|
||||
// which the cache will respect based on the cache policy
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// First request - fetches from origin
|
||||
const data1 = await getData();
|
||||
|
||||
// Second request - served from cache if within max-age
|
||||
const data2 = await getData();
|
||||
```
|
||||
|
||||
#### Key Features:
|
||||
- **Automatic Caching**: Respects `Cache-Control` and `Expires` headers
|
||||
- **Validation**: Supports `ETag` and `Last-Modified` validation
|
||||
- **Storage Options**: In-memory or persistent SQLite storage
|
||||
- **Flexible**: Configure cache size, TTL, and more
|
||||
|
||||
## Global Installation
|
||||
|
||||
Undici provides an `install()` function to add fetch-related and other web API classes to `globalThis`, making them available globally:
|
||||
|
||||
```js
|
||||
import { install } from 'undici'
|
||||
|
||||
// Install undici's global web APIs
|
||||
install()
|
||||
|
||||
// Now you can use fetch classes globally without importing
|
||||
const response = await fetch('https://api.example.com/data')
|
||||
const data = await response.json()
|
||||
|
||||
// All classes are available globally:
|
||||
const headers = new Headers([['content-type', 'application/json']])
|
||||
const request = new Request('https://example.com')
|
||||
const formData = new FormData()
|
||||
const ws = new WebSocket('wss://example.com')
|
||||
const eventSource = new EventSource('https://example.com/events')
|
||||
```
|
||||
|
||||
The `install()` function adds the following classes to `globalThis`:
|
||||
|
||||
- `fetch` - The fetch function
|
||||
- `Headers` - HTTP headers management
|
||||
- `Response` - HTTP response representation
|
||||
- `Request` - HTTP request representation
|
||||
- `FormData` - Form data handling
|
||||
- `WebSocket` - WebSocket client
|
||||
- `CloseEvent`, `ErrorEvent`, `MessageEvent` - WebSocket events
|
||||
- `EventSource` - Server-sent events client
|
||||
|
||||
When you call `install()`, these globals come from the same undici
|
||||
implementation. For example, global `fetch` and global `FormData` will both be
|
||||
undici's versions, and `WebSocket` and `EventSource` will also come from
|
||||
undici, which is the recommended setup if you want to use undici through
|
||||
globals.
|
||||
|
||||
This is useful for:
|
||||
- Polyfilling environments that don't have fetch
|
||||
- Ensuring consistent fetch behavior across different Node.js versions
|
||||
- Making undici's implementations available globally for libraries that expect them
|
||||
|
||||
## Body Mixins
|
||||
|
||||
The `body` mixins are the most common way to format the request/response body. Mixins include:
|
||||
|
||||
- [`.arrayBuffer()`](https://fetch.spec.whatwg.org/#dom-body-arraybuffer)
|
||||
- [`.blob()`](https://fetch.spec.whatwg.org/#dom-body-blob)
|
||||
- [`.bytes()`](https://fetch.spec.whatwg.org/#dom-body-bytes)
|
||||
- [`.json()`](https://fetch.spec.whatwg.org/#dom-body-json)
|
||||
- [`.text()`](https://fetch.spec.whatwg.org/#dom-body-text)
|
||||
|
||||
> [!NOTE]
|
||||
> The body returned from `undici.request` does not implement `.formData()`.
|
||||
|
||||
> [!WARNING]
|
||||
> Calling `body.formData()` on a fetch response causes undici to buffer and parse the entire body. Since this is dictated by the spec, `body.formData()` must only be called on responses from trusted servers.
|
||||
|
||||
Example usage:
|
||||
|
||||
```js
|
||||
import { request } from 'undici'
|
||||
|
||||
const {
|
||||
statusCode,
|
||||
headers,
|
||||
trailers,
|
||||
body
|
||||
} = await request('http://localhost:3000/foo')
|
||||
|
||||
console.log('response received', statusCode)
|
||||
console.log('headers', headers)
|
||||
console.log('data', await body.json())
|
||||
console.log('trailers', trailers)
|
||||
```
|
||||
|
||||
_Note: Once a mixin has been called then the body cannot be reused, thus calling additional mixins on `.body`, e.g. `.body.json(); .body.text()` will result in an error `TypeError: unusable` being thrown and returned through the `Promise` rejection._
|
||||
|
||||
Should you need to access the `body` in plain-text after using a mixin, the best practice is to use the `.text()` mixin first and then manually parse the text to the desired format.
|
||||
|
||||
For more information about their behavior, please reference the body mixin from the [Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin).
|
||||
|
||||
## Common API Methods
|
||||
|
||||
This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
|
||||
|
||||
### `undici.request([url, options])`
|
||||
|
||||
* `url` {string|URL|UrlObject}
|
||||
* `options` {RequestOptions} See [`RequestOptions`](./docs/docs/api/Dispatcher.md#parameter-requestoptions).
|
||||
* `dispatcher` {Dispatcher} **Default:** [getGlobalDispatcher](#undicigetglobaldispatcher).
|
||||
* `method` {string} **Default:** `PUT` if `options.body`, otherwise `GET`.
|
||||
* Returns: {Promise} A promise with the result of the `Dispatcher.request` method.
|
||||
|
||||
Calls `options.dispatcher.request(options)`.
|
||||
|
||||
See [Dispatcher.request](./docs/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./docs/examples/README.md) for examples.
|
||||
|
||||
### `undici.stream([url, options, ]factory)`
|
||||
|
||||
* `url` {string|URL|UrlObject}
|
||||
* `options` {StreamOptions} See [`StreamOptions`](./docs/docs/api/Dispatcher.md#parameter-streamoptions).
|
||||
* `dispatcher` {Dispatcher} **Default:** [getGlobalDispatcher](#undicigetglobaldispatcher).
|
||||
* `method` {string} **Default:** `PUT` if `options.body`, otherwise `GET`.
|
||||
* `factory` {Function} `Dispatcher.stream.factory`.
|
||||
* Returns: {Promise} A promise with the result of the `Dispatcher.stream` method.
|
||||
|
||||
Calls `options.dispatcher.stream(options, factory)`.
|
||||
|
||||
See [Dispatcher.stream](./docs/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
|
||||
|
||||
### `undici.pipeline([url, options, ]handler)`
|
||||
|
||||
* `url` {string|URL|UrlObject}
|
||||
* `options` {PipelineOptions} See [`PipelineOptions`](./docs/docs/api/Dispatcher.md#parameter-pipelineoptions).
|
||||
* `dispatcher` {Dispatcher} **Default:** [getGlobalDispatcher](#undicigetglobaldispatcher).
|
||||
* `method` {string} **Default:** `PUT` if `options.body`, otherwise `GET`.
|
||||
* `handler` {Function} `Dispatcher.pipeline.handler`.
|
||||
* Returns: {Duplex} A `stream.Duplex`.
|
||||
|
||||
Calls `options.dispatch.pipeline(options, handler)`.
|
||||
|
||||
See [Dispatcher.pipeline](./docs/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
|
||||
|
||||
### `undici.connect([url, options])`
|
||||
|
||||
Starts two-way communications with the requested resource using [HTTP CONNECT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/CONNECT).
|
||||
|
||||
* `url` {string|URL|UrlObject}
|
||||
* `options` {ConnectOptions} See [`ConnectOptions`](./docs/docs/api/Dispatcher.md#parameter-connectoptions).
|
||||
* `dispatcher` {Dispatcher} **Default:** [getGlobalDispatcher](#undicigetglobaldispatcher).
|
||||
* `callback` {Function} (optional) — `(err, data) => void`.
|
||||
* Returns: {Promise} A promise with the result of the `Dispatcher.connect` method.
|
||||
|
||||
Calls `options.dispatch.connect(options)`.
|
||||
|
||||
See [Dispatcher.connect](./docs/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
|
||||
|
||||
### `undici.fetch(input[, init])`
|
||||
|
||||
Implements [fetch](https://fetch.spec.whatwg.org/#fetch-method).
|
||||
|
||||
* https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
|
||||
* https://fetch.spec.whatwg.org/#fetch-method
|
||||
|
||||
Basic usage example:
|
||||
|
||||
```js
|
||||
import { fetch } from 'undici'
|
||||
|
||||
|
||||
const res = await fetch('https://example.com')
|
||||
const json = await res.json()
|
||||
console.log(json)
|
||||
```
|
||||
|
||||
You can pass an optional dispatcher to `fetch` as:
|
||||
|
||||
```js
|
||||
import { fetch, Agent } from 'undici'
|
||||
|
||||
const res = await fetch('https://example.com', {
|
||||
// Mocks are also supported
|
||||
dispatcher: new Agent({
|
||||
keepAliveTimeout: 10,
|
||||
keepAliveMaxTimeout: 10
|
||||
})
|
||||
})
|
||||
const json = await res.json()
|
||||
console.log(json)
|
||||
```
|
||||
|
||||
#### `request.body`
|
||||
|
||||
A body can be of the following types:
|
||||
|
||||
- ArrayBuffer
|
||||
- ArrayBufferView
|
||||
- AsyncIterables
|
||||
- Blob
|
||||
- Iterables
|
||||
- String
|
||||
- URLSearchParams
|
||||
- FormData
|
||||
|
||||
In this implementation of fetch, ```request.body``` now accepts ```Async Iterables```. It is not present in the [Fetch Standard](https://fetch.spec.whatwg.org).
|
||||
|
||||
```js
|
||||
import { fetch } from 'undici'
|
||||
|
||||
const data = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield 'hello'
|
||||
yield 'world'
|
||||
},
|
||||
}
|
||||
|
||||
await fetch('https://example.com', { body: data, method: 'POST', duplex: 'half' })
|
||||
```
|
||||
|
||||
[FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData) besides text data and buffers can also utilize streams via [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) objects:
|
||||
|
||||
```js
|
||||
import { openAsBlob } from 'node:fs'
|
||||
|
||||
const file = await openAsBlob('./big.csv')
|
||||
const body = new FormData()
|
||||
body.set('file', file, 'big.csv')
|
||||
|
||||
await fetch('http://example.com', { method: 'POST', body })
|
||||
```
|
||||
|
||||
#### `request.duplex`
|
||||
|
||||
- `'half'`
|
||||
|
||||
In this implementation of fetch, `request.duplex` must be set if `request.body` is `ReadableStream` or `Async Iterables`, however, even though the value must be set to `'half'`, it is actually a _full_ duplex. For more detail refer to the [Fetch Standard](https://fetch.spec.whatwg.org/#dom-requestinit-duplex).
|
||||
|
||||
#### `response.body`
|
||||
|
||||
Nodejs has two kinds of streams: [web streams](https://nodejs.org/api/webstreams.html), which follow the API of the WHATWG web standard found in browsers, and an older Node-specific [streams API](https://nodejs.org/api/stream.html). `response.body` returns a readable web stream. If you would prefer to work with a Node stream you can convert a web stream using `.fromWeb()`.
|
||||
|
||||
```js
|
||||
import { fetch } from 'undici'
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
const response = await fetch('https://example.com')
|
||||
const readableWebStream = response.body
|
||||
const readableNodeStream = Readable.fromWeb(readableWebStream)
|
||||
```
|
||||
|
||||
## Specification Compliance
|
||||
|
||||
This section documents parts of the [HTTP/1.1](https://www.rfc-editor.org/rfc/rfc9110.html) and [Fetch Standard](https://fetch.spec.whatwg.org) that Undici does
|
||||
not support or does not fully implement.
|
||||
|
||||
#### CORS
|
||||
|
||||
Unlike browsers, Undici does not implement CORS (Cross-Origin Resource Sharing) checks by default. This means:
|
||||
|
||||
- No preflight requests are automatically sent for cross-origin requests
|
||||
- No validation of `Access-Control-Allow-Origin` headers is performed
|
||||
- Requests to any origin are allowed regardless of the source
|
||||
|
||||
This behavior is intentional for server-side environments where CORS restrictions are typically unnecessary. If your application requires CORS-like protections, you will need to implement these checks manually.
|
||||
|
||||
#### Garbage Collection
|
||||
|
||||
* https://fetch.spec.whatwg.org/#garbage-collection
|
||||
|
||||
The [Fetch Standard](https://fetch.spec.whatwg.org) allows users to skip consuming the response body by relying on
|
||||
[garbage collection](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Memory_Management#garbage_collection) to release connection resources.
|
||||
|
||||
Garbage collection in Node is less aggressive and deterministic
|
||||
(due to the lack of clear idle periods that browsers have through the rendering refresh rate)
|
||||
which means that leaving the release of connection resources to the garbage collector can lead
|
||||
to excessive connection usage, reduced performance (due to less connection re-use), and even
|
||||
stalls or deadlocks when running out of connections.
|
||||
Therefore, __it is important to always either consume or cancel the response body anyway__.
|
||||
|
||||
```js
|
||||
// Do
|
||||
const { body, headers } = await fetch(url);
|
||||
for await (const chunk of body) {
|
||||
// force consumption of body
|
||||
}
|
||||
|
||||
// Do not
|
||||
const { headers } = await fetch(url);
|
||||
```
|
||||
|
||||
However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details.
|
||||
|
||||
```js
|
||||
const headers = await fetch(url, { method: 'HEAD' })
|
||||
.then(res => res.headers)
|
||||
```
|
||||
|
||||
Note that consuming the response body is _mandatory_ for `request`:
|
||||
|
||||
```js
|
||||
// Do
|
||||
const { body, headers } = await request(url);
|
||||
await body.dump(); // force consumption of body
|
||||
|
||||
// Do not
|
||||
const { headers } = await request(url);
|
||||
```
|
||||
|
||||
#### Forbidden and Safelisted Header Names
|
||||
|
||||
* https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
|
||||
* https://fetch.spec.whatwg.org/#forbidden-header-name
|
||||
* https://fetch.spec.whatwg.org/#forbidden-response-header-name
|
||||
* https://github.com/wintercg/fetch/issues/6
|
||||
|
||||
The [Fetch Standard](https://fetch.spec.whatwg.org) requires implementations to exclude certain headers from requests and responses. In browser environments, some headers are forbidden so the user agent remains in full control over them. In Undici, these constraints are removed to give more control to the user.
|
||||
|
||||
#### Content-Encoding
|
||||
|
||||
* https://www.rfc-editor.org/rfc/rfc9110#field.content-encoding
|
||||
|
||||
Undici limits the number of `Content-Encoding` layers in a response to **5** to prevent resource exhaustion attacks. If a server responds with more than 5 content-encodings (e.g., `Content-Encoding: gzip, gzip, gzip, gzip, gzip, gzip`), the fetch will be rejected with an error. This limit matches the approach taken by [curl](https://curl.se/docs/CVE-2022-32206.html) and [urllib3](https://github.com/advisories/GHSA-gm62-xv2j-4rw9).
|
||||
|
||||
#### `undici.upgrade([url, options])`
|
||||
|
||||
Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) for more details.
|
||||
|
||||
* `url` {string|URL|UrlObject}
|
||||
* `options` {UpgradeOptions} See [`UpgradeOptions`](./docs/docs/api/Dispatcher.md#parameter-upgradeoptions).
|
||||
* `dispatcher` {Dispatcher} **Default:** [getGlobalDispatcher](#undicigetglobaldispatcher).
|
||||
* `callback` {Function} (optional) — `(error, data) => void`.
|
||||
* Returns: {Promise} A promise with the result of the `Dispatcher.upgrade` method.
|
||||
|
||||
Calls `options.dispatcher.upgrade(options)`.
|
||||
|
||||
See [Dispatcher.upgrade](./docs/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
|
||||
|
||||
### `undici.setGlobalDispatcher(dispatcher)`
|
||||
|
||||
* `dispatcher` {Dispatcher}
|
||||
|
||||
Sets the global dispatcher used by Common API Methods. Global dispatcher is shared among compatible undici modules,
|
||||
including undici that is bundled internally with node.js.
|
||||
|
||||
Undici stores this dispatcher under `Symbol.for('undici.globalDispatcher.2')`.
|
||||
|
||||
`setGlobalDispatcher()` also mirrors the configured dispatcher to
|
||||
`Symbol.for('undici.globalDispatcher.1')` using `Dispatcher1Wrapper`, so Node.js built-in `fetch`
|
||||
can keep using the legacy handler contract while Undici uses the new handler API.
|
||||
|
||||
### `undici.getGlobalDispatcher()`
|
||||
|
||||
Gets the global dispatcher used by Common API Methods.
|
||||
|
||||
* Returns: {Dispatcher}
|
||||
|
||||
### `undici.setGlobalOrigin(origin)`
|
||||
|
||||
* `origin` {string|URL|undefined}
|
||||
|
||||
Sets the global origin used in `fetch`.
|
||||
|
||||
If `undefined` is passed, the global origin will be reset. This will cause `Response.redirect`, `new Request()`, and `fetch` to throw an error when a relative path is passed.
|
||||
|
||||
```js
|
||||
setGlobalOrigin('http://localhost:3000')
|
||||
|
||||
const response = await fetch('/api/ping')
|
||||
|
||||
console.log(response.url) // http://localhost:3000/api/ping
|
||||
```
|
||||
|
||||
### `undici.getGlobalOrigin()`
|
||||
|
||||
Gets the global origin used in `fetch`.
|
||||
|
||||
* Returns: {URL}
|
||||
|
||||
### `UrlObject`
|
||||
|
||||
* `port` {string|number} (optional)
|
||||
* `path` {string} (optional)
|
||||
* `pathname` {string} (optional)
|
||||
* `hostname` {string} (optional)
|
||||
* `origin` {string} (optional)
|
||||
* `protocol` {string} (optional)
|
||||
* `search` {string} (optional)
|
||||
|
||||
#### Expect
|
||||
|
||||
Undici does not support the `Expect` request header field. The request
|
||||
body is always immediately sent and the `100 Continue` response will be
|
||||
ignored.
|
||||
|
||||
Refs: https://tools.ietf.org/html/rfc7231#section-5.1.1
|
||||
|
||||
#### Pipelining
|
||||
|
||||
Undici will only use pipelining if configured with a `pipelining` factor
|
||||
greater than `1`. Only enable pipelining when the remote server is trusted.
|
||||
Also it is important to pass `blocking: false` to the request options to
|
||||
properly pipeline requests.
|
||||
|
||||
Undici always assumes that connections are persistent and will immediately
|
||||
pipeline requests, without checking whether the connection is persistent.
|
||||
Hence, automatic fallback to HTTP/1.0 or HTTP/1.1 without pipelining is
|
||||
not supported.
|
||||
|
||||
Undici will immediately pipeline when retrying requests after a failed
|
||||
connection. However, Undici will not retry the first remaining requests in
|
||||
the prior pipeline and instead error the corresponding callback/promise/stream.
|
||||
|
||||
Undici will abort all running requests in the pipeline when any of them are
|
||||
aborted.
|
||||
|
||||
* Refs: https://tools.ietf.org/html/rfc2616#section-8.1.2.2
|
||||
* Refs: https://tools.ietf.org/html/rfc7230#section-6.3.2
|
||||
|
||||
#### Manual Redirect
|
||||
|
||||
Since it is not possible to manually follow an HTTP redirect on the server-side,
|
||||
Undici returns the actual response instead of an `opaqueredirect` filtered one
|
||||
when invoked with a `manual` redirect. This aligns `fetch()` with the other
|
||||
implementations in Deno and Cloudflare Workers.
|
||||
|
||||
Refs: https://fetch.spec.whatwg.org/#atomic-http-redirect-handling
|
||||
|
||||
### Workarounds
|
||||
|
||||
#### Network address family autoselection.
|
||||
|
||||
If you experience problem when connecting to a remote server that is resolved by your DNS servers to a IPv6 (AAAA record)
|
||||
first, there are chances that your local router or ISP might have problem connecting to IPv6 networks. In that case
|
||||
undici will throw an error with code `UND_ERR_CONNECT_TIMEOUT`.
|
||||
|
||||
If the target server resolves to both a IPv6 and IPv4 (A records) address and you are using a compatible Node version
|
||||
(18.3.0 and above), you can fix the problem by providing the `autoSelectFamily` option (support by both `undici.request`
|
||||
and `undici.Agent`) which will enable the family autoselection algorithm when establishing the connection.
|
||||
|
||||
## Collaborators
|
||||
|
||||
* [__Daniele Belardi__](https://github.com/dnlup), <https://www.npmjs.com/~dnlup>
|
||||
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
|
||||
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
|
||||
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
|
||||
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
|
||||
* [__Szymon Marczak__](https://github.com/szmarczak), <https://www.npmjs.com/~szmarczak>
|
||||
|
||||
## Past Collaborators
|
||||
* [__Tomas Della Vedova__](https://github.com/delvedor), <https://www.npmjs.com/~delvedor>
|
||||
|
||||
### Releasers
|
||||
|
||||
* [__Ethan Arrowood__](https://github.com/ethan-arrowood), <https://www.npmjs.com/~ethan_arrowood>
|
||||
* [__Matteo Collina__](https://github.com/mcollina), <https://www.npmjs.com/~matteo.collina>
|
||||
* [__Robert Nagy__](https://github.com/ronag), <https://www.npmjs.com/~ronag>
|
||||
* [__Matthew Aitken__](https://github.com/KhafraDev), <https://www.npmjs.com/~khaf>
|
||||
|
||||
## Long Term Support
|
||||
|
||||
Undici aligns with the Node.js LTS schedule. The following table shows the supported versions:
|
||||
|
||||
| Undici Version | Bundled in Node.js | Node.js Versions Supported | End of Life |
|
||||
|----------------|--------------------|----------------------------|-------------|
|
||||
| 5.x | 18.x | ≥14.0 (tested: 14, 16, 18) | 2024-04-30 |
|
||||
| 6.x | 20.x, 22.x | ≥18.17 (tested: 18, 20, 21, 22) | 2027-04-30 |
|
||||
| 7.x | 24.x | ≥20.18.1 (tested: 20, 22, 24) | 2028-04-30 |
|
||||
| 8.x | 26.x | ≥22.19.0 (tested: 22, 24, 26) | 2029-04-30 |
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
Vendored
+125
@@ -0,0 +1,125 @@
|
||||
{
|
||||
"sidebar": [
|
||||
{
|
||||
"groupName": "Documentation",
|
||||
"items": [
|
||||
{ "link": "/", "label": "Home" },
|
||||
{ "link": "/getting-started", "label": "Getting Started" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "API",
|
||||
"items": [
|
||||
{
|
||||
"label": "Core",
|
||||
"items": [
|
||||
{ "link": "/api/Dispatcher", "label": "Dispatcher" },
|
||||
{ "link": "/api/Client", "label": "Client" },
|
||||
{ "link": "/api/H2CClient", "label": "H2CClient" },
|
||||
{ "link": "/api/Pool", "label": "Pool" },
|
||||
{ "link": "/api/BalancedPool", "label": "BalancedPool" },
|
||||
{ "link": "/api/RoundRobinPool", "label": "RoundRobinPool" },
|
||||
{ "link": "/api/Agent", "label": "Agent" },
|
||||
{ "link": "/api/PoolStats", "label": "PoolStats" },
|
||||
{ "link": "/api/Connector", "label": "Connector" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Proxy & Agents",
|
||||
"items": [
|
||||
{ "link": "/api/ProxyAgent", "label": "ProxyAgent" },
|
||||
{
|
||||
"link": "/api/EnvHttpProxyAgent",
|
||||
"label": "EnvHttpProxyAgent"
|
||||
},
|
||||
{ "link": "/api/Socks5ProxyAgent", "label": "Socks5ProxyAgent" },
|
||||
{ "link": "/api/RetryAgent", "label": "RetryAgent" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Web Standards",
|
||||
"items": [
|
||||
{ "link": "/api/Fetch", "label": "Fetch" },
|
||||
{ "link": "/api/EventSource", "label": "EventSource" },
|
||||
{ "link": "/api/WebSocket", "label": "WebSocket" },
|
||||
{ "link": "/api/Cookies", "label": "Cookies" },
|
||||
{ "link": "/api/CacheStorage", "label": "CacheStorage" },
|
||||
{ "link": "/api/ContentType", "label": "MIME Type Parsing" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Mocking",
|
||||
"items": [
|
||||
{ "link": "/api/MockClient", "label": "MockClient" },
|
||||
{ "link": "/api/MockPool", "label": "MockPool" },
|
||||
{ "link": "/api/MockAgent", "label": "MockAgent" },
|
||||
{ "link": "/api/SnapshotAgent", "label": "SnapshotAgent" },
|
||||
{ "link": "/api/MockCallHistory", "label": "MockCallHistory" },
|
||||
{
|
||||
"link": "/api/MockCallHistoryLog",
|
||||
"label": "MockCallHistoryLog"
|
||||
},
|
||||
{ "link": "/api/MockErrors", "label": "MockErrors" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Interceptors",
|
||||
"items": [
|
||||
{ "link": "/api/Interceptors", "label": "Interceptors" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Handlers",
|
||||
"items": [
|
||||
{ "link": "/api/RedirectHandler", "label": "RedirectHandler" },
|
||||
{ "link": "/api/RetryHandler", "label": "RetryHandler" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"label": "Diagnostics & Utilities",
|
||||
"items": [
|
||||
{ "link": "/api/Errors", "label": "Errors" },
|
||||
{
|
||||
"link": "/api/DiagnosticsChannel",
|
||||
"label": "Diagnostics Channel Support"
|
||||
},
|
||||
{ "link": "/api/Debug", "label": "Debug" },
|
||||
{ "link": "/api/Util", "label": "Util" },
|
||||
{
|
||||
"link": "/api/GlobalInstallation",
|
||||
"label": "Global Installation"
|
||||
},
|
||||
{ "link": "/api/api-lifecycle", "label": "API Lifecycle" }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "Best Practices",
|
||||
"items": [
|
||||
{
|
||||
"link": "/best-practices/undici-vs-builtin-fetch",
|
||||
"label": "Undici vs. Built-in Fetch"
|
||||
},
|
||||
{
|
||||
"link": "/best-practices/migrating-from-v7-to-v8",
|
||||
"label": "Migrating from Undici 7 to 8"
|
||||
},
|
||||
{ "link": "/best-practices/proxy", "label": "Proxy" },
|
||||
{
|
||||
"link": "/best-practices/client-certificate",
|
||||
"label": "Client Certificate"
|
||||
},
|
||||
{
|
||||
"link": "/best-practices/writing-tests",
|
||||
"label": "Writing Tests"
|
||||
},
|
||||
{
|
||||
"link": "/best-practices/mocking-request",
|
||||
"label": "Mocking Request"
|
||||
},
|
||||
{ "link": "/best-practices/crawling", "label": "Crawling" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
{
|
||||
"AbortError": "/api/Errors.md#class-aborterror",
|
||||
"Agent": "/api/Agent.md#class-agent",
|
||||
"AgentDispatchOptions": "/api/Agent.md#agentdispatchoptions-handler",
|
||||
"AgentOptions": "/api/Agent.md#new-agentoptions",
|
||||
"BalancedPool": "/api/BalancedPool.md#class-balancedpool",
|
||||
"BalancedPoolOptions": "/api/BalancedPool.md#parameter-balancedpooloptions",
|
||||
"BodyMixin": "/api/Fetch.md#new-formdata",
|
||||
"Buffer": "https://nodejs.org/api/buffer.html#class-buffer",
|
||||
"BuildOptions": "/api/Connector.md#buildconnectoroptions",
|
||||
"CacheKey": "/api/CacheStore.md#cachekey",
|
||||
"CacheStore": "/api/CacheStore.md#new-memorycachestoreoptions",
|
||||
"CacheValue": "/api/CacheStore.md#cachevalue",
|
||||
"ClientDestroyedError": "/api/Errors.md#class-clientdestroyederror",
|
||||
"ClientOptions": "/api/Client.md#new-clienturl-options",
|
||||
"ClientStats": "/api/ClientStats.md#class-clientstats",
|
||||
"ConnectOptions": "/api/Dispatcher.md#class-dispatcher",
|
||||
"Cookie": "/api/Cookies.md#cookie",
|
||||
"DispatchController": "/api/Dispatcher.md#class-dispatcher",
|
||||
"DispatchHandler": "/api/Dispatcher.md#class-dispatcher",
|
||||
"DispatchOptions": "/api/Dispatcher.md#class-dispatcher",
|
||||
"Dispatcher": "/api/Dispatcher.md#class-dispatcher",
|
||||
"Duplex": "https://nodejs.org/api/stream.html#class-streamduplex",
|
||||
"EnvHttpProxyAgent": "/api/EnvHttpProxyAgent.md#class-envhttpproxyagent",
|
||||
"EnvHttpProxyAgentOptions": "/api/EnvHttpProxyAgent.md#new-envhttpproxyagentoptions",
|
||||
"EventEmitter": "https://nodejs.org/api/events.html#class-eventemitter",
|
||||
"GetResult": "/api/CacheStore.md#getresult",
|
||||
"H2CClientOptions": "/api/H2CClient.md#new-h2cclienturl-options",
|
||||
"HeadersInit": "https://developer.mozilla.org/docs/Web/API/Headers/Headers",
|
||||
"InvalidArgumentError": "/api/Errors.md#class-invalidargumenterror",
|
||||
"Iterable": "https://developer.mozilla.org/docs/Web/JavaScript/Reference/Iteration_protocols",
|
||||
"MaxOriginsReachedError": "/api/Errors.md#class-maxoriginsreachederror",
|
||||
"MockAgent": "/api/MockAgent.md#class-mockagent",
|
||||
"MockAgentOptions": "/api/MockAgent.md#parameter-mockagentoptions",
|
||||
"MockCallHistory": "/api/MockCallHistory.md#class-mockcallhistory",
|
||||
"MockCallHistoryLog": "/api/MockCallHistoryLog.md#class-mockcallhistorylog",
|
||||
"MockClient": "/api/MockClient.md#class-mockclient",
|
||||
"MockClientOptions": "/api/MockClient.md#parameter-mockclientoptions",
|
||||
"MockInterceptor": "/api/MockPool.md#return-mockinterceptor",
|
||||
"MockInterceptor.Options": "/api/MockPool.md#parameter-mockpoolinterceptoptions",
|
||||
"MockPool": "/api/MockPool.md#class-mockpool",
|
||||
"MockPoolInterceptOptions": "/api/MockPool.md#parameter-mockpoolinterceptoptions",
|
||||
"MockPoolOptions": "/api/MockPool.md#parameter-mockpooloptions",
|
||||
"MockResponseOptions": "/api/MockPool.md#parameter-mockresponseoptions",
|
||||
"MockScope": "/api/MockPool.md#return-mockscope",
|
||||
"PendingInterceptor": "/api/MockAgent.md#new-mockagentoptions",
|
||||
"PipelineOptions": "/api/Dispatcher.md#class-dispatcher",
|
||||
"Pool": "/api/Pool.md#class-pool",
|
||||
"PoolOptions": "/api/Pool.md#new-poolurl-options",
|
||||
"PoolStats": "/api/PoolStats.md#class-poolstats",
|
||||
"ProxyAgent": "/api/ProxyAgent.md#class-proxyagent",
|
||||
"ProxyAgentOptions": "/api/ProxyAgent.md#new-proxyagentoptions",
|
||||
"Readable": "https://nodejs.org/api/stream.html#class-streamreadable",
|
||||
"Record": "https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type",
|
||||
"RequestInit": "/api/Fetch.md#new-formdata",
|
||||
"RequestOptions": "/api/Dispatcher.md#class-dispatcher",
|
||||
"ResponseInit": "/api/Fetch.md#new-formdata",
|
||||
"RetryContext": "/api/RetryHandler.md#retrycontext",
|
||||
"RetryHandler": "/api/RetryHandler.md#class-retryhandler",
|
||||
"RetryHandlers": "/api/RetryHandler.md#parameter-retryhandlers",
|
||||
"RetryOptions": "/api/RetryHandler.md#retryoptions",
|
||||
"RetryState": "/api/RetryHandler.md#retrystate",
|
||||
"RoundRobinPoolOptions": "/api/RoundRobinPool.md#parameter-roundrobinpooloptions",
|
||||
"SnapshotRecorder": "/api/SnapshotAgent.md#new-snapshotagentoptions",
|
||||
"Socket": "https://nodejs.org/api/net.html#class-netsocket",
|
||||
"SocketInfo": "/api/Errors.md#new-requestretryerrormessage-code-options",
|
||||
"StreamOptions": "/api/Dispatcher.md#class-dispatcher",
|
||||
"TLSSocket": "https://nodejs.org/api/tls.html#class-tlstlssocket",
|
||||
"UndiciError": "/api/Errors.md#class-undicierror",
|
||||
"UndiciHeaders": "/api/Dispatcher.md#class-dispatcher",
|
||||
"UpgradeOptions": "/api/Dispatcher.md#class-dispatcher",
|
||||
"UrlObject": "/index.md#urlobject",
|
||||
"WebSocketInit": "/api/WebSocket.md#new-websocketurl-protocols",
|
||||
"WebSocketStreamOptions": "/api/WebSocket.md#new-websocketurl-protocols",
|
||||
"Writable": "https://nodejs.org/api/stream.html#class-streamwritable",
|
||||
"buildConnector.BuildOptions": "/api/Connector.md#buildconnectoroptions",
|
||||
"net.Socket": "https://nodejs.org/api/net.html#class-netsocket",
|
||||
"stream.Readable": "https://nodejs.org/api/stream.html#class-streamreadable"
|
||||
}
|
||||
+7
-1
@@ -66,7 +66,13 @@ class RequestHandler extends AsyncResource {
|
||||
this.removeAbortListener = util.addAbortListener(signal, () => {
|
||||
this.reason = signal.reason ?? new RequestAbortedError()
|
||||
if (this.res) {
|
||||
util.destroy(this.res.on('error', noop), this.reason)
|
||||
// Null the reference before destroying, mirroring onResponseError, so
|
||||
// that chunks flushed after the abort (e.g. an async decompressor
|
||||
// flush) are dropped by the `!this.res` guard in onResponseData
|
||||
// instead of being pushed into the torn-down stream.
|
||||
const res = this.res
|
||||
this.res = null
|
||||
util.destroy(res.on('error', noop), this.reason)
|
||||
} else if (this.abort) {
|
||||
this.abort(this.reason)
|
||||
}
|
||||
|
||||
Vendored
+47
-2
@@ -15,6 +15,7 @@ const kContentType = Symbol('kContentType')
|
||||
const kContentLength = Symbol('kContentLength')
|
||||
const kUsed = Symbol('kUsed')
|
||||
const kBytesRead = Symbol('kBytesRead')
|
||||
const kPreservedBuffer = Symbol('kPreservedBuffer')
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
@@ -325,7 +326,37 @@ class BodyReadable extends Readable {
|
||||
*/
|
||||
setEncoding (encoding) {
|
||||
if (Buffer.isEncoding(encoding)) {
|
||||
this._readableState.encoding = encoding
|
||||
// Preserve raw Buffer chunks for the consume path (body.text(),
|
||||
// body.json(), etc.) before super.setEncoding() replaces them
|
||||
// with decoded strings. Without this, the consume path would
|
||||
// lose access to the original bytes — some of which may be held
|
||||
// by the decoder for incomplete multi-byte sequences, and the
|
||||
// rest converted to strings that can't be safely concatenated
|
||||
// byte-wise.
|
||||
const state = this._readableState
|
||||
const buffer = state.buffer
|
||||
if (buffer && state.length > 0) {
|
||||
const bufferIndex = state.bufferIndex ?? 0
|
||||
const preserved = []
|
||||
const source = typeof buffer.slice === 'function'
|
||||
? buffer.slice(bufferIndex)
|
||||
: buffer
|
||||
for (const data of source) {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
preserved.push(data)
|
||||
}
|
||||
}
|
||||
if (preserved.length > 0) {
|
||||
this[kPreservedBuffer] = (this[kPreservedBuffer] || []).concat(preserved)
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate to Node.js Readable.setEncoding() which initializes a
|
||||
// StringDecoder and re-encodes already-buffered chunks. This properly
|
||||
// handles multi-byte sequences split at chunk boundaries for the
|
||||
// for-await / on('data') paths. Without this, Node.js uses
|
||||
// buf.toString(encoding) on each chunk, producing U+FFFD for split chars.
|
||||
super.setEncoding(encoding)
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -433,7 +464,17 @@ function consumeStart (consume) {
|
||||
|
||||
const { _readableState: state } = consume.stream
|
||||
|
||||
if (state.bufferIndex) {
|
||||
// If setEncoding() was called, state.buffer may contain decoded strings
|
||||
// (which would break Buffer.concat in chunksDecode). Use the preserved
|
||||
// raw Buffers (saved before super.setEncoding() in setEncoding()) for
|
||||
// byte-level accurate consumption. Otherwise read from state.buffer.
|
||||
const preserved = consume.stream[kPreservedBuffer]
|
||||
if (preserved && preserved.length > 0) {
|
||||
for (const chunk of preserved) {
|
||||
consumePush(consume, chunk)
|
||||
}
|
||||
consume.stream[kPreservedBuffer] = null
|
||||
} else if (state.bufferIndex) {
|
||||
const start = state.bufferIndex
|
||||
const end = state.buffer.length
|
||||
for (let n = start; n < end; n++) {
|
||||
@@ -546,6 +587,10 @@ function consumeEnd (consume, encoding) {
|
||||
* @returns {void}
|
||||
*/
|
||||
function consumePush (consume, chunk) {
|
||||
if (consume.body === null) {
|
||||
return
|
||||
}
|
||||
|
||||
consume.length += chunk.length
|
||||
consume.body.push(chunk)
|
||||
}
|
||||
|
||||
Vendored
+20
@@ -403,6 +403,25 @@ class SecureProxyConnectionError extends UndiciError {
|
||||
}
|
||||
}
|
||||
|
||||
const kProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_CONN')
|
||||
class ProxyConnectionError extends UndiciError {
|
||||
constructor (cause, message, options = {}) {
|
||||
super(message, { cause, ...options })
|
||||
this.name = 'ProxyConnectionError'
|
||||
this.message = message || 'Proxy Connection failed'
|
||||
this.code = 'UND_ERR_PRX_CONN'
|
||||
this.cause = cause
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance] (instance) {
|
||||
return instance && instance[kProxyConnectionError] === true
|
||||
}
|
||||
|
||||
get [kProxyConnectionError] () {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
const kMaxOriginsReachedError = Symbol.for('undici.error.UND_ERR_MAX_ORIGINS_REACHED')
|
||||
class MaxOriginsReachedError extends UndiciError {
|
||||
constructor (message) {
|
||||
@@ -471,6 +490,7 @@ module.exports = {
|
||||
RequestRetryError,
|
||||
ResponseError,
|
||||
SecureProxyConnectionError,
|
||||
ProxyConnectionError,
|
||||
MaxOriginsReachedError,
|
||||
Socks5ProxyError,
|
||||
MessageSizeExceededError
|
||||
|
||||
Vendored
+1
-1
@@ -225,7 +225,7 @@ class Request {
|
||||
this.protocol = getProtocolFromUrlString(origin)
|
||||
|
||||
this.idempotent = idempotent == null
|
||||
? method === 'HEAD' || method === 'GET'
|
||||
? method === 'HEAD' || method === 'GET' || method === 'QUERY'
|
||||
: idempotent
|
||||
|
||||
this.blocking = blocking ?? this.method !== 'HEAD'
|
||||
|
||||
Vendored
+24
-1
@@ -919,11 +919,32 @@ function onConnectTimeout (socket, opts) {
|
||||
destroy(socket, new ConnectTimeoutError(message))
|
||||
}
|
||||
|
||||
let lastUrlString = null
|
||||
let lastProtocol = null
|
||||
|
||||
/**
|
||||
* @param {string} urlString
|
||||
* @returns {string}
|
||||
*/
|
||||
function getProtocolFromUrlString (urlString) {
|
||||
// Requests are typically dispatched against the same origin over and over,
|
||||
// so cache the last (urlString, protocol) pair to skip re-parsing.
|
||||
if (urlString === lastUrlString) {
|
||||
return lastProtocol
|
||||
}
|
||||
|
||||
const protocol = getProtocolFromUrlStringSlow(urlString)
|
||||
lastUrlString = urlString
|
||||
lastProtocol = protocol
|
||||
|
||||
return protocol
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} urlString
|
||||
* @returns {string}
|
||||
*/
|
||||
function getProtocolFromUrlStringSlow (urlString) {
|
||||
if (
|
||||
urlString[0] === 'h' &&
|
||||
urlString[1] === 't' &&
|
||||
@@ -960,7 +981,9 @@ const normalizedMethodRecordsBase = {
|
||||
post: 'POST',
|
||||
POST: 'POST',
|
||||
put: 'PUT',
|
||||
PUT: 'PUT'
|
||||
PUT: 'PUT',
|
||||
query: 'QUERY',
|
||||
QUERY: 'QUERY'
|
||||
}
|
||||
|
||||
const normalizedMethodRecords = {
|
||||
|
||||
+23
@@ -377,6 +377,29 @@ class Parser {
|
||||
|
||||
const { llhttp } = this
|
||||
|
||||
// The peer closed the connection. If the body parser was paused by
|
||||
// backpressure we must finish parsing before signalling EOF, otherwise
|
||||
// llhttp_finish() would crash (it used to assert !paused) or report a
|
||||
// half-parsed message. Backpressure is advisory here: onData keeps buffering
|
||||
// delivered bytes into the response stream, so resume across pauses and
|
||||
// drain whatever is still buffered on the socket. A Content-Length/chunked
|
||||
// body reaches on_message_complete during execute(); an EOF-delimited body
|
||||
// stays paused (its length is unknown) and is completed by llhttp_finish().
|
||||
if (this.paused) {
|
||||
let data
|
||||
do {
|
||||
llhttp.llhttp_resume(this.ptr)
|
||||
this.paused = false
|
||||
data = this.socket.read() || EMPTY_BUF
|
||||
this.execute(data)
|
||||
} while (this.paused && data.length > 0)
|
||||
|
||||
if (this.paused) {
|
||||
llhttp.llhttp_resume(this.ptr)
|
||||
this.paused = false
|
||||
}
|
||||
}
|
||||
|
||||
let ret
|
||||
|
||||
try {
|
||||
|
||||
+243
-111
@@ -154,13 +154,23 @@ function requeueUnsentRequest (client, request) {
|
||||
}
|
||||
|
||||
function completeRequest (client, request, resetPendingIdx = false) {
|
||||
const index = client[kQueue].indexOf(request, client[kRunningIdx])
|
||||
const queue = client[kQueue]
|
||||
const runningIdx = client[kRunningIdx]
|
||||
|
||||
// In-order completion: advance the running index instead of splicing.
|
||||
// The client's resume loop compacts the queue once the index grows.
|
||||
if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
|
||||
client[kRunningIdx] = runningIdx + 1
|
||||
return
|
||||
}
|
||||
|
||||
const index = queue.indexOf(request, runningIdx)
|
||||
|
||||
if (index === -1 || index >= client[kPendingIdx]) {
|
||||
return
|
||||
}
|
||||
|
||||
client[kQueue].splice(index, 1)
|
||||
queue.splice(index, 1)
|
||||
client[kPendingIdx]--
|
||||
|
||||
if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
|
||||
@@ -174,11 +184,7 @@ function canRetryRequestAfterGoAway (request) {
|
||||
return body == null || util.isBuffer(body) || util.isBlobLike(body)
|
||||
}
|
||||
|
||||
function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
|
||||
const stream = request[kRequestStream]
|
||||
|
||||
clearRequestStream(request)
|
||||
|
||||
function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) {
|
||||
if (stream != null && !stream.destroyed && !stream.closed) {
|
||||
try {
|
||||
stream.close(code)
|
||||
@@ -186,6 +192,14 @@ function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) {
|
||||
}
|
||||
}
|
||||
|
||||
function detachRequestStreamForClose (request) {
|
||||
const stream = request[kRequestStream]
|
||||
|
||||
clearRequestStream(request)
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
function connectH2 (client, socket) {
|
||||
client[kSocket] = socket
|
||||
|
||||
@@ -208,6 +222,11 @@ function connectH2 (client, socket) {
|
||||
session[kSocket] = socket
|
||||
session[kHTTP2SessionState] = {
|
||||
idleTimeout: null,
|
||||
// Sockets start out ref'd. Session ref/unref proxies to the socket, so a
|
||||
// single cached flag lets us skip redundant uv ref/unref calls, provided
|
||||
// every ref/unref of the session or its socket goes through
|
||||
// refH2Session/unrefH2Session.
|
||||
refed: true,
|
||||
ping: {
|
||||
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
|
||||
}
|
||||
@@ -234,7 +253,7 @@ function connectH2 (client, socket) {
|
||||
// TODO (@metcoder95): implement SETTINGS support
|
||||
// util.addListener(session, 'localSettings', onHttp2RemoteSettings)
|
||||
|
||||
session.unref()
|
||||
unrefH2Session(session)
|
||||
|
||||
client[kHTTP2Session] = session
|
||||
socket[kHTTP2Session] = session
|
||||
@@ -323,17 +342,36 @@ function connectH2 (client, socket) {
|
||||
}
|
||||
}
|
||||
|
||||
// Session ref/unref proxies to the underlying socket, so refH2Session and
|
||||
// unrefH2Session cover both and can skip the call when the cached ref state
|
||||
// already matches.
|
||||
function refH2Session (session) {
|
||||
const state = session[kHTTP2SessionState]
|
||||
|
||||
if (state.refed === false) {
|
||||
state.refed = true
|
||||
session.ref()
|
||||
}
|
||||
}
|
||||
|
||||
function unrefH2Session (session) {
|
||||
const state = session[kHTTP2SessionState]
|
||||
|
||||
if (state.refed === true) {
|
||||
state.refed = false
|
||||
session.unref()
|
||||
}
|
||||
}
|
||||
|
||||
function resumeH2 (client) {
|
||||
const socket = client[kSocket]
|
||||
const session = client[kHTTP2Session]
|
||||
|
||||
if (socket?.destroyed === false) {
|
||||
if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
|
||||
socket.unref()
|
||||
session.unref()
|
||||
unrefH2Session(session)
|
||||
} else {
|
||||
socket.ref()
|
||||
session.ref()
|
||||
refH2Session(session)
|
||||
}
|
||||
|
||||
if (client[kSize] === 0 && session[kOpenStreams] === 0) {
|
||||
@@ -447,11 +485,20 @@ function onHttp2SessionError (err) {
|
||||
assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID')
|
||||
|
||||
this[kSocket][kError] = err
|
||||
|
||||
if (this[kReceivedGoAway]) {
|
||||
return
|
||||
}
|
||||
|
||||
this[kClient][kOnError](err)
|
||||
}
|
||||
|
||||
function onHttp2FrameError (type, code, id) {
|
||||
if (id === 0) {
|
||||
if (this[kReceivedGoAway]) {
|
||||
return
|
||||
}
|
||||
|
||||
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)
|
||||
this[kSocket][kError] = err
|
||||
this[kClient][kOnError](err)
|
||||
@@ -485,12 +532,16 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
||||
const previousPendingIdx = client[kPendingIdx]
|
||||
const pendingIdx = getGoAwayPendingIdx(client, lastStreamID)
|
||||
const retriableRequests = []
|
||||
const streamsToClose = []
|
||||
|
||||
// Closing one stream after GOAWAY can synchronously emit frameError on
|
||||
// sibling streams. Detach all affected requests first so those errors do
|
||||
// not fail requests that are about to be requeued.
|
||||
for (let i = pendingIdx; i < previousPendingIdx; i++) {
|
||||
const request = client[kQueue][i]
|
||||
|
||||
if (request != null) {
|
||||
closeRequestStream(request)
|
||||
streamsToClose.push(detachRequestStreamForClose(request))
|
||||
|
||||
if (canRetryRequestAfterGoAway(request)) {
|
||||
retriableRequests.push(request)
|
||||
@@ -500,6 +551,10 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) {
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < streamsToClose.length; i++) {
|
||||
closeStream(streamsToClose[i])
|
||||
}
|
||||
|
||||
if (pendingIdx !== previousPendingIdx) {
|
||||
const remainingPendingRequests = client[kQueue].slice(previousPendingIdx)
|
||||
client[kQueue].length = pendingIdx
|
||||
@@ -595,6 +650,10 @@ function onHttp2SocketError (err) {
|
||||
|
||||
this[kError] = err
|
||||
|
||||
if (this[kHTTP2Session]?.[kReceivedGoAway]) {
|
||||
return
|
||||
}
|
||||
|
||||
this[kClient][kOnError](err)
|
||||
}
|
||||
|
||||
@@ -614,7 +673,7 @@ function closeStreamSession (stream) {
|
||||
stream[kHTTP2Session] = null
|
||||
session[kOpenStreams] -= 1
|
||||
if (session[kOpenStreams] === 0) {
|
||||
session.unref()
|
||||
unrefH2Session(session)
|
||||
setHttp2IdleTimeout(session)
|
||||
}
|
||||
}
|
||||
@@ -639,12 +698,10 @@ function onRequestStreamClose () {
|
||||
|
||||
if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
|
||||
state.request.onResponseEnd(state.trailers || {})
|
||||
state.finalizeRequest()
|
||||
finalizeRequest(state)
|
||||
}
|
||||
}
|
||||
|
||||
this.off('data', onData)
|
||||
this.off('error', noop)
|
||||
closeStreamSession(this)
|
||||
this[kRequestStreamState] = null
|
||||
}
|
||||
@@ -767,7 +824,7 @@ function onUpgradeResponse (headers, _flags) {
|
||||
|
||||
removeUpgradeStreamListeners(stream)
|
||||
detachRequestFromStream(request)
|
||||
state.finalizeRequest()
|
||||
finalizeRequest(state)
|
||||
}
|
||||
|
||||
function setupUpgradeStream (stream, state) {
|
||||
@@ -790,12 +847,51 @@ function setupUpgradeStream (stream, state) {
|
||||
stream.setTimeout(headersTimeout)
|
||||
}
|
||||
|
||||
function finalizeRequest (state, resetPendingIdx = false) {
|
||||
if (state.requestFinalized) {
|
||||
return
|
||||
}
|
||||
|
||||
state.requestFinalized = true
|
||||
completeRequest(state.client, state.request, resetPendingIdx)
|
||||
|
||||
state.client[kResume]()
|
||||
}
|
||||
|
||||
function openStream (client, request, session, abort, headers, options) {
|
||||
try {
|
||||
return session.request(headers, options)
|
||||
} catch (err) {
|
||||
// A GOAWAY'd session rejects new streams, same as an invalid session:
|
||||
// reset and requeue on a fresh connection rather than the destroy + abort
|
||||
// below, whose destroy(socket, err) can crash via an unhandled 'error'.
|
||||
if (err?.code === 'ERR_HTTP2_INVALID_SESSION' || err?.code === 'ERR_HTTP2_GOAWAY_SESSION') {
|
||||
const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
|
||||
wrappedErr.cause = err
|
||||
session[kError] = wrappedErr
|
||||
resetHttp2Session(session, wrappedErr)
|
||||
requeueUnsentRequest(client, request)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const wrappedErr = new InformationalError(err.message, { cause: err })
|
||||
session[kError] = wrappedErr
|
||||
session[kSocket][kError] = wrappedErr
|
||||
|
||||
session.destroy(wrappedErr)
|
||||
util.destroy(session[kSocket], wrappedErr)
|
||||
abort(wrappedErr)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function writeH2 (client, request) {
|
||||
const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
|
||||
const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
|
||||
const session = client[kHTTP2Session]
|
||||
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
|
||||
let { body } = request
|
||||
|
||||
if (upgrade != null && upgrade !== 'websocket') {
|
||||
util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`))
|
||||
@@ -804,22 +900,28 @@ function writeH2 (client, request) {
|
||||
|
||||
const headers = buildRequestHeaders(reqHeaders)
|
||||
|
||||
/** @type {import('node:http2').ClientHttp2Stream} */
|
||||
let stream = null
|
||||
|
||||
headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority]
|
||||
headers[HTTP2_HEADER_METHOD] = method
|
||||
|
||||
let requestFinalized = false
|
||||
const finalizeRequest = (resetPendingIdx = false) => {
|
||||
if (requestFinalized) {
|
||||
return
|
||||
}
|
||||
|
||||
requestFinalized = true
|
||||
completeRequest(client, request, resetPendingIdx)
|
||||
|
||||
client[kResume]()
|
||||
// Single pre-shaped state object shared by all stream event handlers.
|
||||
// All fields are declared up-front so the object keeps a stable hidden
|
||||
// class for the whole request lifetime.
|
||||
const state = {
|
||||
abort: null,
|
||||
body: request.body,
|
||||
client,
|
||||
contentLength: null,
|
||||
expectsPayload: false,
|
||||
request,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
requestFinalized: false,
|
||||
responseReceived: false,
|
||||
bodySent: false,
|
||||
pendingEnd: false,
|
||||
trailers: null,
|
||||
session,
|
||||
stream: null
|
||||
}
|
||||
|
||||
const abort = (err, resetPendingIdx = false) => {
|
||||
@@ -831,47 +933,39 @@ function writeH2 (client, request) {
|
||||
|
||||
util.errorRequest(client, request, err)
|
||||
|
||||
if (stream != null) {
|
||||
if (state.stream != null) {
|
||||
clearRequestStream(request)
|
||||
|
||||
// On Abort, we close the stream to send RST_STREAM frame
|
||||
// On Abort, we close the stream to send RST_STREAM frame.
|
||||
const stream = state.stream
|
||||
stream.close()
|
||||
|
||||
// close() alone leaves cleanup waiting on the 'close' event; on a busy,
|
||||
// long-lived multiplexed session that event can fail to fire, leaving the
|
||||
// native Http2Stream (and the whole request graph it pins) alive for the
|
||||
// session's life. Destroy the stream to release the handle
|
||||
// deterministically, but defer it by a setImmediate so the RST_STREAM
|
||||
// frame queued by close() gets a chance to be written first.
|
||||
setImmediate(() => {
|
||||
if (!stream.destroyed) {
|
||||
util.destroy(stream)
|
||||
}
|
||||
})
|
||||
|
||||
// We move the running index to the next request
|
||||
client[kOnError](err)
|
||||
finalizeRequest(resetPendingIdx)
|
||||
finalizeRequest(state, resetPendingIdx)
|
||||
}
|
||||
|
||||
// We do not destroy the socket as we can continue using the session
|
||||
// the stream gets destroyed and the session remains to create new streams
|
||||
util.destroy(body, err)
|
||||
util.destroy(state.body, err)
|
||||
}
|
||||
|
||||
const requestStream = (headers, options) => {
|
||||
try {
|
||||
return session.request(headers, options)
|
||||
} catch (err) {
|
||||
if (err?.code === 'ERR_HTTP2_INVALID_SESSION') {
|
||||
const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket]))
|
||||
wrappedErr.cause = err
|
||||
session[kError] = wrappedErr
|
||||
resetHttp2Session(session, wrappedErr)
|
||||
requeueUnsentRequest(client, request)
|
||||
state.abort = abort
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
const wrappedErr = new InformationalError(err.message, { cause: err })
|
||||
session[kError] = wrappedErr
|
||||
session[kSocket][kError] = wrappedErr
|
||||
|
||||
session.destroy(wrappedErr)
|
||||
util.destroy(session[kSocket], wrappedErr)
|
||||
abort(wrappedErr)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
/** @type {import('node:http2').ClientHttp2Stream} */
|
||||
let stream = null
|
||||
|
||||
try {
|
||||
// We are already connected, streams are pending.
|
||||
@@ -886,24 +980,13 @@ function writeH2 (client, request) {
|
||||
}
|
||||
|
||||
if (upgrade || method === 'CONNECT') {
|
||||
session.ref()
|
||||
|
||||
const upgradeState = {
|
||||
abort,
|
||||
finalizeRequest,
|
||||
request,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
responseReceived: false,
|
||||
session,
|
||||
stream: null
|
||||
}
|
||||
refH2Session(session)
|
||||
|
||||
if (upgrade === 'websocket') {
|
||||
// We cannot upgrade to websocket if extended CONNECT protocol is not supported
|
||||
if (session[kEnableConnectProtocol] === false) {
|
||||
util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server'))
|
||||
session.unref()
|
||||
unrefH2Session(session)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -921,12 +1004,12 @@ function writeH2 (client, request) {
|
||||
headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https'
|
||||
}
|
||||
|
||||
stream = requestStream(headers, { endStream: false, signal })
|
||||
stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
|
||||
if (stream == null) {
|
||||
session.unref()
|
||||
unrefH2Session(session)
|
||||
return false
|
||||
}
|
||||
setupUpgradeStream(stream, upgradeState)
|
||||
setupUpgradeStream(stream, state)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -935,12 +1018,12 @@ function writeH2 (client, request) {
|
||||
// will create a new stream. We trigger a request to create the stream and wait until
|
||||
// `ready` event is triggered
|
||||
// We disabled endStream to allow the user to write to the stream
|
||||
stream = requestStream(headers, { endStream: false, signal })
|
||||
stream = openStream(client, request, session, abort, headers, { endStream: false, signal })
|
||||
if (stream == null) {
|
||||
session.unref()
|
||||
unrefH2Session(session)
|
||||
return false
|
||||
}
|
||||
setupUpgradeStream(stream, upgradeState)
|
||||
setupUpgradeStream(stream, state)
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -968,6 +1051,8 @@ function writeH2 (client, request) {
|
||||
method === 'PROPPATCH'
|
||||
)
|
||||
|
||||
let body = state.body
|
||||
|
||||
if (body && typeof body.read === 'function') {
|
||||
// Try to read EOF in order to get length.
|
||||
body.read(0)
|
||||
@@ -1014,7 +1099,7 @@ function writeH2 (client, request) {
|
||||
headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`
|
||||
}
|
||||
|
||||
session.ref()
|
||||
refH2Session(session)
|
||||
|
||||
if (channels.sendHeaders.hasSubscribers) {
|
||||
let header = ''
|
||||
@@ -1026,26 +1111,16 @@ function writeH2 (client, request) {
|
||||
|
||||
// TODO(metcoder95): add support for sending trailers
|
||||
const shouldEndStream = body === null || contentLength === 0
|
||||
const state = {
|
||||
abort,
|
||||
body,
|
||||
client,
|
||||
contentLength,
|
||||
expectsPayload,
|
||||
finalizeRequest,
|
||||
request,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
responseReceived: false,
|
||||
session,
|
||||
stream: null
|
||||
}
|
||||
|
||||
state.body = body
|
||||
state.contentLength = contentLength
|
||||
state.expectsPayload = expectsPayload
|
||||
|
||||
if (expectContinue) {
|
||||
headers[HTTP2_HEADER_EXPECT] = '100-continue'
|
||||
}
|
||||
|
||||
stream = requestStream(headers, { endStream: shouldEndStream, signal })
|
||||
stream = openStream(client, request, session, abort, headers, { endStream: shouldEndStream, signal })
|
||||
if (stream == null) {
|
||||
return false
|
||||
}
|
||||
@@ -1056,22 +1131,30 @@ function writeH2 (client, request) {
|
||||
// Increment counter as we have new streams open
|
||||
clearHttp2IdleTimeout(session)
|
||||
++session[kOpenStreams]
|
||||
stream.setTimeout(headersTimeout)
|
||||
|
||||
if (headersTimeout) {
|
||||
stream.setTimeout(headersTimeout)
|
||||
}
|
||||
|
||||
stream[kHTTP2Session] = session
|
||||
stream.once('close', onRequestStreamClose)
|
||||
stream.on('close', onRequestStreamClose)
|
||||
|
||||
bindRequestToStream(request, stream, releaseRequestStream)
|
||||
if (expectContinue) {
|
||||
stream.once('continue', writeBodyH2)
|
||||
}
|
||||
stream.once('response', onResponse)
|
||||
stream.once('end', onEnd)
|
||||
stream.once('error', onError)
|
||||
stream.once('frameError', onFrameError)
|
||||
// The handlers below either remove themselves on first invocation or
|
||||
// become unreachable once the stream closes, so plain `on` avoids the
|
||||
// per-listener `once` wrapper allocation.
|
||||
stream.on('response', onResponse)
|
||||
stream.on('end', onEnd)
|
||||
stream.on('error', onError)
|
||||
stream.on('frameError', onFrameError)
|
||||
stream.on('aborted', onAborted)
|
||||
stream.on('timeout', onTimeout)
|
||||
stream.once('trailers', onTrailers)
|
||||
if (headersTimeout || bodyTimeout) {
|
||||
stream.on('timeout', onTimeout)
|
||||
}
|
||||
stream.on('trailers', onTrailers)
|
||||
|
||||
if (!expectContinue) {
|
||||
writeBodyH2.call(stream)
|
||||
@@ -1109,16 +1192,24 @@ function releaseRequestStream (stream) {
|
||||
detachRequestFromStream(request)
|
||||
}
|
||||
|
||||
removeRequestStreamListeners(stream)
|
||||
|
||||
// A closed or destroyed stream cannot emit further events; leaving the
|
||||
// listeners in place saves the removal scans (they are collected with
|
||||
// the stream). All handlers bail out when the stream state is gone.
|
||||
if (!stream.destroyed && !stream.closed) {
|
||||
removeRequestStreamListeners(stream)
|
||||
stream.once('error', noop)
|
||||
}
|
||||
}
|
||||
|
||||
function onData (chunk) {
|
||||
const stream = this
|
||||
const { request } = stream[kRequestStreamState]
|
||||
const state = stream[kRequestStreamState]
|
||||
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { request } = state
|
||||
|
||||
if (request.aborted || request.completed) {
|
||||
return
|
||||
@@ -1132,22 +1223,40 @@ function onData (chunk) {
|
||||
function onResponse (headers) {
|
||||
const stream = this
|
||||
const state = stream[kRequestStreamState]
|
||||
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { request } = state
|
||||
|
||||
stream.off('response', onResponse)
|
||||
|
||||
// Final response received while still awaiting 100 (Continue): the body won't
|
||||
// be sent, so close our half or the stream stays open and never completes.
|
||||
if (state.body != null && !state.bodySent && !stream.writableEnded) {
|
||||
stream.removeListener('continue', writeBodyH2)
|
||||
stream.end()
|
||||
}
|
||||
|
||||
const statusCode = headers[HTTP2_HEADER_STATUS]
|
||||
delete headers[HTTP2_HEADER_STATUS]
|
||||
request.onResponseStarted()
|
||||
state.responseReceived = true
|
||||
stream.setTimeout(state.bodyTimeout)
|
||||
|
||||
if (state.headersTimeout || state.bodyTimeout) {
|
||||
stream.setTimeout(state.bodyTimeout)
|
||||
}
|
||||
|
||||
// Due to the stream nature, it is possible we face a race condition
|
||||
// where the stream has been assigned, but the request has been aborted
|
||||
// the request remains in-flight and headers hasn't been received yet
|
||||
// for those scenarios, best effort is to destroy the stream immediately
|
||||
// as there's no value to keep it open.
|
||||
if (request.aborted) {
|
||||
// or already completed and headers hasn't been received yet. A late
|
||||
// 'response' delivered after completion would call request.onResponseStart
|
||||
// post-completion, tripping its `assert(!this.completed)` (an uncatchable
|
||||
// throw on the http2 event tick). Guard `completed` here as onEnd/onTrailers
|
||||
// already do; best effort is to release the stream immediately as there's
|
||||
// no value to keep it open.
|
||||
if (request.aborted || request.completed) {
|
||||
releaseRequestStream(stream)
|
||||
return
|
||||
}
|
||||
@@ -1162,6 +1271,11 @@ function onResponse (headers) {
|
||||
function onEnd () {
|
||||
const stream = this
|
||||
const state = stream[kRequestStreamState]
|
||||
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { request } = state
|
||||
|
||||
stream.off('end', onEnd)
|
||||
@@ -1185,6 +1299,10 @@ function onError (err) {
|
||||
const stream = this
|
||||
const state = stream[kRequestStreamState]
|
||||
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
stream.off('error', onError)
|
||||
state.abort(err)
|
||||
}
|
||||
@@ -1193,6 +1311,10 @@ function onFrameError (type, code) {
|
||||
const stream = this
|
||||
const state = stream[kRequestStreamState]
|
||||
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
stream.off('frameError', onFrameError)
|
||||
state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`))
|
||||
}
|
||||
@@ -1205,6 +1327,10 @@ function onTimeout () {
|
||||
const stream = this
|
||||
const state = stream[kRequestStreamState]
|
||||
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Remove self so timeout doesn't fire again after we handle it
|
||||
stream.off('timeout', onTimeout)
|
||||
|
||||
@@ -1217,6 +1343,11 @@ function onTimeout () {
|
||||
function onTrailers (trailers) {
|
||||
const stream = this
|
||||
const state = stream[kRequestStreamState]
|
||||
|
||||
if (state == null) {
|
||||
return
|
||||
}
|
||||
|
||||
const { request } = state
|
||||
|
||||
stream.off('trailers', onTrailers)
|
||||
@@ -1233,6 +1364,7 @@ function onTrailers (trailers) {
|
||||
function writeBodyH2 () {
|
||||
const stream = this
|
||||
const state = stream[kRequestStreamState]
|
||||
state.bodySent = true
|
||||
const { abort, body, client, contentLength, expectsPayload, request } = state
|
||||
|
||||
if (!body || contentLength === 0) {
|
||||
|
||||
+40
-6
@@ -4,7 +4,7 @@ const { kProxy, kClose, kDestroy, kDispatch } = require('../core/symbols')
|
||||
const Agent = require('./agent')
|
||||
const Pool = require('./pool')
|
||||
const DispatcherBase = require('./dispatcher-base')
|
||||
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
|
||||
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError, ProxyConnectionError } = require('../core/errors')
|
||||
const buildConnector = require('../core/connect')
|
||||
const Client = require('./client')
|
||||
const { channels } = require('../core/diagnostics')
|
||||
@@ -37,10 +37,15 @@ function defaultAgentFactory (origin, opts) {
|
||||
return new Pool(origin, opts)
|
||||
}
|
||||
|
||||
function shouldProxyTunnel (requestProtocol, proxyTunnel) {
|
||||
return proxyTunnel === true || requestProtocol !== 'http:'
|
||||
}
|
||||
|
||||
class Http1ProxyWrapper extends DispatcherBase {
|
||||
#client
|
||||
#proxyServername
|
||||
|
||||
constructor (proxyUrl, { headers = {}, connect, factory }) {
|
||||
constructor (proxyUrl, { headers = {}, connect, factory, proxyServername }) {
|
||||
if (!proxyUrl) {
|
||||
throw new InvalidArgumentError('Proxy URL is mandatory')
|
||||
}
|
||||
@@ -48,6 +53,7 @@ class Http1ProxyWrapper extends DispatcherBase {
|
||||
super()
|
||||
|
||||
this[kProxyHeaders] = headers
|
||||
this.#proxyServername = proxyServername
|
||||
if (factory) {
|
||||
this.#client = factory(proxyUrl, { connect })
|
||||
} else {
|
||||
@@ -82,6 +88,13 @@ class Http1ProxyWrapper extends DispatcherBase {
|
||||
}
|
||||
opts.headers = { ...this[kProxyHeaders], ...headers }
|
||||
|
||||
// Pin the SNI/cert hostname to the proxy. Without this the underlying
|
||||
// Client would derive it from the (rewritten) Host header, which points
|
||||
// at the target — wrong for the TLS handshake to the proxy itself.
|
||||
if (this.#proxyServername != null) {
|
||||
opts.servername = this.#proxyServername
|
||||
}
|
||||
|
||||
return this.#client[kDispatch](opts, handler)
|
||||
}
|
||||
|
||||
@@ -105,7 +118,7 @@ class ProxyAgent extends DispatcherBase {
|
||||
throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.')
|
||||
}
|
||||
|
||||
const { proxyTunnel = true, connectTimeout } = opts
|
||||
const { proxyTunnel, connectTimeout } = opts
|
||||
|
||||
super()
|
||||
|
||||
@@ -132,6 +145,7 @@ class ProxyAgent extends DispatcherBase {
|
||||
}
|
||||
|
||||
const connect = buildConnector({ timeout: connectTimeout, ...opts.proxyTls })
|
||||
const connectHTTP1 = buildConnector({ timeout: connectTimeout, ...opts.proxyTls, allowH2: false })
|
||||
this[kConnectEndpoint] = buildConnector({ timeout: connectTimeout, ...opts.requestTls })
|
||||
this[kConnectEndpointHTTP1] = buildConnector({ timeout: connectTimeout, ...opts.requestTls, allowH2: false })
|
||||
|
||||
@@ -152,11 +166,23 @@ class ProxyAgent extends DispatcherBase {
|
||||
})
|
||||
}
|
||||
|
||||
if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') {
|
||||
if (!shouldProxyTunnel(protocol, this[kTunnelProxy])) {
|
||||
const forwardConnect = this[kProxy].protocol === 'https:'
|
||||
? (opts, cb) => connectHTTP1(opts, (err, socket) => {
|
||||
if (err && err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
|
||||
cb(new SecureProxyConnectionError(err))
|
||||
} else {
|
||||
cb(err, socket)
|
||||
}
|
||||
})
|
||||
: connectHTTP1
|
||||
return new Http1ProxyWrapper(this[kProxy].uri, {
|
||||
headers: this[kProxyHeaders],
|
||||
connect,
|
||||
factory: agentFactory
|
||||
connect: forwardConnect,
|
||||
factory: agentFactory,
|
||||
proxyServername: this[kProxy].protocol === 'https:'
|
||||
? (this[kProxyTls]?.servername || proxyHostname)
|
||||
: undefined
|
||||
})
|
||||
}
|
||||
return agentFactory(origin, options)
|
||||
@@ -231,6 +257,14 @@ class ProxyAgent extends DispatcherBase {
|
||||
if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
|
||||
// Throw a custom error to avoid loop in client.js#connect
|
||||
callback(new SecureProxyConnectionError(err))
|
||||
} else if (err.code === 'UND_ERR_SOCKET') {
|
||||
// A socket failure while establishing the tunnel means the CONNECT
|
||||
// never completed, so there is nothing to recover - the proxy just
|
||||
// tore down the connection. client.js#onError treats UND_ERR_SOCKET
|
||||
// as a recoverable error on an established connection and leaves the
|
||||
// request queued, which makes connect() retry forever. Surface it as
|
||||
// a non-recoverable proxy error so the request fails instead. (#3897)
|
||||
callback(new ProxyConnectionError(err))
|
||||
} else {
|
||||
callback(err)
|
||||
}
|
||||
|
||||
@@ -55,6 +55,7 @@ class RedirectHandler {
|
||||
// https://tools.ietf.org/html/rfc7231#section-6.4.2
|
||||
// https://fetch.spec.whatwg.org/#http-redirect-fetch
|
||||
// In case of HTTP 301 or 302 with POST, change the method to GET
|
||||
// QUERY is safe (RFC 10008) and should not change method like GET.
|
||||
if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') {
|
||||
this.opts.method = 'GET'
|
||||
if (util.isStream(this.opts.body)) {
|
||||
|
||||
+57
-19
@@ -14,6 +14,28 @@ function calculateRetryAfterHeader (retryAfter) {
|
||||
return isNaN(retryTime) ? 0 : retryTime - Date.now()
|
||||
}
|
||||
|
||||
// A stable controller handed to the downstream handler for the lifetime of the
|
||||
// request. Each transparent retry/resume is a *separate* dispatch with its
|
||||
// *own* connection controller. Without a stable proxy the downstream body keeps
|
||||
// flow-controlling the original (now-dead) controller while data flows on the
|
||||
// new one: backpressure pauses the new connection's controller, but the
|
||||
// consumer's resume() targets the old one, so the resumed body stalls forever.
|
||||
// The proxy always forwards to the controller of the currently active connection.
|
||||
class RetryController {
|
||||
constructor () {
|
||||
this.target = null
|
||||
}
|
||||
|
||||
pause () { this.target?.pause() }
|
||||
resume () { this.target?.resume() }
|
||||
abort (reason) { this.target?.abort(reason) }
|
||||
get paused () { return this.target?.paused ?? false }
|
||||
get aborted () { return this.target?.aborted ?? false }
|
||||
get reason () { return this.target?.reason ?? null }
|
||||
get rawHeaders () { return this.target?.rawHeaders ?? null }
|
||||
get rawTrailers () { return this.target?.rawTrailers ?? null }
|
||||
}
|
||||
|
||||
class RetryHandler {
|
||||
constructor (opts, { dispatch, handler }) {
|
||||
const { retryOptions, ...dispatchOpts } = opts
|
||||
@@ -45,7 +67,7 @@ class RetryHandler {
|
||||
timeoutFactor: timeoutFactor ?? 2,
|
||||
maxRetries: maxRetries ?? 5,
|
||||
// What errors we should retry
|
||||
methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'],
|
||||
methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'QUERY'],
|
||||
// Indicates which errors to retry
|
||||
statusCodes: statusCodes ?? [500, 502, 503, 504, 429],
|
||||
// List of errors to retry
|
||||
@@ -70,6 +92,7 @@ class RetryHandler {
|
||||
this.etag = null
|
||||
this.statusCode = null
|
||||
this.headers = null
|
||||
this.controllerProxy = new RetryController()
|
||||
}
|
||||
|
||||
onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
|
||||
@@ -77,7 +100,7 @@ class RetryHandler {
|
||||
// Preserve old behavior for status codes that are not eligible for retry
|
||||
if (this.retryOpts.statusCodes.includes(statusCode) === false) {
|
||||
this.headersSent = true
|
||||
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
||||
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
||||
} else {
|
||||
this.error = err
|
||||
}
|
||||
@@ -87,14 +110,14 @@ class RetryHandler {
|
||||
|
||||
if (isDisturbed(this.opts.body)) {
|
||||
this.headersSent = true
|
||||
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
||||
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
||||
return
|
||||
}
|
||||
|
||||
function shouldRetry (passedErr) {
|
||||
if (passedErr) {
|
||||
this.headersSent = true
|
||||
this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
|
||||
this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
|
||||
controller.resume()
|
||||
return
|
||||
}
|
||||
@@ -103,6 +126,13 @@ class RetryHandler {
|
||||
controller.resume()
|
||||
}
|
||||
|
||||
// The pause()/resume() pair (here and in shouldRetry) acts on THIS
|
||||
// connection's controller -- never the downstream proxy. We hold this exact
|
||||
// connection while the retry policy decides (possibly after a timeout) and
|
||||
// must resume the same one. Routing this through controllerProxy would risk
|
||||
// resuming a different connection if a later dispatch re-points the proxy in
|
||||
// between, leaving this one paused forever -- the very stall the proxy exists
|
||||
// to prevent.
|
||||
controller.pause()
|
||||
this.retryOpts.retry(
|
||||
err,
|
||||
@@ -115,13 +145,19 @@ class RetryHandler {
|
||||
}
|
||||
|
||||
onRequestStart (controller, context) {
|
||||
// request.js creates a fresh RequestController per dispatch and passes that
|
||||
// same instance to every later callback of the dispatch. onRequestStart is
|
||||
// the first callback (it is where the controller is created), so re-pointing
|
||||
// the proxy here is enough to keep it on the active connection across every
|
||||
// transparent retry/resume.
|
||||
this.controllerProxy.target = controller
|
||||
if (!this.headersSent) {
|
||||
this.handler.onRequestStart?.(controller, context)
|
||||
this.handler.onRequestStart?.(this.controllerProxy, context)
|
||||
}
|
||||
}
|
||||
|
||||
onRequestUpgrade (controller, statusCode, headers, socket) {
|
||||
this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
|
||||
onRequestUpgrade (_controller, statusCode, headers, socket) {
|
||||
this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket)
|
||||
}
|
||||
|
||||
static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
|
||||
@@ -248,7 +284,7 @@ class RetryHandler {
|
||||
if (range == null) {
|
||||
this.headersSent = true
|
||||
this.handler.onResponseStart?.(
|
||||
controller,
|
||||
this.controllerProxy,
|
||||
statusCode,
|
||||
headers,
|
||||
statusMessage
|
||||
@@ -295,7 +331,7 @@ class RetryHandler {
|
||||
|
||||
this.headersSent = true
|
||||
this.handler.onResponseStart?.(
|
||||
controller,
|
||||
this.controllerProxy,
|
||||
statusCode,
|
||||
headers,
|
||||
statusMessage
|
||||
@@ -308,17 +344,17 @@ class RetryHandler {
|
||||
}
|
||||
}
|
||||
|
||||
onResponseData (controller, chunk) {
|
||||
onResponseData (_controller, chunk) {
|
||||
if (this.error) {
|
||||
return
|
||||
}
|
||||
|
||||
this.start += chunk.length
|
||||
|
||||
this.handler.onResponseData?.(controller, chunk)
|
||||
this.handler.onResponseData?.(this.controllerProxy, chunk)
|
||||
}
|
||||
|
||||
onResponseEnd (controller, trailers) {
|
||||
onResponseEnd (_controller, trailers) {
|
||||
if (this.error && this.retryOpts.throwOnError) {
|
||||
throw this.error
|
||||
}
|
||||
@@ -335,13 +371,13 @@ class RetryHandler {
|
||||
}
|
||||
}
|
||||
this.retryCount = 0
|
||||
return this.handler.onResponseEnd?.(controller, trailers)
|
||||
return this.handler.onResponseEnd?.(this.controllerProxy, trailers)
|
||||
}
|
||||
|
||||
this.retry(controller)
|
||||
this.retry()
|
||||
}
|
||||
|
||||
retry (controller) {
|
||||
retry () {
|
||||
if (this.start !== 0) {
|
||||
const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
|
||||
|
||||
@@ -363,23 +399,25 @@ class RetryHandler {
|
||||
this.retryCountCheckpoint = this.retryCount
|
||||
this.dispatch(this.opts, this)
|
||||
} catch (err) {
|
||||
this.handler.onResponseError?.(controller, err)
|
||||
this.handler.onResponseError?.(this.controllerProxy, err)
|
||||
}
|
||||
}
|
||||
|
||||
onResponseError (controller, err) {
|
||||
// controller is THIS failed connection (not the proxy): we inspect whether
|
||||
// the consumer aborted it to decide retry-vs-propagate.
|
||||
if (controller?.aborted || isDisturbed(this.opts.body)) {
|
||||
this.handler.onResponseError?.(controller, err)
|
||||
this.handler.onResponseError?.(this.controllerProxy, err)
|
||||
return
|
||||
}
|
||||
|
||||
function shouldRetry (returnedErr) {
|
||||
if (!returnedErr) {
|
||||
this.retry(controller)
|
||||
this.retry()
|
||||
return
|
||||
}
|
||||
|
||||
this.handler?.onResponseError?.(controller, returnedErr)
|
||||
this.handler?.onResponseError?.(this.controllerProxy, returnedErr)
|
||||
}
|
||||
|
||||
// We reconcile in case of a mix between network errors
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
|
||||
> undici@8.5.0 build:wasm
|
||||
> undici@8.7.0 build:wasm
|
||||
> node build/wasm.js --docker
|
||||
|
||||
> docker run --rm --platform=linux/x86_64 --user 1001:1001 --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/lib/llhttp,target=/home/node/build/lib/llhttp --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/build,target=/home/node/build/build --mount type=bind,source=/home/runner/work/node/node/deps/undici/src/deps,target=/home/node/build/deps -t ghcr.io/nodejs/wasm-builder@sha256:975f391d907e42a75b8c72eb77c782181e941608687d4d8694c3e9df415a0970 node build/wasm.js
|
||||
|
||||
+3
-2
@@ -178,8 +178,9 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {})
|
||||
|
||||
// 2. If the attribute-value failed to parse as a cookie date, ignore
|
||||
// the cookie-av.
|
||||
|
||||
cookieAttributeList.expires = expiryTime
|
||||
if (!Number.isNaN(expiryTime.getTime())) {
|
||||
cookieAttributeList.expires = expiryTime
|
||||
}
|
||||
} else if (attributeNameLowercase === 'max-age') {
|
||||
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2
|
||||
// If the attribute-name case-insensitively matches the string "Max-
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ function validateCookiePath (path) {
|
||||
|
||||
if (
|
||||
code < 0x20 || // exclude CTLs (0-31)
|
||||
code === 0x7F || // DEL
|
||||
code > 0x7E || // exclude non-ascii and DEL
|
||||
code === 0x3B // ;
|
||||
) {
|
||||
throw new Error('Invalid cookie path')
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ function createPotentialCORSRequest (url, destination, corsAttributeState, sameO
|
||||
destination,
|
||||
mode,
|
||||
credentials: credentialsMode,
|
||||
useCredentials: true
|
||||
useURLCredentials: true
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -46,7 +46,7 @@ const referrerPolicyTokensSet = new Set(referrerPolicyTokens)
|
||||
|
||||
const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error'])
|
||||
|
||||
const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE'])
|
||||
const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE', 'QUERY'])
|
||||
const safeMethodsSet = new Set(safeMethods)
|
||||
|
||||
const requestMode = /** @type {const} */ (['navigate', 'same-origin', 'no-cors', 'cors'])
|
||||
|
||||
+11
-2
@@ -1431,7 +1431,16 @@ async function httpNetworkOrCacheFetch (
|
||||
// Otherwise:
|
||||
|
||||
// 1. Set httpRequest to a clone of request.
|
||||
httpRequest = cloneRequest(request)
|
||||
// Implementations are encouraged to avoid teeing request’s body’s stream
|
||||
// when request’s body’s source is null as only a single body is needed in
|
||||
// that case. E.g., when request’s body’s source is null, redirects and
|
||||
// authentication will end up failing the fetch.
|
||||
if (request.body?.source != null) {
|
||||
httpRequest = cloneRequest(request)
|
||||
} else {
|
||||
httpRequest = cloneRequest({ ...request, body: null })
|
||||
httpRequest.body = request.body
|
||||
}
|
||||
|
||||
// 2. Set httpFetchParams to a copy of fetchParams.
|
||||
httpFetchParams = { ...fetchParams }
|
||||
@@ -1560,7 +1569,7 @@ async function httpNetworkOrCacheFetch (
|
||||
// TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
|
||||
if (!httpRequest.headersList.contains('accept-encoding', true)) {
|
||||
if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
|
||||
httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate', true)
|
||||
httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate, zstd', true)
|
||||
} else {
|
||||
httpRequest.headersList.append('accept-encoding', 'gzip, deflate', true)
|
||||
}
|
||||
|
||||
+4
-1
@@ -1195,7 +1195,10 @@ function simpleRangeHeaderValue (value, allowWhitespace) {
|
||||
// 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is
|
||||
// greater than rangeEndValue, then return failure.
|
||||
// Note: ... when can they not be numbers?
|
||||
if (rangeStartValue > rangeEndValue) {
|
||||
// Note: rangeStartValue or rangeEndValue may be null for open-ended ranges
|
||||
// such as `bytes=5-` or `bytes=-5`. A null value must not be coerced to 0
|
||||
// in the comparison, so this check only applies when both are numbers.
|
||||
if (rangeStartValue !== null && rangeEndValue !== null && rangeStartValue > rangeEndValue) {
|
||||
return 'failure'
|
||||
}
|
||||
|
||||
|
||||
+115
-122
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "undici",
|
||||
"version": "8.5.0",
|
||||
"version": "8.7.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "undici",
|
||||
"version": "8.5.0",
|
||||
"version": "8.7.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@fastify/busboy": "3.2.0",
|
||||
@@ -623,9 +623,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -640,9 +640,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -657,9 +657,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -674,9 +674,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -691,9 +691,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -708,9 +708,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -725,9 +725,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -742,9 +742,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -759,9 +759,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -776,9 +776,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -793,9 +793,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -810,9 +810,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -827,9 +827,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -844,9 +844,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -861,9 +861,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -878,9 +878,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -895,9 +895,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -912,9 +912,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -929,9 +929,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -946,9 +946,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -963,9 +963,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -980,9 +980,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -997,9 +997,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1014,9 +1014,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1031,9 +1031,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1048,9 +1048,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3335,12 +3335,6 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/basic-auth-parser": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/basic-auth-parser/-/basic-auth-parser-0.0.2-1.tgz",
|
||||
"integrity": "sha512-GFj8iVxo9onSU6BnnQvVwqvxh60UcSHJEDnIk3z4B6iOjsKSmqe+ibW0Rsz7YO7IE1HG3D3tqCNIidP46SZVdQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/borp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/borp/-/borp-1.0.0.tgz",
|
||||
@@ -4486,9 +4480,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -4499,32 +4493,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -8502,14 +8496,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy/-/proxy-4.0.0.tgz",
|
||||
"integrity": "sha512-qYi7N3kisRTQW0Y13JL0upO8e9mVLpu4yuxrXvWzCsaJVA9yqVF+iQkC/rCdnx2lGPQhC+qjefWSY7M6SHclhQ==",
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy/-/proxy-4.1.0.tgz",
|
||||
"integrity": "sha512-X3gqRNkp5dLWQolWksIfFSltI0ls8oQ01+tckxnPoz3bQ5+OUsdE7bwX/i/Q3TDEG4gx2PQjc3D07dIx90CQdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"args": "^5.0.3",
|
||||
"basic-auth-parser": "0.0.2-1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"bin": {
|
||||
@@ -10118,9 +10111,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz",
|
||||
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
|
||||
"version": "6.27.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
||||
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
Vendored
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "undici",
|
||||
"version": "8.5.0",
|
||||
"version": "8.7.0",
|
||||
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
||||
"homepage": "https://undici.nodejs.org",
|
||||
"bugs": {
|
||||
|
||||
Vendored
+1
-1
@@ -57,7 +57,7 @@ export declare namespace Client {
|
||||
keepAliveTimeoutThreshold?: number;
|
||||
/** An IPC endpoint, either a Unix domain socket or Windows named pipe. Default: `null`. */
|
||||
socketPath?: string;
|
||||
/** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Default: `1`. */
|
||||
/** The amount of concurrent requests to be sent over the single TCP/TLS connection according to [RFC7230](https://tools.ietf.org/html/rfc7230#section-6.3.2). Only enable values greater than `1` when the remote server is trusted. Default: `1`. */
|
||||
pipelining?: number;
|
||||
/** @deprecated use the connect option instead */
|
||||
tls?: never;
|
||||
|
||||
Vendored
+1
-1
@@ -18,7 +18,7 @@ export interface Cookie {
|
||||
export function deleteCookie (
|
||||
headers: Headers,
|
||||
name: string,
|
||||
attributes?: { name?: string, domain?: string }
|
||||
attributes?: { path?: string, domain?: string }
|
||||
): void
|
||||
|
||||
export function getCookies (headers: Headers): Record<string, string>
|
||||
|
||||
Vendored
+10
@@ -154,6 +154,16 @@ declare namespace Errors {
|
||||
code: 'UND_ERR_PRX_TLS'
|
||||
}
|
||||
|
||||
export class ProxyConnectionError extends UndiciError {
|
||||
constructor (
|
||||
cause?: Error,
|
||||
message?: string,
|
||||
options?: Record<any, any>
|
||||
)
|
||||
name: 'ProxyConnectionError'
|
||||
code: 'UND_ERR_PRX_CONN'
|
||||
}
|
||||
|
||||
export class MaxOriginsReachedError extends UndiciError {
|
||||
name: 'MaxOriginsReachedError'
|
||||
code: 'UND_ERR_MAX_ORIGINS_REACHED'
|
||||
|
||||
Vendored
+2
@@ -1,6 +1,8 @@
|
||||
import Dispatcher from './dispatcher'
|
||||
|
||||
export declare class RedirectHandler implements Dispatcher.DispatchHandler {
|
||||
static buildDispatch (dispatcher: Dispatcher, maxRedirections: number): Dispatcher.Dispatch
|
||||
|
||||
constructor (
|
||||
dispatch: Dispatcher.Dispatch,
|
||||
maxRedirections: number,
|
||||
|
||||
+7
@@ -24,6 +24,13 @@ declare namespace ProxyAgent {
|
||||
requestTls?: buildConnector.BuildOptions;
|
||||
proxyTls?: buildConnector.BuildOptions;
|
||||
clientFactory?(origin: URL, opts: object): Dispatcher;
|
||||
/**
|
||||
* Undici tunnels via CONNECT when the target endpoint uses HTTPS, and
|
||||
* forwards (HTTP/1.1 absolute-form request-target, per RFC 9112 §3.2.2)
|
||||
* otherwise, regardless of whether the proxy URL is HTTP or HTTPS. The
|
||||
* non-tunneled forwarding path does not negotiate HTTP/2 with the proxy.
|
||||
* Set to true to force tunneling for plain HTTP requests as well.
|
||||
*/
|
||||
proxyTunnel?: boolean;
|
||||
}
|
||||
}
|
||||
Vendored
+297
-117
@@ -3,7 +3,11 @@ var __defProp = Object.defineProperty;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
||||
var __commonJS = (cb, mod) => function __require() {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
try {
|
||||
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
||||
} catch (e) {
|
||||
throw mod = 0, e;
|
||||
}
|
||||
};
|
||||
|
||||
// lib/core/errors.js
|
||||
@@ -413,6 +417,25 @@ var require_errors = __commonJS({
|
||||
return true;
|
||||
}
|
||||
};
|
||||
var kProxyConnectionError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_PRX_CONN");
|
||||
var ProxyConnectionError = class extends UndiciError {
|
||||
static {
|
||||
__name(this, "ProxyConnectionError");
|
||||
}
|
||||
constructor(cause, message, options = {}) {
|
||||
super(message, { cause, ...options });
|
||||
this.name = "ProxyConnectionError";
|
||||
this.message = message || "Proxy Connection failed";
|
||||
this.code = "UND_ERR_PRX_CONN";
|
||||
this.cause = cause;
|
||||
}
|
||||
static [Symbol.hasInstance](instance) {
|
||||
return instance && instance[kProxyConnectionError] === true;
|
||||
}
|
||||
get [kProxyConnectionError]() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
var kMaxOriginsReachedError = /* @__PURE__ */ Symbol.for("undici.error.UND_ERR_MAX_ORIGINS_REACHED");
|
||||
var MaxOriginsReachedError = class extends UndiciError {
|
||||
static {
|
||||
@@ -483,6 +506,7 @@ var require_errors = __commonJS({
|
||||
RequestRetryError,
|
||||
ResponseError,
|
||||
SecureProxyConnectionError,
|
||||
ProxyConnectionError,
|
||||
MaxOriginsReachedError,
|
||||
Socks5ProxyError,
|
||||
MessageSizeExceededError
|
||||
@@ -2378,7 +2402,19 @@ var require_util = __commonJS({
|
||||
destroy(socket, new ConnectTimeoutError(message));
|
||||
}
|
||||
__name(onConnectTimeout, "onConnectTimeout");
|
||||
var lastUrlString = null;
|
||||
var lastProtocol = null;
|
||||
function getProtocolFromUrlString(urlString) {
|
||||
if (urlString === lastUrlString) {
|
||||
return lastProtocol;
|
||||
}
|
||||
const protocol = getProtocolFromUrlStringSlow(urlString);
|
||||
lastUrlString = urlString;
|
||||
lastProtocol = protocol;
|
||||
return protocol;
|
||||
}
|
||||
__name(getProtocolFromUrlString, "getProtocolFromUrlString");
|
||||
function getProtocolFromUrlStringSlow(urlString) {
|
||||
if (urlString[0] === "h" && urlString[1] === "t" && urlString[2] === "t" && urlString[3] === "p") {
|
||||
switch (urlString[4]) {
|
||||
case ":":
|
||||
@@ -2391,7 +2427,7 @@ var require_util = __commonJS({
|
||||
}
|
||||
return urlString.slice(0, urlString.indexOf(":") + 1);
|
||||
}
|
||||
__name(getProtocolFromUrlString, "getProtocolFromUrlString");
|
||||
__name(getProtocolFromUrlStringSlow, "getProtocolFromUrlStringSlow");
|
||||
var kEnumerableProperty = {
|
||||
__proto__: null,
|
||||
enumerable: true
|
||||
@@ -2408,7 +2444,9 @@ var require_util = __commonJS({
|
||||
post: "POST",
|
||||
POST: "POST",
|
||||
put: "PUT",
|
||||
PUT: "PUT"
|
||||
PUT: "PUT",
|
||||
query: "QUERY",
|
||||
QUERY: "QUERY"
|
||||
};
|
||||
var normalizedMethodRecords = {
|
||||
...normalizedMethodRecordsBase,
|
||||
@@ -2875,7 +2913,7 @@ var require_request = __commonJS({
|
||||
this.path = query ? serializePathWithQuery(path, query) : path;
|
||||
this.origin = origin;
|
||||
this.protocol = getProtocolFromUrlString(origin);
|
||||
this.idempotent = idempotent == null ? method === "HEAD" || method === "GET" : idempotent;
|
||||
this.idempotent = idempotent == null ? method === "HEAD" || method === "GET" || method === "QUERY" : idempotent;
|
||||
this.blocking = blocking ?? this.method !== "HEAD";
|
||||
this.reset = reset == null ? null : reset;
|
||||
this.host = null;
|
||||
@@ -4077,7 +4115,7 @@ var require_constants3 = __commonJS({
|
||||
);
|
||||
var safeMethods = (
|
||||
/** @type {const} */
|
||||
["GET", "HEAD", "OPTIONS", "TRACE"]
|
||||
["GET", "HEAD", "OPTIONS", "TRACE", "QUERY"]
|
||||
);
|
||||
var safeMethodsSet = new Set(safeMethods);
|
||||
var requestMode = (
|
||||
@@ -5867,7 +5905,7 @@ var require_util2 = __commonJS({
|
||||
if (rangeEndValue === null && rangeStartValue === null) {
|
||||
return "failure";
|
||||
}
|
||||
if (rangeStartValue > rangeEndValue) {
|
||||
if (rangeStartValue !== null && rangeEndValue !== null && rangeStartValue > rangeEndValue) {
|
||||
return "failure";
|
||||
}
|
||||
return { rangeStartValue, rangeEndValue };
|
||||
@@ -7290,6 +7328,19 @@ var require_client_h1 = __commonJS({
|
||||
assert(currentParser === null);
|
||||
assert(this.ptr != null);
|
||||
const { llhttp } = this;
|
||||
if (this.paused) {
|
||||
let data;
|
||||
do {
|
||||
llhttp.llhttp_resume(this.ptr);
|
||||
this.paused = false;
|
||||
data = this.socket.read() || EMPTY_BUF;
|
||||
this.execute(data);
|
||||
} while (this.paused && data.length > 0);
|
||||
if (this.paused) {
|
||||
llhttp.llhttp_resume(this.ptr);
|
||||
this.paused = false;
|
||||
}
|
||||
}
|
||||
let ret;
|
||||
try {
|
||||
currentParser = this;
|
||||
@@ -8416,11 +8467,17 @@ var require_client_h2 = __commonJS({
|
||||
}
|
||||
__name(requeueUnsentRequest, "requeueUnsentRequest");
|
||||
function completeRequest(client, request, resetPendingIdx = false) {
|
||||
const index = client[kQueue].indexOf(request, client[kRunningIdx]);
|
||||
const queue = client[kQueue];
|
||||
const runningIdx = client[kRunningIdx];
|
||||
if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
|
||||
client[kRunningIdx] = runningIdx + 1;
|
||||
return;
|
||||
}
|
||||
const index = queue.indexOf(request, runningIdx);
|
||||
if (index === -1 || index >= client[kPendingIdx]) {
|
||||
return;
|
||||
}
|
||||
client[kQueue].splice(index, 1);
|
||||
queue.splice(index, 1);
|
||||
client[kPendingIdx]--;
|
||||
if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
|
||||
client[kPendingIdx] = client[kRunningIdx];
|
||||
@@ -8432,9 +8489,7 @@ var require_client_h2 = __commonJS({
|
||||
return body == null || util.isBuffer(body) || util.isBlobLike(body);
|
||||
}
|
||||
__name(canRetryRequestAfterGoAway, "canRetryRequestAfterGoAway");
|
||||
function closeRequestStream(request, code = NGHTTP2_REFUSED_STREAM) {
|
||||
const stream = request[kRequestStream];
|
||||
clearRequestStream(request);
|
||||
function closeStream(stream, code = NGHTTP2_REFUSED_STREAM) {
|
||||
if (stream != null && !stream.destroyed && !stream.closed) {
|
||||
try {
|
||||
stream.close(code);
|
||||
@@ -8442,7 +8497,13 @@ var require_client_h2 = __commonJS({
|
||||
}
|
||||
}
|
||||
}
|
||||
__name(closeRequestStream, "closeRequestStream");
|
||||
__name(closeStream, "closeStream");
|
||||
function detachRequestStreamForClose(request) {
|
||||
const stream = request[kRequestStream];
|
||||
clearRequestStream(request);
|
||||
return stream;
|
||||
}
|
||||
__name(detachRequestStreamForClose, "detachRequestStreamForClose");
|
||||
function connectH2(client, socket) {
|
||||
client[kSocket] = socket;
|
||||
const http2InitialWindowSize = client[kHTTP2InitialWindowSize];
|
||||
@@ -8462,6 +8523,11 @@ var require_client_h2 = __commonJS({
|
||||
session[kSocket] = socket;
|
||||
session[kHTTP2SessionState] = {
|
||||
idleTimeout: null,
|
||||
// Sockets start out ref'd. Session ref/unref proxies to the socket, so a
|
||||
// single cached flag lets us skip redundant uv ref/unref calls, provided
|
||||
// every ref/unref of the session or its socket goes through
|
||||
// refH2Session/unrefH2Session.
|
||||
refed: true,
|
||||
ping: {
|
||||
interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref()
|
||||
}
|
||||
@@ -8478,7 +8544,7 @@ var require_client_h2 = __commonJS({
|
||||
util.addListener(session, "goaway", onHttp2SessionGoAway);
|
||||
util.addListener(session, "close", onHttp2SessionClose);
|
||||
util.addListener(session, "remoteSettings", onHttp2RemoteSettings);
|
||||
session.unref();
|
||||
unrefH2Session(session);
|
||||
client[kHTTP2Session] = session;
|
||||
socket[kHTTP2Session] = session;
|
||||
util.addListener(socket, "error", onHttp2SocketError);
|
||||
@@ -8543,16 +8609,30 @@ var require_client_h2 = __commonJS({
|
||||
};
|
||||
}
|
||||
__name(connectH2, "connectH2");
|
||||
function refH2Session(session) {
|
||||
const state = session[kHTTP2SessionState];
|
||||
if (state.refed === false) {
|
||||
state.refed = true;
|
||||
session.ref();
|
||||
}
|
||||
}
|
||||
__name(refH2Session, "refH2Session");
|
||||
function unrefH2Session(session) {
|
||||
const state = session[kHTTP2SessionState];
|
||||
if (state.refed === true) {
|
||||
state.refed = false;
|
||||
session.unref();
|
||||
}
|
||||
}
|
||||
__name(unrefH2Session, "unrefH2Session");
|
||||
function resumeH2(client) {
|
||||
const socket = client[kSocket];
|
||||
const session = client[kHTTP2Session];
|
||||
if (socket?.destroyed === false) {
|
||||
if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
|
||||
socket.unref();
|
||||
session.unref();
|
||||
unrefH2Session(session);
|
||||
} else {
|
||||
socket.ref();
|
||||
session.ref();
|
||||
refH2Session(session);
|
||||
}
|
||||
if (client[kSize] === 0 && session[kOpenStreams] === 0) {
|
||||
setHttp2IdleTimeout(session);
|
||||
@@ -8645,11 +8725,17 @@ var require_client_h2 = __commonJS({
|
||||
function onHttp2SessionError(err) {
|
||||
assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID");
|
||||
this[kSocket][kError] = err;
|
||||
if (this[kReceivedGoAway]) {
|
||||
return;
|
||||
}
|
||||
this[kClient][kOnError](err);
|
||||
}
|
||||
__name(onHttp2SessionError, "onHttp2SessionError");
|
||||
function onHttp2FrameError(type, code, id) {
|
||||
if (id === 0) {
|
||||
if (this[kReceivedGoAway]) {
|
||||
return;
|
||||
}
|
||||
const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`);
|
||||
this[kSocket][kError] = err;
|
||||
this[kClient][kOnError](err);
|
||||
@@ -8672,10 +8758,11 @@ var require_client_h2 = __commonJS({
|
||||
const previousPendingIdx = client[kPendingIdx];
|
||||
const pendingIdx = getGoAwayPendingIdx(client, lastStreamID);
|
||||
const retriableRequests = [];
|
||||
const streamsToClose = [];
|
||||
for (let i = pendingIdx; i < previousPendingIdx; i++) {
|
||||
const request = client[kQueue][i];
|
||||
if (request != null) {
|
||||
closeRequestStream(request);
|
||||
streamsToClose.push(detachRequestStreamForClose(request));
|
||||
if (canRetryRequestAfterGoAway(request)) {
|
||||
retriableRequests.push(request);
|
||||
} else {
|
||||
@@ -8683,6 +8770,9 @@ var require_client_h2 = __commonJS({
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < streamsToClose.length; i++) {
|
||||
closeStream(streamsToClose[i]);
|
||||
}
|
||||
if (pendingIdx !== previousPendingIdx) {
|
||||
const remainingPendingRequests = client[kQueue].slice(previousPendingIdx);
|
||||
client[kQueue].length = pendingIdx;
|
||||
@@ -8751,6 +8841,9 @@ var require_client_h2 = __commonJS({
|
||||
function onHttp2SocketError(err) {
|
||||
assert(err.code !== "ERR_TLS_CERT_ALTNAME_INVALID");
|
||||
this[kError] = err;
|
||||
if (this[kHTTP2Session]?.[kReceivedGoAway]) {
|
||||
return;
|
||||
}
|
||||
this[kClient][kOnError](err);
|
||||
}
|
||||
__name(onHttp2SocketError, "onHttp2SocketError");
|
||||
@@ -8770,7 +8863,7 @@ var require_client_h2 = __commonJS({
|
||||
stream[kHTTP2Session] = null;
|
||||
session[kOpenStreams] -= 1;
|
||||
if (session[kOpenStreams] === 0) {
|
||||
session.unref();
|
||||
unrefH2Session(session);
|
||||
setHttp2IdleTimeout(session);
|
||||
}
|
||||
}
|
||||
@@ -8789,11 +8882,9 @@ var require_client_h2 = __commonJS({
|
||||
releaseRequestStream(this);
|
||||
if (state.pendingEnd && !state.request.aborted && !state.request.completed) {
|
||||
state.request.onResponseEnd(state.trailers || {});
|
||||
state.finalizeRequest();
|
||||
finalizeRequest(state);
|
||||
}
|
||||
}
|
||||
this.off("data", onData);
|
||||
this.off("error", noop);
|
||||
closeStreamSession(this);
|
||||
this[kRequestStreamState] = null;
|
||||
}
|
||||
@@ -8896,7 +8987,7 @@ var require_client_h2 = __commonJS({
|
||||
}
|
||||
removeUpgradeStreamListeners(stream);
|
||||
detachRequestFromStream(request);
|
||||
state.finalizeRequest();
|
||||
finalizeRequest(state);
|
||||
}
|
||||
__name(onUpgradeResponse, "onUpgradeResponse");
|
||||
function setupUpgradeStream(stream, state) {
|
||||
@@ -8916,64 +9007,88 @@ var require_client_h2 = __commonJS({
|
||||
stream.setTimeout(headersTimeout);
|
||||
}
|
||||
__name(setupUpgradeStream, "setupUpgradeStream");
|
||||
function finalizeRequest(state, resetPendingIdx = false) {
|
||||
if (state.requestFinalized) {
|
||||
return;
|
||||
}
|
||||
state.requestFinalized = true;
|
||||
completeRequest(state.client, state.request, resetPendingIdx);
|
||||
state.client[kResume]();
|
||||
}
|
||||
__name(finalizeRequest, "finalizeRequest");
|
||||
function openStream(client, request, session, abort, headers, options) {
|
||||
try {
|
||||
return session.request(headers, options);
|
||||
} catch (err) {
|
||||
if (err?.code === "ERR_HTTP2_INVALID_SESSION" || err?.code === "ERR_HTTP2_GOAWAY_SESSION") {
|
||||
const wrappedErr2 = new SocketError(err.message, util.getSocketInfo(session[kSocket]));
|
||||
wrappedErr2.cause = err;
|
||||
session[kError] = wrappedErr2;
|
||||
resetHttp2Session(session, wrappedErr2);
|
||||
requeueUnsentRequest(client, request);
|
||||
return null;
|
||||
}
|
||||
const wrappedErr = new InformationalError(err.message, { cause: err });
|
||||
session[kError] = wrappedErr;
|
||||
session[kSocket][kError] = wrappedErr;
|
||||
session.destroy(wrappedErr);
|
||||
util.destroy(session[kSocket], wrappedErr);
|
||||
abort(wrappedErr);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
__name(openStream, "openStream");
|
||||
function writeH2(client, request) {
|
||||
const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout];
|
||||
const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout];
|
||||
const session = client[kHTTP2Session];
|
||||
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request;
|
||||
let { body } = request;
|
||||
if (upgrade != null && upgrade !== "websocket") {
|
||||
util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`));
|
||||
return false;
|
||||
}
|
||||
const headers = buildRequestHeaders(reqHeaders);
|
||||
let stream = null;
|
||||
headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority];
|
||||
headers[HTTP2_HEADER_METHOD] = method;
|
||||
let requestFinalized = false;
|
||||
const finalizeRequest = /* @__PURE__ */ __name((resetPendingIdx = false) => {
|
||||
if (requestFinalized) {
|
||||
return;
|
||||
}
|
||||
requestFinalized = true;
|
||||
completeRequest(client, request, resetPendingIdx);
|
||||
client[kResume]();
|
||||
}, "finalizeRequest");
|
||||
const state = {
|
||||
abort: null,
|
||||
body: request.body,
|
||||
client,
|
||||
contentLength: null,
|
||||
expectsPayload: false,
|
||||
request,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
requestFinalized: false,
|
||||
responseReceived: false,
|
||||
bodySent: false,
|
||||
pendingEnd: false,
|
||||
trailers: null,
|
||||
session,
|
||||
stream: null
|
||||
};
|
||||
const abort = /* @__PURE__ */ __name((err, resetPendingIdx = false) => {
|
||||
if (request.aborted || request.completed) {
|
||||
return;
|
||||
}
|
||||
err = err || new RequestAbortedError();
|
||||
util.errorRequest(client, request, err);
|
||||
if (stream != null) {
|
||||
if (state.stream != null) {
|
||||
clearRequestStream(request);
|
||||
stream.close();
|
||||
const stream2 = state.stream;
|
||||
stream2.close();
|
||||
setImmediate(() => {
|
||||
if (!stream2.destroyed) {
|
||||
util.destroy(stream2);
|
||||
}
|
||||
});
|
||||
client[kOnError](err);
|
||||
finalizeRequest(resetPendingIdx);
|
||||
finalizeRequest(state, resetPendingIdx);
|
||||
}
|
||||
util.destroy(body, err);
|
||||
util.destroy(state.body, err);
|
||||
}, "abort");
|
||||
const requestStream = /* @__PURE__ */ __name((headers2, options) => {
|
||||
try {
|
||||
return session.request(headers2, options);
|
||||
} catch (err) {
|
||||
if (err?.code === "ERR_HTTP2_INVALID_SESSION") {
|
||||
const wrappedErr2 = new SocketError(err.message, util.getSocketInfo(session[kSocket]));
|
||||
wrappedErr2.cause = err;
|
||||
session[kError] = wrappedErr2;
|
||||
resetHttp2Session(session, wrappedErr2);
|
||||
requeueUnsentRequest(client, request);
|
||||
return null;
|
||||
}
|
||||
const wrappedErr = new InformationalError(err.message, { cause: err });
|
||||
session[kError] = wrappedErr;
|
||||
session[kSocket][kError] = wrappedErr;
|
||||
session.destroy(wrappedErr);
|
||||
util.destroy(session[kSocket], wrappedErr);
|
||||
abort(wrappedErr);
|
||||
return null;
|
||||
}
|
||||
}, "requestStream");
|
||||
state.abort = abort;
|
||||
let stream = null;
|
||||
try {
|
||||
request.onRequestStart(abort, null);
|
||||
} catch (err) {
|
||||
@@ -8983,21 +9098,11 @@ var require_client_h2 = __commonJS({
|
||||
return false;
|
||||
}
|
||||
if (upgrade || method === "CONNECT") {
|
||||
session.ref();
|
||||
const upgradeState = {
|
||||
abort,
|
||||
finalizeRequest,
|
||||
request,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
responseReceived: false,
|
||||
session,
|
||||
stream: null
|
||||
};
|
||||
refH2Session(session);
|
||||
if (upgrade === "websocket") {
|
||||
if (session[kEnableConnectProtocol] === false) {
|
||||
util.errorRequest(client, request, new InformationalError("HTTP/2: Extended CONNECT protocol not supported by server"));
|
||||
session.unref();
|
||||
unrefH2Session(session);
|
||||
return false;
|
||||
}
|
||||
headers[HTTP2_HEADER_METHOD] = "CONNECT";
|
||||
@@ -9008,25 +9113,26 @@ var require_client_h2 = __commonJS({
|
||||
} else {
|
||||
headers[HTTP2_HEADER_SCHEME] = protocol === "http:" ? "http" : "https";
|
||||
}
|
||||
stream = requestStream(headers, { endStream: false, signal });
|
||||
stream = openStream(client, request, session, abort, headers, { endStream: false, signal });
|
||||
if (stream == null) {
|
||||
session.unref();
|
||||
unrefH2Session(session);
|
||||
return false;
|
||||
}
|
||||
setupUpgradeStream(stream, upgradeState);
|
||||
setupUpgradeStream(stream, state);
|
||||
return true;
|
||||
}
|
||||
stream = requestStream(headers, { endStream: false, signal });
|
||||
stream = openStream(client, request, session, abort, headers, { endStream: false, signal });
|
||||
if (stream == null) {
|
||||
session.unref();
|
||||
unrefH2Session(session);
|
||||
return false;
|
||||
}
|
||||
setupUpgradeStream(stream, upgradeState);
|
||||
setupUpgradeStream(stream, state);
|
||||
return true;
|
||||
}
|
||||
headers[HTTP2_HEADER_PATH] = path;
|
||||
headers[HTTP2_HEADER_SCHEME] = protocol === "http:" ? "http" : "https";
|
||||
const expectsPayload = method === "PUT" || method === "POST" || method === "PATCH" || method === "QUERY" || method === "PROPFIND" || method === "PROPPATCH";
|
||||
let body = state.body;
|
||||
if (body && typeof body.read === "function") {
|
||||
body.read(0);
|
||||
}
|
||||
@@ -9055,7 +9161,7 @@ var require_client_h2 = __commonJS({
|
||||
assert(body || contentLength === 0, "no body must not have content length");
|
||||
headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`;
|
||||
}
|
||||
session.ref();
|
||||
refH2Session(session);
|
||||
if (channels.sendHeaders.hasSubscribers) {
|
||||
let header = "";
|
||||
for (const key in headers) {
|
||||
@@ -9065,24 +9171,13 @@ var require_client_h2 = __commonJS({
|
||||
channels.sendHeaders.publish({ request, headers: header, socket: session[kSocket] });
|
||||
}
|
||||
const shouldEndStream = body === null || contentLength === 0;
|
||||
const state = {
|
||||
abort,
|
||||
body,
|
||||
client,
|
||||
contentLength,
|
||||
expectsPayload,
|
||||
finalizeRequest,
|
||||
request,
|
||||
headersTimeout,
|
||||
bodyTimeout,
|
||||
responseReceived: false,
|
||||
session,
|
||||
stream: null
|
||||
};
|
||||
state.body = body;
|
||||
state.contentLength = contentLength;
|
||||
state.expectsPayload = expectsPayload;
|
||||
if (expectContinue) {
|
||||
headers[HTTP2_HEADER_EXPECT] = "100-continue";
|
||||
}
|
||||
stream = requestStream(headers, { endStream: shouldEndStream, signal });
|
||||
stream = openStream(client, request, session, abort, headers, { endStream: shouldEndStream, signal });
|
||||
if (stream == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -9091,20 +9186,24 @@ var require_client_h2 = __commonJS({
|
||||
state.stream = stream;
|
||||
clearHttp2IdleTimeout(session);
|
||||
++session[kOpenStreams];
|
||||
stream.setTimeout(headersTimeout);
|
||||
if (headersTimeout) {
|
||||
stream.setTimeout(headersTimeout);
|
||||
}
|
||||
stream[kHTTP2Session] = session;
|
||||
stream.once("close", onRequestStreamClose);
|
||||
stream.on("close", onRequestStreamClose);
|
||||
bindRequestToStream(request, stream, releaseRequestStream);
|
||||
if (expectContinue) {
|
||||
stream.once("continue", writeBodyH2);
|
||||
}
|
||||
stream.once("response", onResponse);
|
||||
stream.once("end", onEnd);
|
||||
stream.once("error", onError);
|
||||
stream.once("frameError", onFrameError);
|
||||
stream.on("response", onResponse);
|
||||
stream.on("end", onEnd);
|
||||
stream.on("error", onError);
|
||||
stream.on("frameError", onFrameError);
|
||||
stream.on("aborted", onAborted);
|
||||
stream.on("timeout", onTimeout);
|
||||
stream.once("trailers", onTrailers);
|
||||
if (headersTimeout || bodyTimeout) {
|
||||
stream.on("timeout", onTimeout);
|
||||
}
|
||||
stream.on("trailers", onTrailers);
|
||||
if (!expectContinue) {
|
||||
writeBodyH2.call(stream);
|
||||
}
|
||||
@@ -9136,15 +9235,19 @@ var require_client_h2 = __commonJS({
|
||||
if (request[kRequestStream] === stream) {
|
||||
detachRequestFromStream(request);
|
||||
}
|
||||
removeRequestStreamListeners(stream);
|
||||
if (!stream.destroyed && !stream.closed) {
|
||||
removeRequestStreamListeners(stream);
|
||||
stream.once("error", noop);
|
||||
}
|
||||
}
|
||||
__name(releaseRequestStream, "releaseRequestStream");
|
||||
function onData(chunk) {
|
||||
const stream = this;
|
||||
const { request } = stream[kRequestStreamState];
|
||||
const state = stream[kRequestStreamState];
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
const { request } = state;
|
||||
if (request.aborted || request.completed) {
|
||||
return;
|
||||
}
|
||||
@@ -9156,14 +9259,23 @@ var require_client_h2 = __commonJS({
|
||||
function onResponse(headers) {
|
||||
const stream = this;
|
||||
const state = stream[kRequestStreamState];
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
const { request } = state;
|
||||
stream.off("response", onResponse);
|
||||
if (state.body != null && !state.bodySent && !stream.writableEnded) {
|
||||
stream.removeListener("continue", writeBodyH2);
|
||||
stream.end();
|
||||
}
|
||||
const statusCode = headers[HTTP2_HEADER_STATUS];
|
||||
delete headers[HTTP2_HEADER_STATUS];
|
||||
request.onResponseStarted();
|
||||
state.responseReceived = true;
|
||||
stream.setTimeout(state.bodyTimeout);
|
||||
if (request.aborted) {
|
||||
if (state.headersTimeout || state.bodyTimeout) {
|
||||
stream.setTimeout(state.bodyTimeout);
|
||||
}
|
||||
if (request.aborted || request.completed) {
|
||||
releaseRequestStream(stream);
|
||||
return;
|
||||
}
|
||||
@@ -9176,6 +9288,9 @@ var require_client_h2 = __commonJS({
|
||||
function onEnd() {
|
||||
const stream = this;
|
||||
const state = stream[kRequestStreamState];
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
const { request } = state;
|
||||
stream.off("end", onEnd);
|
||||
if (state.responseReceived) {
|
||||
@@ -9190,6 +9305,9 @@ var require_client_h2 = __commonJS({
|
||||
function onError(err) {
|
||||
const stream = this;
|
||||
const state = stream[kRequestStreamState];
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
stream.off("error", onError);
|
||||
state.abort(err);
|
||||
}
|
||||
@@ -9197,6 +9315,9 @@ var require_client_h2 = __commonJS({
|
||||
function onFrameError(type, code) {
|
||||
const stream = this;
|
||||
const state = stream[kRequestStreamState];
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
stream.off("frameError", onFrameError);
|
||||
state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`));
|
||||
}
|
||||
@@ -9208,6 +9329,9 @@ var require_client_h2 = __commonJS({
|
||||
function onTimeout() {
|
||||
const stream = this;
|
||||
const state = stream[kRequestStreamState];
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
stream.off("timeout", onTimeout);
|
||||
const err = state.responseReceived ? new BodyTimeoutError(`HTTP/2: "stream timeout after ${state.bodyTimeout}"`) : new HeadersTimeoutError(`HTTP/2: "headers timeout after ${state.headersTimeout}"`);
|
||||
state.abort(err);
|
||||
@@ -9216,6 +9340,9 @@ var require_client_h2 = __commonJS({
|
||||
function onTrailers(trailers) {
|
||||
const stream = this;
|
||||
const state = stream[kRequestStreamState];
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
const { request } = state;
|
||||
stream.off("trailers", onTrailers);
|
||||
stream.off("data", onData);
|
||||
@@ -9228,6 +9355,7 @@ var require_client_h2 = __commonJS({
|
||||
function writeBodyH2() {
|
||||
const stream = this;
|
||||
const state = stream[kRequestStreamState];
|
||||
state.bodySent = true;
|
||||
const { abort, body, client, contentLength, expectsPayload, request } = state;
|
||||
if (!body || contentLength === 0) {
|
||||
writeBuffer(
|
||||
@@ -11107,7 +11235,7 @@ var require_proxy_agent = __commonJS({
|
||||
var Agent = require_agent();
|
||||
var Pool = require_pool();
|
||||
var DispatcherBase = require_dispatcher_base();
|
||||
var { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require_errors();
|
||||
var { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError, ProxyConnectionError } = require_errors();
|
||||
var buildConnector = require_connect();
|
||||
var Client = require_client();
|
||||
var { channels } = require_diagnostics();
|
||||
@@ -11138,17 +11266,23 @@ var require_proxy_agent = __commonJS({
|
||||
return new Pool(origin, opts);
|
||||
}
|
||||
__name(defaultAgentFactory, "defaultAgentFactory");
|
||||
function shouldProxyTunnel(requestProtocol, proxyTunnel) {
|
||||
return proxyTunnel === true || requestProtocol !== "http:";
|
||||
}
|
||||
__name(shouldProxyTunnel, "shouldProxyTunnel");
|
||||
var Http1ProxyWrapper = class extends DispatcherBase {
|
||||
static {
|
||||
__name(this, "Http1ProxyWrapper");
|
||||
}
|
||||
#client;
|
||||
constructor(proxyUrl, { headers = {}, connect, factory }) {
|
||||
#proxyServername;
|
||||
constructor(proxyUrl, { headers = {}, connect, factory, proxyServername }) {
|
||||
if (!proxyUrl) {
|
||||
throw new InvalidArgumentError("Proxy URL is mandatory");
|
||||
}
|
||||
super();
|
||||
this[kProxyHeaders] = headers;
|
||||
this.#proxyServername = proxyServername;
|
||||
if (factory) {
|
||||
this.#client = factory(proxyUrl, { connect });
|
||||
} else {
|
||||
@@ -11177,6 +11311,9 @@ var require_proxy_agent = __commonJS({
|
||||
headers.host = host;
|
||||
}
|
||||
opts.headers = { ...this[kProxyHeaders], ...headers };
|
||||
if (this.#proxyServername != null) {
|
||||
opts.servername = this.#proxyServername;
|
||||
}
|
||||
return this.#client[kDispatch](opts, handler);
|
||||
}
|
||||
[kClose]() {
|
||||
@@ -11198,7 +11335,7 @@ var require_proxy_agent = __commonJS({
|
||||
if (typeof clientFactory !== "function") {
|
||||
throw new InvalidArgumentError("Proxy opts.clientFactory must be a function.");
|
||||
}
|
||||
const { proxyTunnel = true, connectTimeout } = opts;
|
||||
const { proxyTunnel, connectTimeout } = opts;
|
||||
super();
|
||||
const url = this.#getUrl(opts);
|
||||
const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url;
|
||||
@@ -11219,6 +11356,7 @@ var require_proxy_agent = __commonJS({
|
||||
this[kProxyHeaders]["proxy-authorization"] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:`).toString("base64")}`;
|
||||
}
|
||||
const connect = buildConnector({ timeout: connectTimeout, ...opts.proxyTls });
|
||||
const connectHTTP1 = buildConnector({ timeout: connectTimeout, ...opts.proxyTls, allowH2: false });
|
||||
this[kConnectEndpoint] = buildConnector({ timeout: connectTimeout, ...opts.requestTls });
|
||||
this[kConnectEndpointHTTP1] = buildConnector({ timeout: connectTimeout, ...opts.requestTls, allowH2: false });
|
||||
const agentFactory = opts.factory || defaultAgentFactory;
|
||||
@@ -11235,11 +11373,19 @@ var require_proxy_agent = __commonJS({
|
||||
requestTls: opts.requestTls
|
||||
});
|
||||
}
|
||||
if (!this[kTunnelProxy] && protocol2 === "http:" && this[kProxy].protocol === "http:") {
|
||||
if (!shouldProxyTunnel(protocol2, this[kTunnelProxy])) {
|
||||
const forwardConnect = this[kProxy].protocol === "https:" ? (opts2, cb) => connectHTTP1(opts2, (err, socket) => {
|
||||
if (err && err.code === "ERR_TLS_CERT_ALTNAME_INVALID") {
|
||||
cb(new SecureProxyConnectionError(err));
|
||||
} else {
|
||||
cb(err, socket);
|
||||
}
|
||||
}) : connectHTTP1;
|
||||
return new Http1ProxyWrapper(this[kProxy].uri, {
|
||||
headers: this[kProxyHeaders],
|
||||
connect,
|
||||
factory: agentFactory
|
||||
connect: forwardConnect,
|
||||
factory: agentFactory,
|
||||
proxyServername: this[kProxy].protocol === "https:" ? this[kProxyTls]?.servername || proxyHostname : void 0
|
||||
});
|
||||
}
|
||||
return agentFactory(origin2, options);
|
||||
@@ -11301,6 +11447,8 @@ var require_proxy_agent = __commonJS({
|
||||
} catch (err) {
|
||||
if (err.code === "ERR_TLS_CERT_ALTNAME_INVALID") {
|
||||
callback(new SecureProxyConnectionError(err));
|
||||
} else if (err.code === "UND_ERR_SOCKET") {
|
||||
callback(new ProxyConnectionError(err));
|
||||
} else {
|
||||
callback(err);
|
||||
}
|
||||
@@ -14047,7 +14195,12 @@ var require_fetch = __commonJS({
|
||||
httpFetchParams = fetchParams;
|
||||
httpRequest = request;
|
||||
} else {
|
||||
httpRequest = cloneRequest(request);
|
||||
if (request.body?.source != null) {
|
||||
httpRequest = cloneRequest(request);
|
||||
} else {
|
||||
httpRequest = cloneRequest({ ...request, body: null });
|
||||
httpRequest.body = request.body;
|
||||
}
|
||||
httpFetchParams = { ...fetchParams };
|
||||
httpFetchParams.request = httpRequest;
|
||||
}
|
||||
@@ -14092,7 +14245,7 @@ var require_fetch = __commonJS({
|
||||
}
|
||||
if (!httpRequest.headersList.contains("accept-encoding", true)) {
|
||||
if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
|
||||
httpRequest.headersList.append("accept-encoding", "br, gzip, deflate", true);
|
||||
httpRequest.headersList.append("accept-encoding", "br, gzip, deflate, zstd", true);
|
||||
} else {
|
||||
httpRequest.headersList.append("accept-encoding", "gzip, deflate", true);
|
||||
}
|
||||
@@ -16361,7 +16514,7 @@ var require_util4 = __commonJS({
|
||||
destination,
|
||||
mode,
|
||||
credentials: credentialsMode,
|
||||
useCredentials: true
|
||||
useURLCredentials: true
|
||||
});
|
||||
}
|
||||
__name(createPotentialCORSRequest, "createPotentialCORSRequest");
|
||||
@@ -17050,6 +17203,7 @@ var require_readable = __commonJS({
|
||||
var kContentLength = /* @__PURE__ */ Symbol("kContentLength");
|
||||
var kUsed = /* @__PURE__ */ Symbol("kUsed");
|
||||
var kBytesRead = /* @__PURE__ */ Symbol("kBytesRead");
|
||||
var kPreservedBuffer = /* @__PURE__ */ Symbol("kPreservedBuffer");
|
||||
var noop = /* @__PURE__ */ __name(() => {
|
||||
}, "noop");
|
||||
var BodyReadable = class extends Readable {
|
||||
@@ -17291,7 +17445,22 @@ var require_readable = __commonJS({
|
||||
*/
|
||||
setEncoding(encoding) {
|
||||
if (Buffer.isEncoding(encoding)) {
|
||||
this._readableState.encoding = encoding;
|
||||
const state = this._readableState;
|
||||
const buffer = state.buffer;
|
||||
if (buffer && state.length > 0) {
|
||||
const bufferIndex = state.bufferIndex ?? 0;
|
||||
const preserved = [];
|
||||
const source = typeof buffer.slice === "function" ? buffer.slice(bufferIndex) : buffer;
|
||||
for (const data of source) {
|
||||
if (Buffer.isBuffer(data)) {
|
||||
preserved.push(data);
|
||||
}
|
||||
}
|
||||
if (preserved.length > 0) {
|
||||
this[kPreservedBuffer] = (this[kPreservedBuffer] || []).concat(preserved);
|
||||
}
|
||||
}
|
||||
super.setEncoding(encoding);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -17344,7 +17513,13 @@ var require_readable = __commonJS({
|
||||
return;
|
||||
}
|
||||
const { _readableState: state } = consume2.stream;
|
||||
if (state.bufferIndex) {
|
||||
const preserved = consume2.stream[kPreservedBuffer];
|
||||
if (preserved && preserved.length > 0) {
|
||||
for (const chunk of preserved) {
|
||||
consumePush(consume2, chunk);
|
||||
}
|
||||
consume2.stream[kPreservedBuffer] = null;
|
||||
} else if (state.bufferIndex) {
|
||||
const start = state.bufferIndex;
|
||||
const end = state.buffer.length;
|
||||
for (let n = start; n < end; n++) {
|
||||
@@ -17419,6 +17594,9 @@ var require_readable = __commonJS({
|
||||
}
|
||||
__name(consumeEnd, "consumeEnd");
|
||||
function consumePush(consume2, chunk) {
|
||||
if (consume2.body === null) {
|
||||
return;
|
||||
}
|
||||
consume2.length += chunk.length;
|
||||
consume2.body.push(chunk);
|
||||
}
|
||||
@@ -17511,7 +17689,9 @@ var require_api_request = __commonJS({
|
||||
this.removeAbortListener = util.addAbortListener(signal, () => {
|
||||
this.reason = signal.reason ?? new RequestAbortedError();
|
||||
if (this.res) {
|
||||
util.destroy(this.res.on("error", noop), this.reason);
|
||||
const res = this.res;
|
||||
this.res = null;
|
||||
util.destroy(res.on("error", noop), this.reason);
|
||||
} else if (this.abort) {
|
||||
this.abort(this.reason);
|
||||
}
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
// Refer to tools/dep_updaters/update-undici.sh
|
||||
#ifndef SRC_UNDICI_VERSION_H_
|
||||
#define SRC_UNDICI_VERSION_H_
|
||||
#define UNDICI_VERSION "8.5.0"
|
||||
#define UNDICI_VERSION "8.7.0"
|
||||
#endif // SRC_UNDICI_VERSION_H_
|
||||
@@ -19,15 +19,17 @@ await once(proxy, 'listening');
|
||||
|
||||
const serverHost = `localhost:${server.address().port}`;
|
||||
|
||||
// FIXME(undici:4083): undici currently always tunnels the request over
|
||||
// CONNECT if proxyTunnel is not explicitly set to false.
|
||||
const expectedLogs = [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
method: 'GET',
|
||||
url: `http://${serverHost}/test`,
|
||||
headers: {
|
||||
'connection': 'close',
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
'accept': '*/*',
|
||||
'accept-language': '*',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'user-agent': 'node',
|
||||
'accept-encoding': 'gzip, deflate',
|
||||
},
|
||||
}];
|
||||
|
||||
|
||||
@@ -20,15 +20,17 @@ await checkProxiedFetch({
|
||||
|
||||
shutdown();
|
||||
|
||||
// FIXME(undici:4083): undici currently always tunnels the request over
|
||||
// CONNECT if proxyTunnel is not explicitly set to false.
|
||||
const expectedLogs = [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'close',
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
'accept': '*/*',
|
||||
'accept-language': '*',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'user-agent': 'node',
|
||||
'accept-encoding': 'gzip, deflate',
|
||||
},
|
||||
}];
|
||||
|
||||
|
||||
@@ -20,15 +20,17 @@ await checkProxiedFetch({
|
||||
|
||||
shutdown();
|
||||
|
||||
// FIXME(undici:4083): undici currently always tunnels the request over
|
||||
// CONNECT if proxyTunnel is not explicitly set to false.
|
||||
const expectedLogs = [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'close',
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
'accept': '*/*',
|
||||
'accept-language': '*',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'user-agent': 'node',
|
||||
'accept-encoding': 'gzip, deflate',
|
||||
},
|
||||
}];
|
||||
|
||||
|
||||
@@ -19,15 +19,17 @@ await checkProxiedFetch({
|
||||
|
||||
shutdown();
|
||||
|
||||
// FIXME(undici:4083): undici currently always tunnels the request over
|
||||
// CONNECT if proxyTunnel is not explicitly set to false.
|
||||
const expectedLogs = [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'close',
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
'accept': '*/*',
|
||||
'accept-language': '*',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'user-agent': 'node',
|
||||
'accept-encoding': 'gzip, deflate',
|
||||
},
|
||||
}];
|
||||
|
||||
|
||||
@@ -28,17 +28,17 @@ proxy2.close();
|
||||
// Verify request did NOT go through original proxy, but the overridden one.
|
||||
assert.deepStrictEqual(proxyLogs, []);
|
||||
|
||||
// FIXME(undici:4083): undici currently always tunnels the request over
|
||||
// CONNECT if proxyTunnel is not explicitly set to false, but what we
|
||||
// need is for it to be automatically false for HTTP requests to be
|
||||
// consistent with curl.
|
||||
const expectedLogs = [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'close',
|
||||
'host': serverHost,
|
||||
'proxy-connection': 'keep-alive',
|
||||
'connection': 'keep-alive',
|
||||
'accept': '*/*',
|
||||
'accept-language': '*',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'user-agent': 'node',
|
||||
'accept-encoding': 'gzip, deflate',
|
||||
},
|
||||
}];
|
||||
assert.deepStrictEqual(proxyLogs2, expectedLogs);
|
||||
@@ -56,17 +56,17 @@ const requestUrl = `http://${serverHost}/test`;
|
||||
stdout: 'Hello world',
|
||||
}, ['--use-env-proxy']);
|
||||
|
||||
// FIXME(undici:4083): undici currently always tunnels the request over
|
||||
// CONNECT if proxyTunnel is not explicitly set to false, but what we
|
||||
// need is for it to be automatically false for HTTP requests to be
|
||||
// consistent with curl.
|
||||
assert.deepStrictEqual(logs, [{
|
||||
method: 'CONNECT',
|
||||
url: serverHost,
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
headers: {
|
||||
'connection': 'close',
|
||||
'proxy-connection': 'keep-alive',
|
||||
'host': serverHost,
|
||||
'connection': 'keep-alive',
|
||||
'accept': '*/*',
|
||||
'accept-language': '*',
|
||||
'sec-fetch-mode': 'cors',
|
||||
'user-agent': 'node',
|
||||
'accept-encoding': 'gzip, deflate',
|
||||
},
|
||||
}]);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user