undici 6.7.1 → 6.9.0
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/docs/api/Client.md +2 -1
- package/docs/docs/api/Dispatcher.md +135 -0
- package/docs/docs/api/Errors.md +1 -0
- package/index.js +4 -0
- package/lib/api/api-request.js +2 -1
- package/lib/api/readable.js +9 -2
- package/lib/api/util.js +53 -15
- package/lib/core/errors.js +12 -1
- package/lib/core/request.js +3 -2
- package/lib/core/util.js +11 -1
- package/lib/dispatcher/client.js +13 -3
- package/lib/dispatcher/dispatcher.js +47 -1
- package/lib/dispatcher/proxy-agent.js +10 -5
- package/lib/interceptor/redirect.js +24 -0
- package/lib/interceptor/retry.js +19 -0
- package/lib/web/fetch/formdata-parser.js +32 -18
- package/lib/web/fetch/formdata.js +10 -0
- package/lib/web/fetch/headers.js +11 -0
- package/lib/web/fetch/request.js +27 -0
- package/lib/web/fetch/response.js +21 -0
- package/package.json +2 -1
- package/types/fetch.d.ts +1 -1
package/docs/docs/api/Client.md
CHANGED
|
@@ -29,7 +29,8 @@ Returns: `Client`
|
|
|
29
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.
|
|
30
30
|
* **connect** `ConnectOptions | Function | null` (optional) - Default: `null`.
|
|
31
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.
|
|
32
|
-
|
|
32
|
+
<!-- TODO: Remove once we drop its support -->
|
|
33
|
+
* **interceptors** `{ Client: DispatchInterceptor[] }` - Default: `[RedirectInterceptor]` - A list of interceptors that are applied to the dispatch method. Additional logic can be applied (such as, but not limited to: 302 status code handling, authentication, cookies, compression and caching). Note that the behavior of interceptors is Experimental and might change at any given time. **Note: this is deprecated in favor of [Dispatcher#compose](./Dispatcher.md#dispatcher). Support will be droped in next major.**
|
|
33
34
|
* **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
35
|
* **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
36
|
* **allowH2**: `boolean` - Default: `false`. Enables support for H2 if the server has assigned bigger priority to it through ALPN negotiation.
|
|
@@ -817,6 +817,141 @@ try {
|
|
|
817
817
|
}
|
|
818
818
|
```
|
|
819
819
|
|
|
820
|
+
### `Dispatcher.compose(interceptors[, interceptor])`
|
|
821
|
+
|
|
822
|
+
Compose a new dispatcher from the current dispatcher and the given interceptors.
|
|
823
|
+
|
|
824
|
+
> _Notes_:
|
|
825
|
+
> - The order of the interceptors matters. The first interceptor will be the first to be called.
|
|
826
|
+
> - It is important to note that the `interceptor` function should return a function that follows the `Dispatcher.dispatch` signature.
|
|
827
|
+
> - Any fork of the chain of `interceptors` can lead to unexpected results.
|
|
828
|
+
|
|
829
|
+
Arguments:
|
|
830
|
+
|
|
831
|
+
* **interceptors** `Interceptor[interceptor[]]`: It is an array of `Interceptor` functions passed as only argument, or several interceptors passed as separate arguments.
|
|
832
|
+
|
|
833
|
+
Returns: `Dispatcher`.
|
|
834
|
+
|
|
835
|
+
#### Parameter: `Interceptor`
|
|
836
|
+
|
|
837
|
+
A function that takes a `dispatch` method and returns a `dispatch`-like function.
|
|
838
|
+
|
|
839
|
+
#### Example 1 - Basic Compose
|
|
840
|
+
|
|
841
|
+
```js
|
|
842
|
+
const { Client, RedirectHandler } = require('undici')
|
|
843
|
+
|
|
844
|
+
const redirectInterceptor = dispatch => {
|
|
845
|
+
return (opts, handler) => {
|
|
846
|
+
const { maxRedirections } = opts
|
|
847
|
+
|
|
848
|
+
if (!maxRedirections) {
|
|
849
|
+
return dispatch(opts, handler)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const redirectHandler = new RedirectHandler(
|
|
853
|
+
dispatch,
|
|
854
|
+
maxRedirections,
|
|
855
|
+
opts,
|
|
856
|
+
handler
|
|
857
|
+
)
|
|
858
|
+
opts = { ...opts, maxRedirections: 0 } // Stop sub dispatcher from also redirecting.
|
|
859
|
+
return dispatch(opts, redirectHandler)
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const client = new Client('http://localhost:3000')
|
|
864
|
+
.compose(redirectInterceptor)
|
|
865
|
+
|
|
866
|
+
await client.request({ path: '/', method: 'GET' })
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
#### Example 2 - Chained Compose
|
|
870
|
+
|
|
871
|
+
```js
|
|
872
|
+
const { Client, RedirectHandler, RetryHandler } = require('undici')
|
|
873
|
+
|
|
874
|
+
const redirectInterceptor = dispatch => {
|
|
875
|
+
return (opts, handler) => {
|
|
876
|
+
const { maxRedirections } = opts
|
|
877
|
+
|
|
878
|
+
if (!maxRedirections) {
|
|
879
|
+
return dispatch(opts, handler)
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const redirectHandler = new RedirectHandler(
|
|
883
|
+
dispatch,
|
|
884
|
+
maxRedirections,
|
|
885
|
+
opts,
|
|
886
|
+
handler
|
|
887
|
+
)
|
|
888
|
+
opts = { ...opts, maxRedirections: 0 }
|
|
889
|
+
return dispatch(opts, redirectHandler)
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const retryInterceptor = dispatch => {
|
|
894
|
+
return function retryInterceptor (opts, handler) {
|
|
895
|
+
return dispatch(
|
|
896
|
+
opts,
|
|
897
|
+
new RetryHandler(opts, {
|
|
898
|
+
handler,
|
|
899
|
+
dispatch
|
|
900
|
+
})
|
|
901
|
+
)
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const client = new Client('http://localhost:3000')
|
|
906
|
+
.compose(redirectInterceptor)
|
|
907
|
+
.compose(retryInterceptor)
|
|
908
|
+
|
|
909
|
+
await client.request({ path: '/', method: 'GET' })
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
#### Pre-built interceptors
|
|
913
|
+
|
|
914
|
+
##### `redirect`
|
|
915
|
+
|
|
916
|
+
The `redirect` interceptor allows you to customize the way your dispatcher handles redirects.
|
|
917
|
+
|
|
918
|
+
It accepts the same arguments as the [`RedirectHandler` constructor](./RedirectHandler.md).
|
|
919
|
+
|
|
920
|
+
**Example - Basic Redirect Interceptor**
|
|
921
|
+
|
|
922
|
+
```js
|
|
923
|
+
const { Client, interceptors } = require("undici");
|
|
924
|
+
const { redirect } = interceptors;
|
|
925
|
+
|
|
926
|
+
const client = new Client("http://example.com").compose(
|
|
927
|
+
redirect({ maxRedirections: 3, throwOnMaxRedirects: true })
|
|
928
|
+
);
|
|
929
|
+
client.request({ path: "/" })
|
|
930
|
+
```
|
|
931
|
+
|
|
932
|
+
##### `retry`
|
|
933
|
+
|
|
934
|
+
The `retry` interceptor allows you to customize the way your dispatcher handles retries.
|
|
935
|
+
|
|
936
|
+
It accepts the same arguments as the [`RetryHandler` constructor](./RetryHandler.md).
|
|
937
|
+
|
|
938
|
+
**Example - Basic Redirect Interceptor**
|
|
939
|
+
|
|
940
|
+
```js
|
|
941
|
+
const { Client, interceptors } = require("undici");
|
|
942
|
+
const { retry } = interceptors;
|
|
943
|
+
|
|
944
|
+
const client = new Client("http://example.com").compose(
|
|
945
|
+
retry({
|
|
946
|
+
maxRetries: 3,
|
|
947
|
+
minTimeout: 1000,
|
|
948
|
+
maxTimeout: 10000,
|
|
949
|
+
timeoutFactor: 2,
|
|
950
|
+
retryAfter: true,
|
|
951
|
+
})
|
|
952
|
+
);
|
|
953
|
+
```
|
|
954
|
+
|
|
820
955
|
## Instance Events
|
|
821
956
|
|
|
822
957
|
### Event: `'connect'`
|
package/docs/docs/api/Errors.md
CHANGED
|
@@ -26,6 +26,7 @@ import { errors } from 'undici'
|
|
|
26
26
|
| `ResponseContentLengthMismatchError` | `UND_ERR_RES_CONTENT_LENGTH_MISMATCH` | response body does not match content-length header |
|
|
27
27
|
| `InformationalError` | `UND_ERR_INFO` | expected error with reason |
|
|
28
28
|
| `ResponseExceededMaxSizeError` | `UND_ERR_RES_EXCEEDED_MAX_SIZE` | response body exceed the max size allowed |
|
|
29
|
+
| `SecureProxyConnectionError` | `UND_ERR_PRX_TLS` | tls connection to a proxy failed |
|
|
29
30
|
|
|
30
31
|
### `SocketError`
|
|
31
32
|
|
package/index.js
CHANGED
|
@@ -36,6 +36,10 @@ module.exports.RetryHandler = RetryHandler
|
|
|
36
36
|
module.exports.DecoratorHandler = DecoratorHandler
|
|
37
37
|
module.exports.RedirectHandler = RedirectHandler
|
|
38
38
|
module.exports.createRedirectInterceptor = createRedirectInterceptor
|
|
39
|
+
module.exports.interceptors = {
|
|
40
|
+
redirect: require('./lib/interceptor/redirect'),
|
|
41
|
+
retry: require('./lib/interceptor/retry')
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
module.exports.buildConnector = buildConnector
|
|
41
45
|
module.exports.errors = errors
|
package/lib/api/api-request.js
CHANGED
|
@@ -91,7 +91,8 @@ class RequestHandler extends AsyncResource {
|
|
|
91
91
|
|
|
92
92
|
const parsedHeaders = responseHeaders === 'raw' ? util.parseHeaders(rawHeaders) : headers
|
|
93
93
|
const contentType = parsedHeaders['content-type']
|
|
94
|
-
const
|
|
94
|
+
const contentLength = parsedHeaders['content-length']
|
|
95
|
+
const body = new Readable({ resume, abort, contentType, contentLength, highWaterMark })
|
|
95
96
|
|
|
96
97
|
this.callback = null
|
|
97
98
|
this.res = body
|
package/lib/api/readable.js
CHANGED
|
@@ -11,8 +11,9 @@ const { ReadableStreamFrom } = require('../core/util')
|
|
|
11
11
|
const kConsume = Symbol('kConsume')
|
|
12
12
|
const kReading = Symbol('kReading')
|
|
13
13
|
const kBody = Symbol('kBody')
|
|
14
|
-
const kAbort = Symbol('
|
|
14
|
+
const kAbort = Symbol('kAbort')
|
|
15
15
|
const kContentType = Symbol('kContentType')
|
|
16
|
+
const kContentLength = Symbol('kContentLength')
|
|
16
17
|
|
|
17
18
|
const noop = () => {}
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ class BodyReadable extends Readable {
|
|
|
21
22
|
resume,
|
|
22
23
|
abort,
|
|
23
24
|
contentType = '',
|
|
25
|
+
contentLength,
|
|
24
26
|
highWaterMark = 64 * 1024 // Same as nodejs fs streams.
|
|
25
27
|
}) {
|
|
26
28
|
super({
|
|
@@ -35,6 +37,7 @@ class BodyReadable extends Readable {
|
|
|
35
37
|
this[kConsume] = null
|
|
36
38
|
this[kBody] = null
|
|
37
39
|
this[kContentType] = contentType
|
|
40
|
+
this[kContentLength] = contentLength
|
|
38
41
|
|
|
39
42
|
// Is stream being consumed through Readable API?
|
|
40
43
|
// This is an optimization so that we avoid checking
|
|
@@ -146,7 +149,7 @@ class BodyReadable extends Readable {
|
|
|
146
149
|
}
|
|
147
150
|
|
|
148
151
|
async dump (opts) {
|
|
149
|
-
let limit = Number.isFinite(opts?.limit) ? opts.limit :
|
|
152
|
+
let limit = Number.isFinite(opts?.limit) ? opts.limit : 128 * 1024
|
|
150
153
|
const signal = opts?.signal
|
|
151
154
|
|
|
152
155
|
if (signal != null && (typeof signal !== 'object' || !('aborted' in signal))) {
|
|
@@ -160,6 +163,10 @@ class BodyReadable extends Readable {
|
|
|
160
163
|
}
|
|
161
164
|
|
|
162
165
|
return await new Promise((resolve, reject) => {
|
|
166
|
+
if (this[kContentLength] > limit) {
|
|
167
|
+
this.destroy(new AbortError())
|
|
168
|
+
}
|
|
169
|
+
|
|
163
170
|
const onAbort = () => {
|
|
164
171
|
this.destroy(signal.reason ?? new AbortError())
|
|
165
172
|
}
|
package/lib/api/util.js
CHANGED
|
@@ -21,28 +21,66 @@ async function getResolveErrorBodyCallback ({ callback, body, contentType, statu
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`
|
|
25
|
+
|
|
24
26
|
if (statusCode === 204 || !contentType || !chunks) {
|
|
25
|
-
|
|
27
|
+
queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers)))
|
|
26
28
|
return
|
|
27
29
|
}
|
|
28
30
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
|
|
33
|
-
return
|
|
34
|
-
}
|
|
31
|
+
const stackTraceLimit = Error.stackTraceLimit
|
|
32
|
+
Error.stackTraceLimit = 0
|
|
33
|
+
let payload
|
|
35
34
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
35
|
+
try {
|
|
36
|
+
if (isContentTypeApplicationJson(contentType)) {
|
|
37
|
+
payload = JSON.parse(chunksDecode(chunks, length))
|
|
38
|
+
} else if (isContentTypeText(contentType)) {
|
|
39
|
+
payload = chunksDecode(chunks, length)
|
|
40
40
|
}
|
|
41
|
-
} catch
|
|
42
|
-
//
|
|
41
|
+
} catch {
|
|
42
|
+
// process in a callback to avoid throwing in the microtask queue
|
|
43
|
+
} finally {
|
|
44
|
+
Error.stackTraceLimit = stackTraceLimit
|
|
43
45
|
}
|
|
46
|
+
queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers, payload)))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const isContentTypeApplicationJson = (contentType) => {
|
|
50
|
+
return (
|
|
51
|
+
contentType.length > 15 &&
|
|
52
|
+
contentType[11] === '/' &&
|
|
53
|
+
contentType[0] === 'a' &&
|
|
54
|
+
contentType[1] === 'p' &&
|
|
55
|
+
contentType[2] === 'p' &&
|
|
56
|
+
contentType[3] === 'l' &&
|
|
57
|
+
contentType[4] === 'i' &&
|
|
58
|
+
contentType[5] === 'c' &&
|
|
59
|
+
contentType[6] === 'a' &&
|
|
60
|
+
contentType[7] === 't' &&
|
|
61
|
+
contentType[8] === 'i' &&
|
|
62
|
+
contentType[9] === 'o' &&
|
|
63
|
+
contentType[10] === 'n' &&
|
|
64
|
+
contentType[12] === 'j' &&
|
|
65
|
+
contentType[13] === 's' &&
|
|
66
|
+
contentType[14] === 'o' &&
|
|
67
|
+
contentType[15] === 'n'
|
|
68
|
+
)
|
|
69
|
+
}
|
|
44
70
|
|
|
45
|
-
|
|
71
|
+
const isContentTypeText = (contentType) => {
|
|
72
|
+
return (
|
|
73
|
+
contentType.length > 4 &&
|
|
74
|
+
contentType[4] === '/' &&
|
|
75
|
+
contentType[0] === 't' &&
|
|
76
|
+
contentType[1] === 'e' &&
|
|
77
|
+
contentType[2] === 'x' &&
|
|
78
|
+
contentType[3] === 't'
|
|
79
|
+
)
|
|
46
80
|
}
|
|
47
81
|
|
|
48
|
-
module.exports = {
|
|
82
|
+
module.exports = {
|
|
83
|
+
getResolveErrorBodyCallback,
|
|
84
|
+
isContentTypeApplicationJson,
|
|
85
|
+
isContentTypeText
|
|
86
|
+
}
|
package/lib/core/errors.js
CHANGED
|
@@ -195,6 +195,16 @@ class RequestRetryError extends UndiciError {
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
|
|
198
|
+
class SecureProxyConnectionError extends UndiciError {
|
|
199
|
+
constructor (cause, message, options) {
|
|
200
|
+
super(message, { cause, ...(options ?? {}) })
|
|
201
|
+
this.name = 'SecureProxyConnectionError'
|
|
202
|
+
this.message = message || 'Secure Proxy Connection failed'
|
|
203
|
+
this.code = 'UND_ERR_PRX_TLS'
|
|
204
|
+
this.cause = cause
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
198
208
|
module.exports = {
|
|
199
209
|
AbortError,
|
|
200
210
|
HTTPParserError,
|
|
@@ -216,5 +226,6 @@ module.exports = {
|
|
|
216
226
|
ResponseContentLengthMismatchError,
|
|
217
227
|
BalancedPoolMissingUpstreamError,
|
|
218
228
|
ResponseExceededMaxSizeError,
|
|
219
|
-
RequestRetryError
|
|
229
|
+
RequestRetryError,
|
|
230
|
+
SecureProxyConnectionError
|
|
220
231
|
}
|
package/lib/core/request.js
CHANGED
|
@@ -40,7 +40,8 @@ class Request {
|
|
|
40
40
|
bodyTimeout,
|
|
41
41
|
reset,
|
|
42
42
|
throwOnError,
|
|
43
|
-
expectContinue
|
|
43
|
+
expectContinue,
|
|
44
|
+
servername
|
|
44
45
|
}, handler) {
|
|
45
46
|
if (typeof path !== 'string') {
|
|
46
47
|
throw new InvalidArgumentError('path must be a string')
|
|
@@ -181,7 +182,7 @@ class Request {
|
|
|
181
182
|
|
|
182
183
|
validateHandler(handler, method, upgrade)
|
|
183
184
|
|
|
184
|
-
this.servername = getServerName(this.host)
|
|
185
|
+
this.servername = servername || getServerName(this.host)
|
|
185
186
|
|
|
186
187
|
this[kHandler] = handler
|
|
187
188
|
|
package/lib/core/util.js
CHANGED
|
@@ -440,7 +440,8 @@ function addAbortListener (signal, listener) {
|
|
|
440
440
|
return () => signal.removeListener('abort', listener)
|
|
441
441
|
}
|
|
442
442
|
|
|
443
|
-
const hasToWellFormed =
|
|
443
|
+
const hasToWellFormed = typeof String.prototype.toWellFormed === 'function'
|
|
444
|
+
const hasIsWellFormed = typeof String.prototype.isWellFormed === 'function'
|
|
444
445
|
|
|
445
446
|
/**
|
|
446
447
|
* @param {string} val
|
|
@@ -449,6 +450,14 @@ function toUSVString (val) {
|
|
|
449
450
|
return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val)
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/**
|
|
454
|
+
* @param {string} val
|
|
455
|
+
*/
|
|
456
|
+
// TODO: move this to webidl
|
|
457
|
+
function isUSVString (val) {
|
|
458
|
+
return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}`
|
|
459
|
+
}
|
|
460
|
+
|
|
452
461
|
/**
|
|
453
462
|
* @see https://tools.ietf.org/html/rfc7230#section-3.2.6
|
|
454
463
|
* @param {number} c
|
|
@@ -538,6 +547,7 @@ module.exports = {
|
|
|
538
547
|
isErrored,
|
|
539
548
|
isReadable,
|
|
540
549
|
toUSVString,
|
|
550
|
+
isUSVString,
|
|
541
551
|
isReadableAborted,
|
|
542
552
|
isBlobLike,
|
|
543
553
|
parseOrigin,
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -59,6 +59,7 @@ const {
|
|
|
59
59
|
} = require('../core/symbols.js')
|
|
60
60
|
const connectH1 = require('./client-h1.js')
|
|
61
61
|
const connectH2 = require('./client-h2.js')
|
|
62
|
+
let deprecatedInterceptorWarned = false
|
|
62
63
|
|
|
63
64
|
const kClosedResolve = Symbol('kClosedResolve')
|
|
64
65
|
|
|
@@ -207,9 +208,18 @@ class Client extends DispatcherBase {
|
|
|
207
208
|
})
|
|
208
209
|
}
|
|
209
210
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
211
|
+
if (interceptors?.Client && Array.isArray(interceptors.Client)) {
|
|
212
|
+
this[kInterceptors] = interceptors.Client
|
|
213
|
+
if (!deprecatedInterceptorWarned) {
|
|
214
|
+
deprecatedInterceptorWarned = true
|
|
215
|
+
process.emitWarning('Client.Options#interceptor is deprecated. Use Dispatcher#compose instead.', {
|
|
216
|
+
code: 'UNDICI-CLIENT-INTERCEPTOR-DEPRECATED'
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
} else {
|
|
220
|
+
this[kInterceptors] = [createRedirectInterceptor({ maxRedirections })]
|
|
221
|
+
}
|
|
222
|
+
|
|
213
223
|
this[kUrl] = util.parseOrigin(url)
|
|
214
224
|
this[kConnector] = connect
|
|
215
225
|
this[kPipelining] = pipelining != null ? pipelining : 1
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
'use strict'
|
|
2
|
-
|
|
3
2
|
const EventEmitter = require('node:events')
|
|
4
3
|
|
|
5
4
|
class Dispatcher extends EventEmitter {
|
|
@@ -14,6 +13,53 @@ class Dispatcher extends EventEmitter {
|
|
|
14
13
|
destroy () {
|
|
15
14
|
throw new Error('not implemented')
|
|
16
15
|
}
|
|
16
|
+
|
|
17
|
+
compose (...args) {
|
|
18
|
+
// So we handle [interceptor1, interceptor2] or interceptor1, interceptor2, ...
|
|
19
|
+
const interceptors = Array.isArray(args[0]) ? args[0] : args
|
|
20
|
+
let dispatch = this.dispatch.bind(this)
|
|
21
|
+
|
|
22
|
+
for (const interceptor of interceptors) {
|
|
23
|
+
if (interceptor == null) {
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof interceptor !== 'function') {
|
|
28
|
+
throw new TypeError(`invalid interceptor, expected function received ${typeof interceptor}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
dispatch = interceptor(dispatch)
|
|
32
|
+
|
|
33
|
+
if (dispatch == null || typeof dispatch !== 'function' || dispatch.length !== 2) {
|
|
34
|
+
throw new TypeError('invalid interceptor')
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return new ComposedDispatcher(this, dispatch)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class ComposedDispatcher extends Dispatcher {
|
|
43
|
+
#dispatcher = null
|
|
44
|
+
#dispatch = null
|
|
45
|
+
|
|
46
|
+
constructor (dispatcher, dispatch) {
|
|
47
|
+
super()
|
|
48
|
+
this.#dispatcher = dispatcher
|
|
49
|
+
this.#dispatch = dispatch
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dispatch (...args) {
|
|
53
|
+
this.#dispatch(...args)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
close (...args) {
|
|
57
|
+
return this.#dispatcher.close(...args)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
destroy (...args) {
|
|
61
|
+
return this.#dispatcher.destroy(...args)
|
|
62
|
+
}
|
|
17
63
|
}
|
|
18
64
|
|
|
19
65
|
module.exports = Dispatcher
|
|
@@ -5,7 +5,7 @@ const { URL } = require('node:url')
|
|
|
5
5
|
const Agent = require('./agent')
|
|
6
6
|
const Pool = require('./pool')
|
|
7
7
|
const DispatcherBase = require('./dispatcher-base')
|
|
8
|
-
const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
|
|
8
|
+
const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = require('../core/errors')
|
|
9
9
|
const buildConnector = require('../core/connect')
|
|
10
10
|
|
|
11
11
|
const kAgent = Symbol('proxy agent')
|
|
@@ -37,10 +37,9 @@ class ProxyAgent extends DispatcherBase {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const url = this.#getUrl(opts)
|
|
40
|
-
const { href, origin, port, protocol, username, password } = url
|
|
40
|
+
const { href, origin, port, protocol, username, password, hostname: proxyHostname } = url
|
|
41
41
|
|
|
42
42
|
this[kProxy] = { uri: href, protocol }
|
|
43
|
-
this[kAgent] = new Agent(opts)
|
|
44
43
|
this[kInterceptors] = opts.interceptors?.ProxyAgent && Array.isArray(opts.interceptors.ProxyAgent)
|
|
45
44
|
? opts.interceptors.ProxyAgent
|
|
46
45
|
: []
|
|
@@ -78,7 +77,8 @@ class ProxyAgent extends DispatcherBase {
|
|
|
78
77
|
headers: {
|
|
79
78
|
...this[kProxyHeaders],
|
|
80
79
|
host: requestedHost
|
|
81
|
-
}
|
|
80
|
+
},
|
|
81
|
+
servername: this[kProxyTls]?.servername || proxyHostname
|
|
82
82
|
})
|
|
83
83
|
if (statusCode !== 200) {
|
|
84
84
|
socket.on('error', () => {}).destroy()
|
|
@@ -96,7 +96,12 @@ class ProxyAgent extends DispatcherBase {
|
|
|
96
96
|
}
|
|
97
97
|
this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback)
|
|
98
98
|
} catch (err) {
|
|
99
|
-
|
|
99
|
+
if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') {
|
|
100
|
+
// Throw a custom error to avoid loop in client.js#connect
|
|
101
|
+
callback(new SecureProxyConnectionError(err))
|
|
102
|
+
} else {
|
|
103
|
+
callback(err)
|
|
104
|
+
}
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const RedirectHandler = require('../handler/redirect-handler')
|
|
3
|
+
|
|
4
|
+
module.exports = opts => {
|
|
5
|
+
const globalMaxRedirections = opts?.maxRedirections
|
|
6
|
+
return dispatch => {
|
|
7
|
+
return function redirectInterceptor (opts, handler) {
|
|
8
|
+
const { maxRedirections = globalMaxRedirections, ...baseOpts } = opts
|
|
9
|
+
|
|
10
|
+
if (!maxRedirections) {
|
|
11
|
+
return dispatch(opts, handler)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const redirectHandler = new RedirectHandler(
|
|
15
|
+
dispatch,
|
|
16
|
+
maxRedirections,
|
|
17
|
+
opts,
|
|
18
|
+
handler
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
return dispatch(baseOpts, redirectHandler)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const RetryHandler = require('../handler/retry-handler')
|
|
3
|
+
|
|
4
|
+
module.exports = globalOpts => {
|
|
5
|
+
return dispatch => {
|
|
6
|
+
return function retryInterceptor (opts, handler) {
|
|
7
|
+
return dispatch(
|
|
8
|
+
opts,
|
|
9
|
+
new RetryHandler(
|
|
10
|
+
{ ...opts, retryOptions: { ...globalOpts, ...opts.retryOptions } },
|
|
11
|
+
{
|
|
12
|
+
handler,
|
|
13
|
+
dispatch
|
|
14
|
+
}
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { toUSVString, isUSVString, bufferToLowerCasedHeaderName } = require('../../core/util')
|
|
4
4
|
const { utf8DecodeBytes } = require('./util')
|
|
5
5
|
const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
|
|
6
6
|
const { isFileLike, File: UndiciFile } = require('./file')
|
|
7
7
|
const { makeEntry } = require('./formdata')
|
|
8
8
|
const assert = require('node:assert')
|
|
9
|
-
const {
|
|
9
|
+
const { File: NodeFile } = require('node:buffer')
|
|
10
10
|
|
|
11
11
|
const File = globalThis.File ?? NodeFile ?? UndiciFile
|
|
12
12
|
|
|
@@ -15,6 +15,18 @@ const filenameBuffer = Buffer.from('; filename')
|
|
|
15
15
|
const dd = Buffer.from('--')
|
|
16
16
|
const ddcrlf = Buffer.from('--\r\n')
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} chars
|
|
20
|
+
*/
|
|
21
|
+
function isAsciiString (chars) {
|
|
22
|
+
for (let i = 0; i < chars.length; ++i) {
|
|
23
|
+
if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return true
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
/**
|
|
19
31
|
* @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
|
|
20
32
|
* @param {string} boundary
|
|
@@ -30,7 +42,7 @@ function validateBoundary (boundary) {
|
|
|
30
42
|
// - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
|
|
31
43
|
// 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
|
|
32
44
|
// 0x2D (-) or 0x5F (_).
|
|
33
|
-
for (let i = 0; i <
|
|
45
|
+
for (let i = 0; i < length; ++i) {
|
|
34
46
|
const cp = boundary.charCodeAt(i)
|
|
35
47
|
|
|
36
48
|
if (!(
|
|
@@ -58,12 +70,12 @@ function escapeFormDataName (name, encoding = 'utf-8', isFilename = false) {
|
|
|
58
70
|
// 1. If isFilename is true:
|
|
59
71
|
if (isFilename) {
|
|
60
72
|
// 1.1. Set name to the result of converting name into a scalar value string.
|
|
61
|
-
name =
|
|
73
|
+
name = toUSVString(name)
|
|
62
74
|
} else {
|
|
63
75
|
// 2. Otherwise:
|
|
64
76
|
|
|
65
77
|
// 2.1. Assert: name is a scalar value string.
|
|
66
|
-
assert(
|
|
78
|
+
assert(isUSVString(name))
|
|
67
79
|
|
|
68
80
|
// 2.2. Replace every occurrence of U+000D (CR) not followed by U+000A (LF),
|
|
69
81
|
// and every occurrence of U+000A (LF) not preceded by U+000D (CR), in
|
|
@@ -94,14 +106,16 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
94
106
|
// 1. Assert: mimeType’s essence is "multipart/form-data".
|
|
95
107
|
assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
|
|
96
108
|
|
|
109
|
+
const boundaryString = mimeType.parameters.get('boundary')
|
|
110
|
+
|
|
97
111
|
// 2. If mimeType’s parameters["boundary"] does not exist, return failure.
|
|
98
112
|
// Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
|
|
99
113
|
// parameters["boundary"].
|
|
100
|
-
if (
|
|
114
|
+
if (boundaryString === undefined) {
|
|
101
115
|
return 'failure'
|
|
102
116
|
}
|
|
103
117
|
|
|
104
|
-
const boundary = Buffer.from(`--${
|
|
118
|
+
const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
|
|
105
119
|
|
|
106
120
|
// 3. Let entry list be an empty entry list.
|
|
107
121
|
const entryList = []
|
|
@@ -200,7 +214,10 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
200
214
|
contentType ??= 'text/plain'
|
|
201
215
|
|
|
202
216
|
// 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
|
|
203
|
-
|
|
217
|
+
|
|
218
|
+
// Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead.
|
|
219
|
+
// Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`.
|
|
220
|
+
if (!isAsciiString(contentType)) {
|
|
204
221
|
contentType = ''
|
|
205
222
|
}
|
|
206
223
|
|
|
@@ -214,8 +231,8 @@ function multipartFormDataParser (input, mimeType) {
|
|
|
214
231
|
}
|
|
215
232
|
|
|
216
233
|
// 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
|
|
217
|
-
assert(
|
|
218
|
-
assert((typeof value === 'string' &&
|
|
234
|
+
assert(isUSVString(name))
|
|
235
|
+
assert((typeof value === 'string' && isUSVString(value)) || isFileLike(value))
|
|
219
236
|
|
|
220
237
|
// 5.13. Create an entry with name and value, and append it to entry list.
|
|
221
238
|
entryList.push(makeEntry(name, value, filename))
|
|
@@ -280,7 +297,7 @@ function parseMultipartFormDataHeaders (input, position) {
|
|
|
280
297
|
)
|
|
281
298
|
|
|
282
299
|
// 2.8. Byte-lowercase header name and switch on the result:
|
|
283
|
-
switch (
|
|
300
|
+
switch (bufferToLowerCasedHeaderName(headerName)) {
|
|
284
301
|
case 'content-disposition': {
|
|
285
302
|
// 1. Set name and filename to null.
|
|
286
303
|
name = filename = null
|
|
@@ -427,16 +444,13 @@ function parseMultipartFormDataName (input, position) {
|
|
|
427
444
|
* @param {{ position: number }} position
|
|
428
445
|
*/
|
|
429
446
|
function collectASequenceOfBytes (condition, input, position) {
|
|
430
|
-
|
|
431
|
-
let index = 0
|
|
432
|
-
|
|
433
|
-
while (position.position < input.length && condition(input[position.position])) {
|
|
434
|
-
result[index++] = input[position.position]
|
|
447
|
+
let start = position.position
|
|
435
448
|
|
|
436
|
-
|
|
449
|
+
while (start < input.length && condition(input[start])) {
|
|
450
|
+
++start
|
|
437
451
|
}
|
|
438
452
|
|
|
439
|
-
return
|
|
453
|
+
return input.subarray(position.position, (position.position = start))
|
|
440
454
|
}
|
|
441
455
|
|
|
442
456
|
/**
|
|
@@ -6,6 +6,7 @@ const { kEnumerableProperty } = require('../../core/util')
|
|
|
6
6
|
const { File: UndiciFile, FileLike, isFileLike } = require('./file')
|
|
7
7
|
const { webidl } = require('./webidl')
|
|
8
8
|
const { File: NativeFile } = require('node:buffer')
|
|
9
|
+
const nodeUtil = require('node:util')
|
|
9
10
|
|
|
10
11
|
/** @type {globalThis['File']} */
|
|
11
12
|
const File = NativeFile ?? UndiciFile
|
|
@@ -154,6 +155,15 @@ class FormData {
|
|
|
154
155
|
this[kState].push(entry)
|
|
155
156
|
}
|
|
156
157
|
}
|
|
158
|
+
|
|
159
|
+
[nodeUtil.inspect.custom] (depth, options) {
|
|
160
|
+
let output = 'FormData:\n'
|
|
161
|
+
this[kState].forEach(entry => {
|
|
162
|
+
output += `${entry.name}: ${entry.value}\n`
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return output
|
|
166
|
+
}
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
iteratorMixin('FormData', FormData, kState, 'name', 'value')
|
package/lib/web/fetch/headers.js
CHANGED
|
@@ -12,6 +12,7 @@ const {
|
|
|
12
12
|
} = require('./util')
|
|
13
13
|
const { webidl } = require('./webidl')
|
|
14
14
|
const assert = require('node:assert')
|
|
15
|
+
const util = require('util')
|
|
15
16
|
|
|
16
17
|
const kHeadersMap = Symbol('headers map')
|
|
17
18
|
const kHeadersSortedMap = Symbol('headers map sorted')
|
|
@@ -576,8 +577,18 @@ class Headers {
|
|
|
576
577
|
|
|
577
578
|
return this[kHeadersList]
|
|
578
579
|
}
|
|
580
|
+
|
|
581
|
+
[util.inspect.custom] (depth, options) {
|
|
582
|
+
const inspected = util.inspect(this[kHeadersList].entries)
|
|
583
|
+
|
|
584
|
+
return `Headers ${inspected}`
|
|
585
|
+
}
|
|
579
586
|
}
|
|
580
587
|
|
|
588
|
+
Object.defineProperty(Headers.prototype, util.inspect.custom, {
|
|
589
|
+
enumerable: false
|
|
590
|
+
})
|
|
591
|
+
|
|
581
592
|
iteratorMixin('Headers', Headers, kHeadersSortedMap, 0, 1)
|
|
582
593
|
|
|
583
594
|
Object.defineProperties(Headers.prototype, {
|
package/lib/web/fetch/request.js
CHANGED
|
@@ -6,6 +6,7 @@ const { extractBody, mixinBody, cloneBody } = require('./body')
|
|
|
6
6
|
const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
|
|
7
7
|
const { FinalizationRegistry } = require('./dispatcher-weakref')()
|
|
8
8
|
const util = require('../../core/util')
|
|
9
|
+
const nodeUtil = require('node:util')
|
|
9
10
|
const {
|
|
10
11
|
isValidHTTPToken,
|
|
11
12
|
sameOrigin,
|
|
@@ -771,6 +772,32 @@ class Request {
|
|
|
771
772
|
// 4. Return clonedRequestObject.
|
|
772
773
|
return fromInnerRequest(clonedRequest, ac.signal, this[kHeaders][kGuard], this[kRealm])
|
|
773
774
|
}
|
|
775
|
+
|
|
776
|
+
[nodeUtil.inspect.custom] (depth, options) {
|
|
777
|
+
if (options.depth === null) {
|
|
778
|
+
options.depth = 2
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const properties = {
|
|
782
|
+
method: this.method,
|
|
783
|
+
url: this.url,
|
|
784
|
+
headers: this.headers,
|
|
785
|
+
destination: this.destination,
|
|
786
|
+
referrer: this.referrer,
|
|
787
|
+
referrerPolicy: this.referrerPolicy,
|
|
788
|
+
mode: this.mode,
|
|
789
|
+
credentials: this.credentials,
|
|
790
|
+
cache: this.cache,
|
|
791
|
+
redirect: this.redirect,
|
|
792
|
+
integrity: this.integrity,
|
|
793
|
+
keepalive: this.keepalive,
|
|
794
|
+
isReloadNavigation: this.isReloadNavigation,
|
|
795
|
+
isHistoryNavigation: this.isHistoryNavigation,
|
|
796
|
+
signal: this.signal
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
return nodeUtil.formatWithOptions(options, { ...properties })
|
|
800
|
+
}
|
|
774
801
|
}
|
|
775
802
|
|
|
776
803
|
mixinBody(Request)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const { Headers, HeadersList, fill } = require('./headers')
|
|
4
4
|
const { extractBody, cloneBody, mixinBody } = require('./body')
|
|
5
5
|
const util = require('../../core/util')
|
|
6
|
+
const nodeUtil = require('node:util')
|
|
6
7
|
const { kEnumerableProperty } = util
|
|
7
8
|
const {
|
|
8
9
|
isValidReasonPhrase,
|
|
@@ -252,6 +253,26 @@ class Response {
|
|
|
252
253
|
// clonedResponse, this’s headers’s guard, and this’s relevant Realm.
|
|
253
254
|
return fromInnerResponse(clonedResponse, this[kHeaders][kGuard], this[kRealm])
|
|
254
255
|
}
|
|
256
|
+
|
|
257
|
+
[nodeUtil.inspect.custom] (depth, options) {
|
|
258
|
+
if (options.depth === null) {
|
|
259
|
+
options.depth = 2
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const properties = {
|
|
263
|
+
status: this.status,
|
|
264
|
+
statusText: this.statusText,
|
|
265
|
+
headers: this.headers,
|
|
266
|
+
body: this.body,
|
|
267
|
+
bodyUsed: this.bodyUsed,
|
|
268
|
+
ok: this.ok,
|
|
269
|
+
redirected: this.redirected,
|
|
270
|
+
type: this.type,
|
|
271
|
+
url: this.url
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return nodeUtil.formatWithOptions(options, `Response ${nodeUtil.inspect(properties)}`)
|
|
275
|
+
}
|
|
255
276
|
}
|
|
256
277
|
|
|
257
278
|
mixinBody(Response)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "undici",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.0",
|
|
4
4
|
"description": "An HTTP/1.1 client, written from scratch for Node.js",
|
|
5
5
|
"homepage": "https://undici.nodejs.org",
|
|
6
6
|
"bugs": {
|
|
@@ -107,6 +107,7 @@
|
|
|
107
107
|
"jest": "^29.0.2",
|
|
108
108
|
"jsdom": "^24.0.0",
|
|
109
109
|
"jsfuzz": "^1.0.15",
|
|
110
|
+
"node-forge": "^1.3.1",
|
|
110
111
|
"pre-commit": "^1.2.2",
|
|
111
112
|
"proxy": "^2.1.1",
|
|
112
113
|
"snazzy": "^9.0.0",
|
package/types/fetch.d.ts
CHANGED