| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| 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 | ||
| 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. | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,108 @@ | |||
| 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 | + ``` | ||
| 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 | |
| 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 | ||
| 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) { | |
| 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 | } | |
| 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 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments