| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -24,6 +24,8 @@ Returns: `Client` | |||
| 24 | 24 | * **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. | |
| 25 | 25 | * **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. | |
| 26 | 26 | * **maxResponseSize** `number | null` (optional) - Default: `-1` - The maximum length of response body in bytes. Set to `-1` to disable. | |
| 27 | + * **webSocket** `WebSocketOptions` (optional) - WebSocket-specific configuration options. | ||
| 28 | + * **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. | ||
| 27 | 29 | * **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. | |
| 28 | 30 | * **connect** `ConnectOptions | Function | null` (optional) - Default: `null`. | |
| 29 | 31 | * **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. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -533,7 +533,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`. | |||
| 533 | 533 | ||
| 534 | 534 | `body` contains the following additional extensions: | |
| 535 | 535 | ||
| 536 | - - `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144. | ||
| 536 | + - `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 131072. | ||
| 537 | 537 | ||
| 538 | 538 | 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`. | |
| 539 | 539 | ||
@@ -1031,7 +1031,7 @@ const client = new Client("http://service.example").compose( | |||
| 1031 | 1031 | The `dump` interceptor enables you to dump the response body from a request upon a given limit. | |
| 1032 | 1032 | ||
| 1033 | 1033 | **Options** | |
| 1034 | - - `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`. | ||
| 1034 | + - `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`. | ||
| 1035 | 1035 | ||
| 1036 | 1036 | > 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. | |
| 1037 | 1037 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,231 @@ | |||
| 1 | + # Migrating from Undici 7 to 8 | ||
| 2 | + | ||
| 3 | + This guide covers the changes you are most likely to hit when upgrading an | ||
| 4 | + application or library from Undici v7 to v8. | ||
| 5 | + | ||
| 6 | + ## Before you upgrade | ||
| 7 | + | ||
| 8 | + - Make sure your runtime is Node.js `>= 22.19.0`. | ||
| 9 | + - If you have custom dispatchers, interceptors, or handlers, review the | ||
| 10 | + handler API changes before updating. | ||
| 11 | + - If you rely on HTTP/1.1-only behavior, plan to set `allowH2: false` | ||
| 12 | + explicitly. | ||
| 13 | + | ||
| 14 | + ## 1. Update your Node.js version | ||
| 15 | + | ||
| 16 | + Undici v8 requires Node.js `>= 22.19.0`. | ||
| 17 | + | ||
| 18 | + If you are still on Node.js 20 or an older Node.js 22 release, upgrade Node.js | ||
| 19 | + first: | ||
| 20 | + | ||
| 21 | + ```bash | ||
| 22 | + node -v | ||
| 23 | + ``` | ||
| 24 | + | ||
| 25 | + If that command prints a version lower than `v22.19.0`, upgrade Node.js before | ||
| 26 | + installing Undici v8. | ||
| 27 | + | ||
| 28 | + ## 2. Migrate custom dispatcher handlers to the v2 API | ||
| 29 | + | ||
| 30 | + Undici v8 uses the newer dispatcher handler API consistently. | ||
| 31 | + | ||
| 32 | + If you implemented custom dispatchers, interceptors, or wrappers around | ||
| 33 | + `dispatch()`, update legacy callbacks such as `onConnect`, `onHeaders`, and | ||
| 34 | + `onComplete` to the newer callback names. | ||
| 35 | + | ||
| 36 | + ### Old handler callbacks vs. v8 callbacks | ||
| 37 | + | ||
| 38 | + | Undici 7 style | Undici 8 style | | ||
| 39 | + |---|---| | ||
| 40 | + | `onConnect(abort, context)` | `onRequestStart(controller, context)` | | ||
| 41 | + | `onHeaders(statusCode, rawHeaders, resume, statusText)` | `onResponseStart(controller, statusCode, headers, statusText)` | | ||
| 42 | + | `onData(chunk)` | `onResponseData(controller, chunk)` | | ||
| 43 | + | `onComplete(trailers)` | `onResponseEnd(controller, trailers)` | | ||
| 44 | + | `onError(err)` | `onResponseError(controller, err)` | | ||
| 45 | + | `onUpgrade(statusCode, rawHeaders, socket)` | `onRequestUpgrade(controller, statusCode, headers, socket)` | | ||
| 46 | + | ||
| 47 | + ### Example | ||
| 48 | + | ||
| 49 | + Before: | ||
| 50 | + | ||
| 51 | + ```js | ||
| 52 | + client.dispatch(options, { | ||
| 53 | + onConnect (abort) { | ||
| 54 | + this.abort = abort | ||
| 55 | + }, | ||
| 56 | + onHeaders (statusCode, headers, resume) { | ||
| 57 | + this.resume = resume | ||
| 58 | + return true | ||
| 59 | + }, | ||
| 60 | + onData (chunk) { | ||
| 61 | + chunks.push(chunk) | ||
| 62 | + return true | ||
| 63 | + }, | ||
| 64 | + onComplete (trailers) { | ||
| 65 | + console.log(trailers) | ||
| 66 | + }, | ||
| 67 | + onError (err) { | ||
| 68 | + console.error(err) | ||
| 69 | + } | ||
| 70 | + }) | ||
| 71 | + ``` | ||
| 72 | + | ||
| 73 | + After: | ||
| 74 | + | ||
| 75 | + ```js | ||
| 76 | + client.dispatch(options, { | ||
| 77 | + onRequestStart (controller) { | ||
| 78 | + this.controller = controller | ||
| 79 | + }, | ||
| 80 | + onResponseStart (controller, statusCode, headers, statusText) { | ||
| 81 | + console.log(statusCode, statusText, headers) | ||
| 82 | + }, | ||
| 83 | + onResponseData (controller, chunk) { | ||
| 84 | + chunks.push(chunk) | ||
| 85 | + }, | ||
| 86 | + onResponseEnd (controller, trailers) { | ||
| 87 | + console.log(trailers) | ||
| 88 | + }, | ||
| 89 | + onResponseError (controller, err) { | ||
| 90 | + console.error(err) | ||
| 91 | + } | ||
| 92 | + }) | ||
| 93 | + ``` | ||
| 94 | + | ||
| 95 | + ### Pause, resume, and abort now go through the controller | ||
| 96 | + | ||
| 97 | + In Undici v7, legacy handlers could return `false` or keep references to | ||
| 98 | + `abort()` and `resume()` callbacks. In Undici v8, use the controller instead: | ||
| 99 | + | ||
| 100 | + ```js | ||
| 101 | + onRequestStart (controller) { | ||
| 102 | + this.controller = controller | ||
| 103 | + } | ||
| 104 | + | ||
| 105 | + onResponseData (controller, chunk) { | ||
| 106 | + controller.pause() | ||
| 107 | + setImmediate(() => controller.resume()) | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + onResponseError (controller, err) { | ||
| 111 | + controller.abort(err) | ||
| 112 | + } | ||
| 113 | + ``` | ||
| 114 | + | ||
| 115 | + ### Raw headers and trailers moved to the controller | ||
| 116 | + | ||
| 117 | + If you need the raw header arrays, read them from the controller: | ||
| 118 | + | ||
| 119 | + - `controller.rawHeaders` | ||
| 120 | + - `controller.rawTrailers` | ||
| 121 | + | ||
| 122 | + ## 3. Update `onBodySent()` handlers | ||
| 123 | + | ||
| 124 | + If you implemented `onBodySent()`, note that its signature changed. | ||
| 125 | + | ||
| 126 | + Before, handlers received counters: | ||
| 127 | + | ||
| 128 | + ```js | ||
| 129 | + onBodySent (chunkSize, totalBytesSent) {} | ||
| 130 | + ``` | ||
| 131 | + | ||
| 132 | + In Undici v8, handlers receive the actual chunk: | ||
| 133 | + | ||
| 134 | + ```js | ||
| 135 | + onBodySent (chunk) {} | ||
| 136 | + ``` | ||
| 137 | + | ||
| 138 | + If you need a notification that the whole body has been sent, use | ||
| 139 | + `onRequestSent()`: | ||
| 140 | + | ||
| 141 | + ```js | ||
| 142 | + onRequestSent () { | ||
| 143 | + console.log('request body fully sent') | ||
| 144 | + } | ||
| 145 | + ``` | ||
| 146 | + | ||
| 147 | + ## 4. If you need HTTP/1.1 only, disable HTTP/2 explicitly | ||
| 148 | + | ||
| 149 | + Undici v8 enables HTTP/2 by default when a TLS server negotiates it via ALPN. | ||
| 150 | + | ||
| 151 | + If your application depends on HTTP/1.1-specific behavior, set `allowH2: false` | ||
| 152 | + explicitly. | ||
| 153 | + | ||
| 154 | + Before: | ||
| 155 | + | ||
| 156 | + ```js | ||
| 157 | + const client = new Client('https://example.com') | ||
| 158 | + ``` | ||
| 159 | + | ||
| 160 | + After, to keep HTTP/1.1 only: | ||
| 161 | + | ||
| 162 | + ```js | ||
| 163 | + const client = new Client('https://example.com', { | ||
| 164 | + allowH2: false | ||
| 165 | + }) | ||
| 166 | + ``` | ||
| 167 | + | ||
| 168 | + The same applies when you configure an `Agent`: | ||
| 169 | + | ||
| 170 | + ```js | ||
| 171 | + const agent = new Agent({ | ||
| 172 | + allowH2: false | ||
| 173 | + }) | ||
| 174 | + ``` | ||
| 175 | + | ||
| 176 | + ## 5. Use real `Blob` and `File` instances | ||
| 177 | + | ||
| 178 | + Undici v8 no longer accepts fake Blob-like values that only imitate `Blob` or | ||
| 179 | + `File` via properties such as `Symbol.toStringTag`. | ||
| 180 | + | ||
| 181 | + If you were passing custom objects that looked like `Blob`s, replace them with | ||
| 182 | + actual `Blob` or `File` instances: | ||
| 183 | + | ||
| 184 | + ```js | ||
| 185 | + const body = new Blob(['hello']) | ||
| 186 | + ``` | ||
| 187 | + | ||
| 188 | + ## 6. Avoid depending on the internal global dispatcher symbol | ||
| 189 | + | ||
| 190 | + `setGlobalDispatcher()` and `getGlobalDispatcher()` remain the public APIs and | ||
| 191 | + should continue to be used. | ||
| 192 | + | ||
| 193 | + Internally, Undici v8 stores its dispatcher under | ||
| 194 | + `Symbol.for('undici.globalDispatcher.2')` and mirrors a v1-compatible wrapper | ||
| 195 | + for legacy consumers such as Node.js built-in `fetch`. | ||
| 196 | + | ||
| 197 | + If your code was reading or writing `Symbol.for('undici.globalDispatcher.1')` | ||
| 198 | + directly, migrate to the public APIs instead: | ||
| 199 | + | ||
| 200 | + ```js | ||
| 201 | + import { setGlobalDispatcher, getGlobalDispatcher, Agent } from 'undici' | ||
| 202 | + | ||
| 203 | + setGlobalDispatcher(new Agent()) | ||
| 204 | + const dispatcher = getGlobalDispatcher() | ||
| 205 | + ``` | ||
| 206 | + | ||
| 207 | + If you must expose a dispatcher to legacy v1 handler consumers, wrap it with | ||
| 208 | + `Dispatcher1Wrapper`: | ||
| 209 | + | ||
| 210 | + ```js | ||
| 211 | + import { Agent, Dispatcher1Wrapper } from 'undici' | ||
| 212 | + | ||
| 213 | + const legacyCompatibleDispatcher = new Dispatcher1Wrapper(new Agent()) | ||
| 214 | + ``` | ||
| 215 | + | ||
| 216 | + ## 7. Verify the upgrade | ||
| 217 | + | ||
| 218 | + After moving to Undici v8, it is worth checking these paths in your test suite: | ||
| 219 | + | ||
| 220 | + - requests that use a custom `dispatcher` | ||
| 221 | + - `setGlobalDispatcher()` behavior | ||
| 222 | + - any custom interceptor or retry handler | ||
| 223 | + - uploads that use `Blob`, `File`, or `FormData` | ||
| 224 | + - integrations that depend on HTTP/1.1-only behavior | ||
| 225 | + | ||
| 226 | + ## Related documentation | ||
| 227 | + | ||
| 228 | + - [Dispatcher](/docs/api/Dispatcher.md) | ||
| 229 | + - [Client](/docs/api/Client.md) | ||
| 230 | + - [Global Installation](/docs/api/GlobalInstallation.md) | ||
| 231 | + - [Undici Module vs. Node.js Built-in Fetch](/docs/best-practices/undici-vs-builtin-fetch.md) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -105,14 +105,14 @@ function makeDispatcher (fn) { | |||
| 105 | 105 | url = util.parseURL(url) | |
| 106 | 106 | } | |
| 107 | 107 | ||
| 108 | - const { agent, dispatcher = getGlobalDispatcher() } = opts | ||
| 108 | + const { agent, dispatcher = getGlobalDispatcher(), ...restOpts } = opts | ||
| 109 | 109 | ||
| 110 | 110 | if (agent) { | |
| 111 | 111 | throw new InvalidArgumentError('unsupported opts.agent. Did you mean opts.client?') | |
| 112 | 112 | } | |
| 113 | 113 | ||
| 114 | 114 | return fn.call(dispatcher, { | |
| 115 | - ...opts, | ||
| 115 | + ...restOpts, | ||
| 116 | 116 | origin: url.origin, | |
| 117 | 117 | path: url.search ? `${url.pathname}${url.search}` : url.pathname, | |
| 118 | 118 | method: opts.method || (opts.body ? 'PUT' : 'GET') | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,8 +12,6 @@ const { InvalidArgumentError, ConnectTimeoutError } = require('./errors') | |||
| 12 | 12 | const { headerNameLowerCasedRecord } = require('./constants') | |
| 13 | 13 | const { tree } = require('./tree') | |
| 14 | 14 | ||
| 15 | - const [nodeMajor, nodeMinor] = process.versions.node.split('.', 2).map(v => Number(v)) | ||
| 16 | - | ||
| 17 | 15 | class BodyAsyncIterable { | |
| 18 | 16 | constructor (body) { | |
| 19 | 17 | this[kBody] = body | |
@@ -323,7 +321,7 @@ function isIterable (obj) { | |||
| 323 | 321 | */ | |
| 324 | 322 | function hasSafeIterator (obj) { | |
| 325 | 323 | const prototype = Object.getPrototypeOf(obj) | |
| 326 | - const ownIterator = Object.prototype.hasOwnProperty.call(obj, Symbol.iterator) | ||
| 324 | + const ownIterator = Object.hasOwn(obj, Symbol.iterator) | ||
| 327 | 325 | return ownIterator || (prototype != null && prototype !== Object.prototype && typeof obj[Symbol.iterator] === 'function') | |
| 328 | 326 | } | |
| 329 | 327 | ||
@@ -989,8 +987,6 @@ module.exports = { | |||
| 989 | 987 | normalizedMethodRecords, | |
| 990 | 988 | isValidPort, | |
| 991 | 989 | isHttpOrHttpsPrefixed, | |
| 992 | - nodeMajor, | ||
| 993 | - nodeMinor, | ||
| 994 | 990 | safeHTTPMethods: Object.freeze(['GET', 'HEAD', 'OPTIONS', 'TRACE']), | |
| 995 | 991 | wrapRequestBody, | |
| 996 | 992 | setupConnectTimeout, | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -35,7 +35,7 @@ class Agent extends DispatcherBase { | |||
| 35 | 35 | throw new InvalidArgumentError('maxOrigins must be a number greater than 0') | |
| 36 | 36 | } | |
| 37 | 37 | ||
| 38 | - super() | ||
| 38 | + super(options) | ||
| 39 | 39 | ||
| 40 | 40 | if (connect && typeof connect !== 'function') { | |
| 41 | 41 | connect = { ...connect } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -57,9 +57,6 @@ class BalancedPool extends PoolBase { | |||
| 57 | 57 | super() | |
| 58 | 58 | ||
| 59 | 59 | this[kOptions] = { ...util.deepClone(opts) } | |
| 60 | - this[kOptions].interceptors = opts.interceptors | ||
| 61 | - ? { ...opts.interceptors } | ||
| 62 | - : undefined | ||
| 63 | 60 | this[kIndex] = -1 | |
| 64 | 61 | this[kCurrentWeight] = 0 | |
| 65 | 62 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -114,7 +114,8 @@ class Client extends DispatcherBase { | |||
| 114 | 114 | useH2c, | |
| 115 | 115 | initialWindowSize, | |
| 116 | 116 | connectionWindowSize, | |
| 117 | - pingInterval | ||
| 117 | + pingInterval, | ||
| 118 | + webSocket | ||
| 118 | 119 | } = {}) { | |
| 119 | 120 | if (keepAlive !== undefined) { | |
| 120 | 121 | throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') | |
@@ -222,7 +223,7 @@ class Client extends DispatcherBase { | |||
| 222 | 223 | throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') | |
| 223 | 224 | } | |
| 224 | 225 | ||
| 225 | - super() | ||
| 226 | + super({ webSocket }) | ||
| 226 | 227 | ||
| 227 | 228 | if (typeof connect !== 'function') { | |
| 228 | 229 | connect = buildConnector({ | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -10,6 +10,7 @@ const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = require('../core/sy | |||
| 10 | 10 | ||
| 11 | 11 | const kOnDestroyed = Symbol('onDestroyed') | |
| 12 | 12 | const kOnClosed = Symbol('onClosed') | |
| 13 | + const kWebSocketOptions = Symbol('webSocketOptions') | ||
| 13 | 14 | ||
| 14 | 15 | class DispatcherBase extends Dispatcher { | |
| 15 | 16 | /** @type {boolean} */ | |
@@ -24,6 +25,23 @@ class DispatcherBase extends Dispatcher { | |||
| 24 | 25 | /** @type {Array<Function>|null} */ | |
| 25 | 26 | [kOnClosed] = null | |
| 26 | 27 | ||
| 28 | + /** | ||
| 29 | + * @param {import('../../types/dispatcher').DispatcherOptions} [opts] | ||
| 30 | + */ | ||
| 31 | + constructor (opts) { | ||
| 32 | + super() | ||
| 33 | + this[kWebSocketOptions] = opts?.webSocket ?? {} | ||
| 34 | + } | ||
| 35 | + | ||
| 36 | + /** | ||
| 37 | + * @returns {import('../../types/dispatcher').WebSocketOptions} | ||
| 38 | + */ | ||
| 39 | + get webSocketOptions () { | ||
| 40 | + return { | ||
| 41 | + maxPayloadSize: this[kWebSocketOptions].maxPayloadSize ?? 128 * 1024 * 1024 // 128 MB default | ||
| 42 | + } | ||
| 43 | + } | ||
| 44 | + | ||
| 27 | 45 | /** @returns {boolean} */ | |
| 28 | 46 | get destroyed () { | |
| 29 | 47 | return this[kDestroyed] | |
@@ -138,6 +156,10 @@ class DispatcherBase extends Dispatcher { | |||
| 138 | 156 | throw new InvalidArgumentError('opts must be an object.') | |
| 139 | 157 | } | |
| 140 | 158 | ||
| 159 | + if (opts.dispatcher) { | ||
| 160 | + 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.') | ||
| 161 | + } | ||
| 162 | + | ||
| 141 | 163 | if (this[kDestroyed] || this[kOnDestroyed]) { | |
| 142 | 164 | throw new ClientDestroyedError() | |
| 143 | 165 | } | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,6 +86,12 @@ class Dispatcher1Wrapper extends Dispatcher { | |||
| 86 | 86 | } | |
| 87 | 87 | ||
| 88 | 88 | dispatch (opts, handler) { | |
| 89 | + // Legacy (v1) consumers do not support HTTP/2, so force HTTP/1.1. | ||
| 90 | + // See https://github.com/nodejs/undici/issues/4989 | ||
| 91 | + if (opts.allowH2 !== false) { | ||
| 92 | + opts = { ...opts, allowH2: false } | ||
| 93 | + } | ||
| 94 | + | ||
| 89 | 95 | return this.#dispatcher.dispatch(opts, Dispatcher1Wrapper.wrapHandler(handler)) | |
| 90 | 96 | } | |
| 91 | 97 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments