undici 5.27.2 → 5.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/api/Client.md +1 -1
- package/docs/api/MockPool.md +38 -4
- package/docs/api/RetryHandler.md +108 -0
- package/index.js +2 -0
- package/lib/api/readable.js +40 -25
- package/lib/client.js +22 -26
- package/lib/core/errors.js +15 -1
- package/lib/core/request.js +23 -7
- package/lib/core/symbols.js +2 -1
- package/lib/core/util.js +21 -13
- package/lib/fetch/headers.js +93 -59
- package/lib/fetch/index.js +4 -4
- package/lib/fetch/request.js +18 -15
- package/lib/fetch/util.js +63 -38
- package/lib/fetch/webidl.js +2 -4
- package/lib/handler/RetryHandler.js +336 -0
- package/package.json +2 -1
- package/types/client.d.ts +1 -1
- package/types/dispatcher.d.ts +1 -1
- package/types/index.d.ts +3 -1
- package/types/retry-handler.d.ts +116 -0
package/docs/api/Client.md
CHANGED
|
@@ -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
|
|
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
|
|
package/docs/api/MockPool.md
CHANGED
|
@@ -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.
|
|
@@ -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
|
+
```
|
package/index.js
CHANGED
|
@@ -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
|
package/lib/api/readable.js
CHANGED
|
@@ -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
|
-
|
|
154
|
+
dump (opts) {
|
|
153
155
|
let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144
|
|
154
156
|
const signal = opts && opts.signal
|
|
155
|
-
|
|
156
|
-
this.destroy()
|
|
157
|
-
}
|
|
158
|
-
let signalListenerCleanup
|
|
157
|
+
|
|
159
158
|
if (signal) {
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
package/lib/client.js
CHANGED
|
@@ -917,11 +917,9 @@ class Parser {
|
|
|
917
917
|
socket[kReset] = true
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
} catch (err) {
|
|
924
|
-
util.destroy(socket, err)
|
|
920
|
+
const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false
|
|
921
|
+
|
|
922
|
+
if (request.aborted) {
|
|
925
923
|
return -1
|
|
926
924
|
}
|
|
927
925
|
|
|
@@ -968,13 +966,8 @@ class Parser {
|
|
|
968
966
|
|
|
969
967
|
this.bytesRead += buf.length
|
|
970
968
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
return constants.ERROR.PAUSED
|
|
974
|
-
}
|
|
975
|
-
} catch (err) {
|
|
976
|
-
util.destroy(socket, err)
|
|
977
|
-
return -1
|
|
969
|
+
if (request.onData(buf) === false) {
|
|
970
|
+
return constants.ERROR.PAUSED
|
|
978
971
|
}
|
|
979
972
|
}
|
|
980
973
|
|
|
@@ -1015,11 +1008,7 @@ class Parser {
|
|
|
1015
1008
|
return -1
|
|
1016
1009
|
}
|
|
1017
1010
|
|
|
1018
|
-
|
|
1019
|
-
request.onComplete(headers)
|
|
1020
|
-
} catch (err) {
|
|
1021
|
-
errorRequest(client, request, err)
|
|
1022
|
-
}
|
|
1011
|
+
request.onComplete(headers)
|
|
1023
1012
|
|
|
1024
1013
|
client[kQueue][client[kRunningIdx]++] = null
|
|
1025
1014
|
|
|
@@ -1183,7 +1172,7 @@ async function connect (client) {
|
|
|
1183
1172
|
const idx = hostname.indexOf(']')
|
|
1184
1173
|
|
|
1185
1174
|
assert(idx !== -1)
|
|
1186
|
-
const ip = hostname.
|
|
1175
|
+
const ip = hostname.substring(1, idx)
|
|
1187
1176
|
|
|
1188
1177
|
assert(net.isIP(ip))
|
|
1189
1178
|
hostname = ip
|
|
@@ -1682,6 +1671,7 @@ function writeH2 (client, session, request) {
|
|
|
1682
1671
|
return false
|
|
1683
1672
|
}
|
|
1684
1673
|
|
|
1674
|
+
/** @type {import('node:http2').ClientHttp2Stream} */
|
|
1685
1675
|
let stream
|
|
1686
1676
|
const h2State = client[kHTTP2SessionState]
|
|
1687
1677
|
|
|
@@ -1777,14 +1767,10 @@ function writeH2 (client, session, request) {
|
|
|
1777
1767
|
const shouldEndStream = method === 'GET' || method === 'HEAD'
|
|
1778
1768
|
if (expectContinue) {
|
|
1779
1769
|
headers[HTTP2_HEADER_EXPECT] = '100-continue'
|
|
1780
|
-
/**
|
|
1781
|
-
* @type {import('node:http2').ClientHttp2Stream}
|
|
1782
|
-
*/
|
|
1783
1770
|
stream = session.request(headers, { endStream: shouldEndStream, signal })
|
|
1784
1771
|
|
|
1785
1772
|
stream.once('continue', writeBodyH2)
|
|
1786
1773
|
} else {
|
|
1787
|
-
/** @type {import('node:http2').ClientHttp2Stream} */
|
|
1788
1774
|
stream = session.request(headers, {
|
|
1789
1775
|
endStream: shouldEndStream,
|
|
1790
1776
|
signal
|
|
@@ -1796,7 +1782,9 @@ function writeH2 (client, session, request) {
|
|
|
1796
1782
|
++h2State.openStreams
|
|
1797
1783
|
|
|
1798
1784
|
stream.once('response', headers => {
|
|
1799
|
-
|
|
1785
|
+
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
1786
|
+
|
|
1787
|
+
if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) {
|
|
1800
1788
|
stream.pause()
|
|
1801
1789
|
}
|
|
1802
1790
|
})
|
|
@@ -1806,13 +1794,17 @@ function writeH2 (client, session, request) {
|
|
|
1806
1794
|
})
|
|
1807
1795
|
|
|
1808
1796
|
stream.on('data', (chunk) => {
|
|
1809
|
-
if (request.onData(chunk) === false)
|
|
1797
|
+
if (request.onData(chunk) === false) {
|
|
1798
|
+
stream.pause()
|
|
1799
|
+
}
|
|
1810
1800
|
})
|
|
1811
1801
|
|
|
1812
1802
|
stream.once('close', () => {
|
|
1813
1803
|
h2State.openStreams -= 1
|
|
1814
1804
|
// TODO(HTTP/2): unref only if current streams count is 0
|
|
1815
|
-
if (h2State.openStreams === 0)
|
|
1805
|
+
if (h2State.openStreams === 0) {
|
|
1806
|
+
session.unref()
|
|
1807
|
+
}
|
|
1816
1808
|
})
|
|
1817
1809
|
|
|
1818
1810
|
stream.once('error', function (err) {
|
|
@@ -1972,7 +1964,11 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength,
|
|
|
1972
1964
|
}
|
|
1973
1965
|
}
|
|
1974
1966
|
const onAbort = function () {
|
|
1975
|
-
|
|
1967
|
+
if (finished) {
|
|
1968
|
+
return
|
|
1969
|
+
}
|
|
1970
|
+
const err = new RequestAbortedError()
|
|
1971
|
+
queueMicrotask(() => onFinished(err))
|
|
1976
1972
|
}
|
|
1977
1973
|
const onFinished = function (err) {
|
|
1978
1974
|
if (finished) {
|
package/lib/core/errors.js
CHANGED
|
@@ -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
|
}
|
package/lib/core/request.js
CHANGED
|
@@ -230,9 +230,9 @@ class Request {
|
|
|
230
230
|
onBodySent (chunk) {
|
|
231
231
|
if (this[kHandler].onBodySent) {
|
|
232
232
|
try {
|
|
233
|
-
this[kHandler].onBodySent(chunk)
|
|
233
|
+
return this[kHandler].onBodySent(chunk)
|
|
234
234
|
} catch (err) {
|
|
235
|
-
this.
|
|
235
|
+
this.abort(err)
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
}
|
|
@@ -244,9 +244,9 @@ class Request {
|
|
|
244
244
|
|
|
245
245
|
if (this[kHandler].onRequestSent) {
|
|
246
246
|
try {
|
|
247
|
-
this[kHandler].onRequestSent()
|
|
247
|
+
return this[kHandler].onRequestSent()
|
|
248
248
|
} catch (err) {
|
|
249
|
-
this.
|
|
249
|
+
this.abort(err)
|
|
250
250
|
}
|
|
251
251
|
}
|
|
252
252
|
}
|
|
@@ -271,14 +271,23 @@ class Request {
|
|
|
271
271
|
channels.headers.publish({ request: this, response: { statusCode, headers, statusText } })
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
|
|
274
|
+
try {
|
|
275
|
+
return this[kHandler].onHeaders(statusCode, headers, resume, statusText)
|
|
276
|
+
} catch (err) {
|
|
277
|
+
this.abort(err)
|
|
278
|
+
}
|
|
275
279
|
}
|
|
276
280
|
|
|
277
281
|
onData (chunk) {
|
|
278
282
|
assert(!this.aborted)
|
|
279
283
|
assert(!this.completed)
|
|
280
284
|
|
|
281
|
-
|
|
285
|
+
try {
|
|
286
|
+
return this[kHandler].onData(chunk)
|
|
287
|
+
} catch (err) {
|
|
288
|
+
this.abort(err)
|
|
289
|
+
return false
|
|
290
|
+
}
|
|
282
291
|
}
|
|
283
292
|
|
|
284
293
|
onUpgrade (statusCode, headers, socket) {
|
|
@@ -297,7 +306,13 @@ class Request {
|
|
|
297
306
|
if (channels.trailers.hasSubscribers) {
|
|
298
307
|
channels.trailers.publish({ request: this, trailers })
|
|
299
308
|
}
|
|
300
|
-
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
return this[kHandler].onComplete(trailers)
|
|
312
|
+
} catch (err) {
|
|
313
|
+
// TODO (fix): This might be a bad idea?
|
|
314
|
+
this.onError(err)
|
|
315
|
+
}
|
|
301
316
|
}
|
|
302
317
|
|
|
303
318
|
onError (error) {
|
|
@@ -311,6 +326,7 @@ class Request {
|
|
|
311
326
|
return
|
|
312
327
|
}
|
|
313
328
|
this.aborted = true
|
|
329
|
+
|
|
314
330
|
return this[kHandler].onError(error)
|
|
315
331
|
}
|
|
316
332
|
|
package/lib/core/symbols.js
CHANGED
|
@@ -57,5 +57,6 @@ module.exports = {
|
|
|
57
57
|
kHTTP2BuildRequest: Symbol('http2 build request'),
|
|
58
58
|
kHTTP1BuildRequest: Symbol('http1 build request'),
|
|
59
59
|
kHTTP2CopyHeaders: Symbol('http2 copy headers'),
|
|
60
|
-
kHTTPConnVersion: Symbol('http connection version')
|
|
60
|
+
kHTTPConnVersion: Symbol('http connection version'),
|
|
61
|
+
kRetryHandlerDefaultRetry: Symbol('retry agent default retry')
|
|
61
62
|
}
|
package/lib/core/util.js
CHANGED
|
@@ -125,13 +125,13 @@ function getHostname (host) {
|
|
|
125
125
|
const idx = host.indexOf(']')
|
|
126
126
|
|
|
127
127
|
assert(idx !== -1)
|
|
128
|
-
return host.
|
|
128
|
+
return host.substring(1, idx)
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
const idx = host.indexOf(':')
|
|
132
132
|
if (idx === -1) return host
|
|
133
133
|
|
|
134
|
-
return host.
|
|
134
|
+
return host.substring(0, idx)
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
// IP addresses are not valid server names per RFC6066
|
|
@@ -228,7 +228,7 @@ function parseHeaders (headers, obj = {}) {
|
|
|
228
228
|
|
|
229
229
|
if (!val) {
|
|
230
230
|
if (Array.isArray(headers[i + 1])) {
|
|
231
|
-
obj[key] = headers[i + 1]
|
|
231
|
+
obj[key] = headers[i + 1].map(x => x.toString('utf8'))
|
|
232
232
|
} else {
|
|
233
233
|
obj[key] = headers[i + 1].toString('utf8')
|
|
234
234
|
}
|
|
@@ -431,16 +431,7 @@ function throwIfAborted (signal) {
|
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
433
|
|
|
434
|
-
let events
|
|
435
434
|
function addAbortListener (signal, listener) {
|
|
436
|
-
if (typeof Symbol.dispose === 'symbol') {
|
|
437
|
-
if (!events) {
|
|
438
|
-
events = require('events')
|
|
439
|
-
}
|
|
440
|
-
if (typeof events.addAbortListener === 'function' && 'aborted' in signal) {
|
|
441
|
-
return events.addAbortListener(signal, listener)
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
435
|
if ('addEventListener' in signal) {
|
|
445
436
|
signal.addEventListener('abort', listener, { once: true })
|
|
446
437
|
return () => signal.removeEventListener('abort', listener)
|
|
@@ -464,6 +455,21 @@ function toUSVString (val) {
|
|
|
464
455
|
return `${val}`
|
|
465
456
|
}
|
|
466
457
|
|
|
458
|
+
// Parsed accordingly to RFC 9110
|
|
459
|
+
// https://www.rfc-editor.org/rfc/rfc9110#field.content-range
|
|
460
|
+
function parseRangeHeader (range) {
|
|
461
|
+
if (range == null || range === '') return { start: 0, end: null, size: null }
|
|
462
|
+
|
|
463
|
+
const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
|
|
464
|
+
return m
|
|
465
|
+
? {
|
|
466
|
+
start: parseInt(m[1]),
|
|
467
|
+
end: m[2] ? parseInt(m[2]) : null,
|
|
468
|
+
size: m[3] ? parseInt(m[3]) : null
|
|
469
|
+
}
|
|
470
|
+
: null
|
|
471
|
+
}
|
|
472
|
+
|
|
467
473
|
const kEnumerableProperty = Object.create(null)
|
|
468
474
|
kEnumerableProperty.enumerable = true
|
|
469
475
|
|
|
@@ -497,7 +503,9 @@ module.exports = {
|
|
|
497
503
|
buildURL,
|
|
498
504
|
throwIfAborted,
|
|
499
505
|
addAbortListener,
|
|
506
|
+
parseRangeHeader,
|
|
500
507
|
nodeMajor,
|
|
501
508
|
nodeMinor,
|
|
502
|
-
nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13)
|
|
509
|
+
nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13),
|
|
510
|
+
safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE']
|
|
503
511
|
}
|