Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions deps/undici/src/docs/docs/api/Client.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Returns: `Client`
* **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.
* **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.
* **connect** `ConnectOptions | Function | 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.
Expand Down
4 changes: 2 additions & 2 deletions deps/undici/src/docs/docs/api/Dispatcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -533,7 +533,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`.

`body` contains the following additional extensions:

- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144.
- `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 131072.

Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`.

Expand Down Expand Up @@ -1031,7 +1031,7 @@ const client = new Client("http://service.example").compose(
The `dump` interceptor enables you to dump the response body from a request upon a given limit.

**Options**
- `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the request's body exceeds this value then the connection will be closed. Default: `1048576`.
- `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the response's body exceeds this value then the connection will be closed. Default: `1048576`.

> The `Dispatcher#options` also gets extended with the options `dumpMaxSize`, `abortOnDumped`, and `waitForTrailers` which can be used to configure the interceptor at a request-per-request basis.

Expand Down
231 changes: 231 additions & 0 deletions deps/undici/src/docs/docs/best-practices/migrating-from-v7-to-v8.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Migrating from Undici 7 to 8

This guide covers the changes you are most likely to hit when upgrading an
application or library from Undici v7 to v8.

## Before you upgrade

- Make sure your runtime is Node.js `>= 22.19.0`.
- If you have custom dispatchers, interceptors, or handlers, review the
handler API changes before updating.
- If you rely on HTTP/1.1-only behavior, plan to set `allowH2: false`
explicitly.

## 1. Update your Node.js version

Undici v8 requires Node.js `>= 22.19.0`.

If you are still on Node.js 20 or an older Node.js 22 release, upgrade Node.js
first:

```bash
node -v
```

If that command prints a version lower than `v22.19.0`, upgrade Node.js before
installing Undici v8.

## 2. Migrate custom dispatcher handlers to the v2 API

Undici v8 uses the newer dispatcher handler API consistently.

If you implemented custom dispatchers, interceptors, or wrappers around
`dispatch()`, update legacy callbacks such as `onConnect`, `onHeaders`, and
`onComplete` to the newer callback names.

### Old handler callbacks vs. v8 callbacks

| Undici 7 style | Undici 8 style |
|---|---|
| `onConnect(abort, context)` | `onRequestStart(controller, context)` |
| `onHeaders(statusCode, rawHeaders, resume, statusText)` | `onResponseStart(controller, statusCode, headers, statusText)` |
| `onData(chunk)` | `onResponseData(controller, chunk)` |
| `onComplete(trailers)` | `onResponseEnd(controller, trailers)` |
| `onError(err)` | `onResponseError(controller, err)` |
| `onUpgrade(statusCode, rawHeaders, socket)` | `onRequestUpgrade(controller, statusCode, headers, socket)` |

### Example

Before:

```js
client.dispatch(options, {
onConnect (abort) {
this.abort = abort
},
onHeaders (statusCode, headers, resume) {
this.resume = resume
return true
},
onData (chunk) {
chunks.push(chunk)
return true
},
onComplete (trailers) {
console.log(trailers)
},
onError (err) {
console.error(err)
}
})
```

After:

```js
client.dispatch(options, {
onRequestStart (controller) {
this.controller = controller
},
onResponseStart (controller, statusCode, headers, statusText) {
console.log(statusCode, statusText, headers)
},
onResponseData (controller, chunk) {
chunks.push(chunk)
},
onResponseEnd (controller, trailers) {
console.log(trailers)
},
onResponseError (controller, err) {
console.error(err)
}
})
```

### Pause, resume, and abort now go through the controller

In Undici v7, legacy handlers could return `false` or keep references to
`abort()` and `resume()` callbacks. In Undici v8, use the controller instead:

```js
onRequestStart (controller) {
this.controller = controller
}

onResponseData (controller, chunk) {
controller.pause()
setImmediate(() => controller.resume())
}

onResponseError (controller, err) {
controller.abort(err)
}
```

### Raw headers and trailers moved to the controller

If you need the raw header arrays, read them from the controller:

- `controller.rawHeaders`
- `controller.rawTrailers`

## 3. Update `onBodySent()` handlers

If you implemented `onBodySent()`, note that its signature changed.

Before, handlers received counters:

```js
onBodySent (chunkSize, totalBytesSent) {}
```

In Undici v8, handlers receive the actual chunk:

```js
onBodySent (chunk) {}
```

If you need a notification that the whole body has been sent, use
`onRequestSent()`:

```js
onRequestSent () {
console.log('request body fully sent')
}
```

## 4. If you need HTTP/1.1 only, disable HTTP/2 explicitly

Undici v8 enables HTTP/2 by default when a TLS server negotiates it via ALPN.

If your application depends on HTTP/1.1-specific behavior, set `allowH2: false`
explicitly.

Before:

```js
const client = new Client('https://example.com')
```

After, to keep HTTP/1.1 only:

```js
const client = new Client('https://example.com', {
allowH2: false
})
```

The same applies when you configure an `Agent`:

```js
const agent = new Agent({
allowH2: false
})
```

## 5. Use real `Blob` and `File` instances

Undici v8 no longer accepts fake Blob-like values that only imitate `Blob` or
`File` via properties such as `Symbol.toStringTag`.

If you were passing custom objects that looked like `Blob`s, replace them with
actual `Blob` or `File` instances:

```js
const body = new Blob(['hello'])
```

## 6. Avoid depending on the internal global dispatcher symbol

`setGlobalDispatcher()` and `getGlobalDispatcher()` remain the public APIs and
should continue to be used.

Internally, Undici v8 stores its dispatcher under
`Symbol.for('undici.globalDispatcher.2')` and mirrors a v1-compatible wrapper
for legacy consumers such as Node.js built-in `fetch`.

If your code was reading or writing `Symbol.for('undici.globalDispatcher.1')`
directly, migrate to the public APIs instead:

```js
import { setGlobalDispatcher, getGlobalDispatcher, Agent } from 'undici'

setGlobalDispatcher(new Agent())
const dispatcher = getGlobalDispatcher()
```

If you must expose a dispatcher to legacy v1 handler consumers, wrap it with
`Dispatcher1Wrapper`:

```js
import { Agent, Dispatcher1Wrapper } from 'undici'

const legacyCompatibleDispatcher = new Dispatcher1Wrapper(new Agent())
```

## 7. Verify the upgrade

After moving to Undici v8, it is worth checking these paths in your test suite:

- requests that use a custom `dispatcher`
- `setGlobalDispatcher()` behavior
- any custom interceptor or retry handler
- uploads that use `Blob`, `File`, or `FormData`
- integrations that depend on HTTP/1.1-only behavior

## 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)
4 changes: 2 additions & 2 deletions deps/undici/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,14 @@ function makeDispatcher (fn) {
url = util.parseURL(url)
}

const { agent, dispatcher = getGlobalDispatcher() } = opts
const { agent, dispatcher = getGlobalDispatcher(), ...restOpts } = opts

if (agent) {
throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?')
}

return fn.call(dispatcher, {
...opts,
...restOpts,
origin: url.origin,
path: url.search ? `${url.pathname}${url.search}` : url.pathname,
method: opts.method || (opts.body ? 'PUT' : 'GET')
Expand Down
6 changes: 1 addition & 5 deletions deps/undici/src/lib/core/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ const { InvalidArgumentError, ConnectTimeoutError } = require('./errors')
const { headerNameLowerCasedRecord } = require('./constants')
const { tree } = require('./tree')

const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(v => Number(v))

class BodyAsyncIterable {
constructor (body) {
this[kBody] = body
Expand Down Expand Up @@ -323,7 +321,7 @@ function isIterable (obj) {
*/
function hasSafeIterator (obj) {
const prototype = Object.getPrototypeOf(obj)
const ownIterator = Object.prototype.hasOwnProperty.call(obj, Symbol.iterator)
const ownIterator = Object.hasOwn(obj, Symbol.iterator)
return ownIterator || (prototype != null && prototype !== Object.prototype && typeof obj[Symbol.iterator] === 'function')
}

Expand Down Expand Up @@ -989,8 +987,6 @@ module.exports = {
normalizedMethodRecords,
isValidPort,
isHttpOrHttpsPrefixed,
nodeMajor,
nodeMinor,
safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']),
wrapRequestBody,
setupConnectTimeout,
Expand Down
2 changes: 1 addition & 1 deletion deps/undici/src/lib/dispatcher/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ class Agent extends DispatcherBase {
throw new InvalidArgumentError('maxOrigins must be a number greater than 0')
}

super()
super(options)

if (connect && typeof connect !== 'function') {
connect = { ...connect }
Expand Down
3 changes: 0 additions & 3 deletions deps/undici/src/lib/dispatcher/balanced-pool.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,6 @@ class BalancedPool extends PoolBase {
super()

this[kOptions] = { ...util.deepClone(opts) }
this[kOptions].interceptors = opts.interceptors
? { ...opts.interceptors }
: undefined
this[kIndex] = -1
this[kCurrentWeight] = 0

Expand Down
5 changes: 3 additions & 2 deletions deps/undici/src/lib/dispatcher/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ class Client extends DispatcherBase {
useH2c,
initialWindowSize,
connectionWindowSize,
pingInterval
pingInterval,
webSocket
} = {}) {
if (keepAlive !== undefined) {
throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead')
Expand Down Expand Up @@ -222,7 +223,7 @@ class Client extends DispatcherBase {
throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0')
}

super()
super({ webSocket })

if (typeof connect !== 'function') {
connect = buildConnector({
Expand Down
22 changes: 22 additions & 0 deletions deps/undici/src/lib/dispatcher/dispatcher-base.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy

const kOnDestroyed = Symbol('onDestroyed')
const kOnClosed = Symbol('onClosed')
const kWebSocketOptions = Symbol('webSocketOptions')

class DispatcherBase extends Dispatcher {
/** @type {boolean} */
Expand All @@ -24,6 +25,23 @@ class DispatcherBase extends Dispatcher {
/** @type {Array<Function>|null} */
[kOnClosed] = null

/**
* @param {import('../../types/dispatcher').DispatcherOptions} [opts]
*/
constructor (opts) {
super()
this[kWebSocketOptions] = opts?.webSocket ?? {}
}

/**
* @returns {import('../../types/dispatcher').WebSocketOptions}
*/
get webSocketOptions () {
return {
maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default
}
}

/** @returns {boolean} */
get destroyed () {
return this[kDestroyed]
Expand Down Expand Up @@ -138,6 +156,10 @@ class DispatcherBase extends Dispatcher {
throw new InvalidArgumentError('opts must be an object.')
}

if (opts.dispatcher) {
throw new InvalidArgumentError('opts.dispatcher is not supported by instance methods. Pass opts.dispatcher to the top-level undici functions or call the dispatcher instance method directly.')
}

if (this[kDestroyed] || this[kOnDestroyed]) {
throw new ClientDestroyedError()
}
Expand Down
6 changes: 6 additions & 0 deletions deps/undici/src/lib/dispatcher/dispatcher1-wrapper.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,12 @@ class Dispatcher1Wrapper extends Dispatcher {
}

dispatch (opts, handler) {
// Legacy (v1) consumers do not support HTTP/2, so force HTTP/1.1.
// See https://github.com/nodejs/undici/issues/4989
if (opts.allowH2 !== false) {
opts = { ...opts, allowH2: false }
}

return this.#dispatcher.dispatch(opts, Dispatcher1Wrapper.wrapHandler(handler))
}

Expand Down
Loading
Loading