GeezerVison is showing github.com — deps: update undici to 5.28.0 · nodejs/node@4b1bed0
File treeExpand file treeCollapse file tree Open diff view settings doc/contributing/maintaining
Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: deps/undici/src/docs/api/Client.md +1-1Lines changed: 1 addition & 1 deletion - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -33,7 +33,7 @@ Returns: `Client` | 33 | 33 | * **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. | 34 | 34 | * **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. | 35 | 35 | * **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation. | 36 | | -* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overriden by a SETTINGS remote frame. | | 36 | +* **maxConcurrentStreams**: `number` - Default: `100`. Dictates the maximum number of concurrent streams for a single H2 session. It can be overridden by a SETTINGS remote frame. | 37 | 37 |
| 38 | 38 | #### Parameter: `ConnectOptions` | 39 | 39 |
| |
Collapse file Copy file name to clipboardExpand all lines: deps/undici/src/docs/api/MockPool.md +38-4Lines changed: 38 additions & 4 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -35,8 +35,7 @@ const mockPool = mockAgent.get('http://localhost:3000') | 35 | 35 |
| 36 | 36 | ### `MockPool.intercept(options)` | 37 | 37 |
| 38 | | -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. | 39 | | -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. | | 38 | +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. | 40 | 39 |
| 41 | 40 | 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. | 42 | 41 |
| @@ -54,11 +53,11 @@ Returns: `MockInterceptor` corresponding to the input options. | 54 | 53 |
| 55 | 54 | ### Parameter: `MockPoolInterceptOptions` | 56 | 55 |
| 57 | | -* **path** `string | RegExp | (path: string) => boolean` - a matcher for the HTTP request path. | | 56 | +* **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. | 58 | 57 | * **method** `string | RegExp | (method: string) => boolean` - (optional) - a matcher for the HTTP request method. Defaults to `GET`. | 59 | 58 | * **body** `string | RegExp | (body: string) => boolean` - (optional) - a matcher for the HTTP request body. | 60 | 59 | * **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. | 61 | | -* **query** `Record<string, any> | null` - (optional) - a matcher for the HTTP request query string params. | | 60 | +* **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`. | 62 | 61 |
| 63 | 62 | ### Return: `MockInterceptor` | 64 | 63 |
| @@ -458,6 +457,41 @@ const result3 = await request('http://localhost:3000/foo') | 458 | 457 | // Will not match and make attempt a real request | 459 | 458 | ``` | 460 | 459 |
| | 460 | +#### Example - Mocked request with path callback | | 461 | + | | 462 | +```js | | 463 | +import { MockAgent, setGlobalDispatcher, request } from 'undici' | | 464 | +import querystring from 'querystring' | | 465 | + | | 466 | +const mockAgent = new MockAgent() | | 467 | +setGlobalDispatcher(mockAgent) | | 468 | + | | 469 | +const mockPool = mockAgent.get('http://localhost:3000') | | 470 | + | | 471 | +const matchPath = requestPath => { | | 472 | + const [pathname, search] = requestPath.split('?') | | 473 | + const requestQuery = querystring.parse(search) | | 474 | + | | 475 | + if (!pathname.startsWith('/foo')) { | | 476 | + return false | | 477 | + } | | 478 | + | | 479 | + if (!Object.keys(requestQuery).includes('foo') || requestQuery.foo !== 'bar') { | | 480 | + return false | | 481 | + } | | 482 | + | | 483 | + return true | | 484 | +} | | 485 | + | | 486 | +mockPool.intercept({ | | 487 | + path: matchPath, | | 488 | + method: 'GET' | | 489 | +}).reply(200, 'foo') | | 490 | + | | 491 | +const result = await request('http://localhost:3000/foo?foo=bar') | | 492 | +// Will match and return mocked data | | 493 | +``` | | 494 | + | 461 | 495 | ### `MockPool.close()` | 462 | 496 |
| 463 | 497 | Closes the mock pool and de-registers from associated MockAgent. | |
Collapse file Copy file name to clipboard +108Lines changed: 108 additions & 0 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +# Class: RetryHandler | | 2 | + | | 3 | +Extends: `undici.DispatcherHandlers` | | 4 | + | | 5 | +A handler class that implements the retry logic for a request. | | 6 | + | | 7 | +## `new RetryHandler(dispatchOptions, retryHandlers, [retryOptions])` | | 8 | + | | 9 | +Arguments: | | 10 | + | | 11 | +- **options** `Dispatch.DispatchOptions & RetryOptions` (required) - It is an intersection of `Dispatcher.DispatchOptions` and `RetryOptions`. | | 12 | +- **retryHandlers** `RetryHandlers` (required) - Object containing the `dispatch` to be used on every retry, and `handler` for handling the `dispatch` lifecycle. | | 13 | + | | 14 | +Returns: `retryHandler` | | 15 | + | | 16 | +### Parameter: `Dispatch.DispatchOptions & RetryOptions` | | 17 | + | | 18 | +Extends: [`Dispatch.DispatchOptions`](Dispatcher.md#parameter-dispatchoptions). | | 19 | + | | 20 | +#### `RetryOptions` | | 21 | + | | 22 | +- **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. | | 23 | +- **maxRetries** `number` (optional) - Maximum number of retries. Default: `5` | | 24 | +- **maxTimeout** `number` (optional) - Maximum number of milliseconds to wait before retrying. Default: `30000` (30 seconds) | | 25 | +- **minTimeout** `number` (optional) - Minimum number of milliseconds to wait before retrying. Default: `500` (half a second) | | 26 | +- **timeoutFactor** `number` (optional) - Factor to multiply the timeout by for each retry attempt. Default: `2` | | 27 | +- **retryAfter** `boolean` (optional) - It enables automatic retry after the `Retry-After` header is received. Default: `true` | | 28 | +- | | 29 | +- **methods** `string[]` (optional) - Array of HTTP methods to retry. Default: `['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE']` | | 30 | +- **statusCodes** `number[]` (optional) - Array of HTTP status codes to retry. Default: `[429, 500, 502, 503, 504]` | | 31 | +- **errorCodes** `string[]` (optional) - Array of Error codes to retry. Default: `['ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'ENETDOWN','ENETUNREACH', 'EHOSTDOWN', | | 32 | + | | 33 | +**`RetryContext`** | | 34 | + | | 35 | +- `state`: `RetryState` - Current retry state. It can be mutated. | | 36 | +- `opts`: `Dispatch.DispatchOptions & RetryOptions` - Options passed to the retry handler. | | 37 | + | | 38 | +### Parameter `RetryHandlers` | | 39 | + | | 40 | +- **dispatch** `(options: Dispatch.DispatchOptions, handlers: Dispatch.DispatchHandlers) => Promise<Dispatch.DispatchResponse>` (required) - Dispatch function to be called after every retry. | | 41 | +- **handler** Extends [`Dispatch.DispatchHandlers`](Dispatcher.md#dispatcherdispatchoptions-handler) (required) - Handler function to be called after the request is successful or the retries are exhausted. | | 42 | + | | 43 | +Examples: | | 44 | + | | 45 | +```js | | 46 | +const client = new Client(`http://localhost:${server.address().port}`); | | 47 | +const chunks = []; | | 48 | +const handler = new RetryHandler( | | 49 | + { | | 50 | + ...dispatchOptions, | | 51 | + retryOptions: { | | 52 | + // custom retry function | | 53 | + retry: function (err, state, callback) { | | 54 | + counter++; | | 55 | + | | 56 | + if (err.code && err.code === "UND_ERR_DESTROYED") { | | 57 | + callback(err); | | 58 | + return; | | 59 | + } | | 60 | + | | 61 | + if (err.statusCode === 206) { | | 62 | + callback(err); | | 63 | + return; | | 64 | + } | | 65 | + | | 66 | + setTimeout(() => callback(null), 1000); | | 67 | + }, | | 68 | + }, | | 69 | + }, | | 70 | + { | | 71 | + dispatch: (...args) => { | | 72 | + return client.dispatch(...args); | | 73 | + }, | | 74 | + handler: { | | 75 | + onConnect() {}, | | 76 | + onBodySent() {}, | | 77 | + onHeaders(status, _rawHeaders, resume, _statusMessage) { | | 78 | + // do something with headers | | 79 | + }, | | 80 | + onData(chunk) { | | 81 | + chunks.push(chunk); | | 82 | + return true; | | 83 | + }, | | 84 | + onComplete() {}, | | 85 | + onError() { | | 86 | + // handle error properly | | 87 | + }, | | 88 | + }, | | 89 | + } | | 90 | +); | | 91 | +``` | | 92 | + | | 93 | +#### Example - Basic RetryHandler with defaults | | 94 | + | | 95 | +```js | | 96 | +const client = new Client(`http://localhost:${server.address().port}`); | | 97 | +const handler = new RetryHandler(dispatchOptions, { | | 98 | + dispatch: client.dispatch.bind(client), | | 99 | + handler: { | | 100 | + onConnect() {}, | | 101 | + onBodySent() {}, | | 102 | + onHeaders(status, _rawHeaders, resume, _statusMessage) {}, | | 103 | + onData(chunk) {}, | | 104 | + onComplete() {}, | | 105 | + onError(err) {}, | | 106 | + }, | | 107 | +}); | | 108 | +``` |
Collapse file Copy file name to clipboardExpand all lines: deps/undici/src/index.js +2Lines changed: 2 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -15,6 +15,7 @@ const MockAgent = require('./lib/mock/mock-agent') | 15 | 15 | const MockPool = require('./lib/mock/mock-pool') | 16 | 16 | const mockErrors = require('./lib/mock/mock-errors') | 17 | 17 | const ProxyAgent = require('./lib/proxy-agent') | | 18 | +const RetryHandler = require('./lib/handler/RetryHandler') | 18 | 19 | const { getGlobalDispatcher, setGlobalDispatcher } = require('./lib/global') | 19 | 20 | const DecoratorHandler = require('./lib/handler/DecoratorHandler') | 20 | 21 | const RedirectHandler = require('./lib/handler/RedirectHandler') | @@ -36,6 +37,7 @@ module.exports.Pool = Pool | 36 | 37 | module.exports.BalancedPool = BalancedPool | 37 | 38 | module.exports.Agent = Agent | 38 | 39 | module.exports.ProxyAgent = ProxyAgent | | 40 | +module.exports.RetryHandler = RetryHandler | 39 | 41 |
| 40 | 42 | module.exports.DecoratorHandler = DecoratorHandler | 41 | 43 | module.exports.RedirectHandler = RedirectHandler | |
Collapse file Copy file name to clipboardExpand all lines: deps/undici/src/lib/api/readable.js +40-25Lines changed: 40 additions & 25 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -16,6 +16,8 @@ const kBody = Symbol('kBody') | 16 | 16 | const kAbort = Symbol('abort') | 17 | 17 | const kContentType = Symbol('kContentType') | 18 | 18 |
| | 19 | +const noop = () => {} | | 20 | + | 19 | 21 | module.exports = class BodyReadable extends Readable { | 20 | 22 | constructor ({ | 21 | 23 | resume, | @@ -149,37 +151,50 @@ module.exports = class BodyReadable extends Readable { | 149 | 151 | return this[kBody] | 150 | 152 | } | 151 | 153 |
| 152 | | - async dump (opts) { | | 154 | + dump (opts) { | 153 | 155 | let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144 | 154 | 156 | const signal = opts && opts.signal | 155 | | - const abortFn = () => { | 156 | | - this.destroy() | 157 | | - } | 158 | | - let signalListenerCleanup | | 157 | + | 159 | 158 | if (signal) { | 160 | | - if (typeof signal !== 'object' || !('aborted' in signal)) { | 161 | | - throw new InvalidArgumentError('signal must be an AbortSignal') | 162 | | - } | 163 | | - util.throwIfAborted(signal) | 164 | | - signalListenerCleanup = util.addAbortListener(signal, abortFn) | 165 | | - } | 166 | | - try { | 167 | | - for await (const chunk of this) { | 168 | | - util.throwIfAborted(signal) | 169 | | - limit -= Buffer.byteLength(chunk) | 170 | | - if (limit < 0) { | 171 | | - return | | 159 | + try { | | 160 | + if (typeof signal !== 'object' || !('aborted' in signal)) { | | 161 | + throw new InvalidArgumentError('signal must be an AbortSignal') | 172 | 162 | } | | 163 | + util.throwIfAborted(signal) | | 164 | + } catch (err) { | | 165 | + return Promise.reject(err) | 173 | 166 | } | 174 | | - } catch { | 175 | | - util.throwIfAborted(signal) | 176 | | - } finally { | 177 | | - if (typeof signalListenerCleanup === 'function') { | 178 | | - signalListenerCleanup() | 179 | | - } else if (signalListenerCleanup) { | 180 | | - signalListenerCleanup[Symbol.dispose]() | 181 | | - } | 182 | 167 | } | | 168 | + | | 169 | + if (this.closed) { | | 170 | + return Promise.resolve(null) | | 171 | + } | | 172 | + | | 173 | + return new Promise((resolve, reject) => { | | 174 | + const signalListenerCleanup = signal | | 175 | + ? util.addAbortListener(signal, () => { | | 176 | + this.destroy() | | 177 | + }) | | 178 | + : noop | | 179 | + | | 180 | + this | | 181 | + .on('close', function () { | | 182 | + signalListenerCleanup() | | 183 | + if (signal?.aborted) { | | 184 | + reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })) | | 185 | + } else { | | 186 | + resolve(null) | | 187 | + } | | 188 | + }) | | 189 | + .on('error', noop) | | 190 | + .on('data', function (chunk) { | | 191 | + limit -= chunk.length | | 192 | + if (limit <= 0) { | | 193 | + this.destroy() | | 194 | + } | | 195 | + }) | | 196 | + .resume() | | 197 | + }) | 183 | 198 | } | 184 | 199 | } | 185 | 200 |
| |
Collapse file Copy file name to clipboardExpand all lines: deps/undici/src/lib/client.js +10-7Lines changed: 10 additions & 7 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -1183,7 +1183,7 @@ async function connect (client) { | 1183 | 1183 | const idx = hostname.indexOf(']') | 1184 | 1184 |
| 1185 | 1185 | assert(idx !== -1) | 1186 | | - const ip = hostname.substr(1, idx - 1) | | 1186 | + const ip = hostname.substring(1, idx) | 1187 | 1187 |
| 1188 | 1188 | assert(net.isIP(ip)) | 1189 | 1189 | hostname = ip | @@ -1682,6 +1682,7 @@ function writeH2 (client, session, request) { | 1682 | 1682 | return false | 1683 | 1683 | } | 1684 | 1684 |
| | 1685 | + /** @type {import('node:http2').ClientHttp2Stream} */ | 1685 | 1686 | let stream | 1686 | 1687 | const h2State = client[kHTTP2SessionState] | 1687 | 1688 |
| @@ -1777,14 +1778,10 @@ function writeH2 (client, session, request) { | 1777 | 1778 | const shouldEndStream = method === 'GET' || method === 'HEAD' | 1778 | 1779 | if (expectContinue) { | 1779 | 1780 | headers[HTTP2_HEADER_EXPECT] = '100-continue' | 1780 | | - /** | 1781 | | - * @type {import('node:http2').ClientHttp2Stream} | 1782 | | - */ | 1783 | 1781 | stream = session.request(headers, { endStream: shouldEndStream, signal }) | 1784 | 1782 |
| 1785 | 1783 | stream.once('continue', writeBodyH2) | 1786 | 1784 | } else { | 1787 | | - /** @type {import('node:http2').ClientHttp2Stream} */ | 1788 | 1785 | stream = session.request(headers, { | 1789 | 1786 | endStream: shouldEndStream, | 1790 | 1787 | signal | @@ -1796,7 +1793,9 @@ function writeH2 (client, session, request) { | 1796 | 1793 | ++h2State.openStreams | 1797 | 1794 |
| 1798 | 1795 | stream.once('response', headers => { | 1799 | | - if (request.onHeaders(Number(headers[HTTP2_HEADER_STATUS]), headers, stream.resume.bind(stream), '') === false) { | | 1796 | + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers | | 1797 | + | | 1798 | + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { | 1800 | 1799 | stream.pause() | 1801 | 1800 | } | 1802 | 1801 | }) | @@ -1972,7 +1971,11 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength, | 1972 | 1971 | } | 1973 | 1972 | } | 1974 | 1973 | const onAbort = function () { | 1975 | | - onFinished(new RequestAbortedError()) | | 1974 | + if (finished) { | | 1975 | + return | | 1976 | + } | | 1977 | + const err = new RequestAbortedError() | | 1978 | + queueMicrotask(() => onFinished(err)) | 1976 | 1979 | } | 1977 | 1980 | const onFinished = function (err) { | 1978 | 1981 | if (finished) { | |
Collapse file Copy file name to clipboardExpand all lines: deps/undici/src/lib/core/errors.js +15-1Lines changed: 15 additions & 1 deletion | Original file line number | Diff line number | Diff line change |
|---|
@@ -193,6 +193,19 @@ class ResponseExceededMaxSizeError extends UndiciError { | 193 | 193 | } | 194 | 194 | } | 195 | 195 |
| | 196 | +class RequestRetryError extends UndiciError { | | 197 | + constructor (message, code, { headers, data }) { | | 198 | + super(message) | | 199 | + Error.captureStackTrace(this, RequestRetryError) | | 200 | + this.name = 'RequestRetryError' | | 201 | + this.message = message || 'Request retry error' | | 202 | + this.code = 'UND_ERR_REQ_RETRY' | | 203 | + this.statusCode = code | | 204 | + this.data = data | | 205 | + this.headers = headers | | 206 | + } | | 207 | +} | | 208 | + | 196 | 209 | module.exports = { | 197 | 210 | HTTPParserError, | 198 | 211 | UndiciError, | @@ -212,5 +225,6 @@ module.exports = { | 212 | 225 | NotSupportedError, | 213 | 226 | ResponseContentLengthMismatchError, | 214 | 227 | BalancedPoolMissingUpstreamError, | 215 | | - ResponseExceededMaxSizeError | | 228 | + ResponseExceededMaxSizeError, | | 229 | + RequestRetryError | 216 | 230 | } |
Collapse file Copy file name to clipboardExpand all lines: deps/undici/src/lib/core/request.js +2-10Lines changed: 2 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -229,11 +229,7 @@ class Request { | 229 | 229 |
| 230 | 230 | onBodySent (chunk) { | 231 | 231 | if (this[kHandler].onBodySent) { | 232 | | - try { | 233 | | - this[kHandler].onBodySent(chunk) | 234 | | - } catch (err) { | 235 | | - this.onError(err) | 236 | | - } | | 232 | + return this[kHandler].onBodySent(chunk) | 237 | 233 | } | 238 | 234 | } | 239 | 235 |
| @@ -243,11 +239,7 @@ class Request { | 243 | 239 | } | 244 | 240 |
| 245 | 241 | if (this[kHandler].onRequestSent) { | 246 | | - try { | 247 | | - this[kHandler].onRequestSent() | 248 | | - } catch (err) { | 249 | | - this.onError(err) | 250 | | - } | | 242 | + return this[kHandler].onRequestSent() | 251 | 243 | } | 252 | 244 | } | 253 | 245 |
| |
|