undici 6.7.0 → 6.8.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/README.md → README.md} +45 -27
- package/docs/docs/api/DiagnosticsChannel.md +2 -2
- package/docs/docs/api/Errors.md +1 -0
- package/docs/docs/api/ProxyAgent.md +2 -0
- package/lib/api/api-request.js +3 -2
- package/lib/api/readable.js +21 -11
- package/lib/api/util.js +59 -19
- 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-h1.js +14 -1
- package/lib/dispatcher/proxy-agent.js +10 -5
- package/lib/llhttp/llhttp-wasm.js +3 -0
- package/lib/web/fetch/body.js +47 -137
- package/lib/web/fetch/data-url.js +3 -2
- package/lib/web/fetch/file.js +5 -6
- package/lib/web/fetch/formdata-parser.js +504 -0
- package/lib/web/fetch/formdata.js +1 -1
- package/lib/web/fetch/headers.js +11 -0
- package/lib/web/fetch/request.js +2 -2
- package/lib/web/fetch/util.js +30 -1
- package/package.json +5 -7
|
@@ -17,24 +17,42 @@ npm i undici
|
|
|
17
17
|
|
|
18
18
|
## Benchmarks
|
|
19
19
|
|
|
20
|
-
The benchmark is a simple
|
|
20
|
+
The benchmark is a simple getting data [example](benchmarks/benchmark.js) using a
|
|
21
21
|
50 TCP connections with a pipelining depth of 10 running on Node 20.10.0.
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
| _Tests_ | _Samples_ | _Result_ | _Tolerance_ | _Difference with slowest_ |
|
|
24
|
+
| :-----------------: | :-------: | :--------------: | :---------: | :-----------------------: |
|
|
25
|
+
| undici - fetch | 30 | 3704.43 req/sec | ± 2.95 % | - |
|
|
26
|
+
| http - no keepalive | 20 | 4275.30 req/sec | ± 2.60 % | + 15.41 % |
|
|
27
|
+
| node-fetch | 10 | 4759.42 req/sec | ± 0.87 % | + 28.48 % |
|
|
28
|
+
| request | 40 | 4803.37 req/sec | ± 2.77 % | + 29.67 % |
|
|
29
|
+
| axios | 45 | 4951.97 req/sec | ± 2.88 % | + 33.68 % |
|
|
30
|
+
| got | 10 | 5969.67 req/sec | ± 2.64 % | + 61.15 % |
|
|
31
|
+
| superagent | 10 | 9471.48 req/sec | ± 1.50 % | + 155.68 % |
|
|
32
|
+
| http - keepalive | 25 | 10327.49 req/sec | ± 2.95 % | + 178.79 % |
|
|
33
|
+
| undici - pipeline | 10 | 15053.41 req/sec | ± 1.63 % | + 306.36 % |
|
|
34
|
+
| undici - request | 10 | 19264.24 req/sec | ± 1.74 % | + 420.03 % |
|
|
35
|
+
| undici - stream | 15 | 20317.29 req/sec | ± 2.13 % | + 448.46 % |
|
|
36
|
+
| undici - dispatch | 10 | 24883.28 req/sec | ± 1.54 % | + 571.72 % |
|
|
37
|
+
|
|
38
|
+
The benchmark is a simple sending data [example](benchmarks/post-benchmark.js) using a
|
|
39
|
+
50 TCP connections with a pipelining depth of 10 running on Node 20.10.0.
|
|
40
|
+
|
|
41
|
+
| _Tests_ | _Samples_ | _Result_ | _Tolerance_ | _Difference with slowest_ |
|
|
42
|
+
| :-----------------: | :-------: | :-------------: | :---------: | :-----------------------: |
|
|
43
|
+
| undici - fetch | 20 | 1968.42 req/sec | ± 2.63 % | - |
|
|
44
|
+
| http - no keepalive | 25 | 2330.30 req/sec | ± 2.99 % | + 18.38 % |
|
|
45
|
+
| node-fetch | 20 | 2485.36 req/sec | ± 2.70 % | + 26.26 % |
|
|
46
|
+
| got | 15 | 2787.68 req/sec | ± 2.56 % | + 41.62 % |
|
|
47
|
+
| request | 30 | 2805.10 req/sec | ± 2.59 % | + 42.50 % |
|
|
48
|
+
| axios | 10 | 3040.45 req/sec | ± 1.72 % | + 54.46 % |
|
|
49
|
+
| superagent | 20 | 3358.29 req/sec | ± 2.51 % | + 70.61 % |
|
|
50
|
+
| http - keepalive | 20 | 3477.94 req/sec | ± 2.51 % | + 76.69 % |
|
|
51
|
+
| undici - pipeline | 25 | 3812.61 req/sec | ± 2.80 % | + 93.69 % |
|
|
52
|
+
| undici - request | 10 | 6067.00 req/sec | ± 0.94 % | + 208.22 % |
|
|
53
|
+
| undici - stream | 10 | 6391.61 req/sec | ± 1.98 % | + 224.71 % |
|
|
54
|
+
| undici - dispatch | 10 | 6397.00 req/sec | ± 1.48 % | + 224.98 % |
|
|
55
|
+
|
|
38
56
|
|
|
39
57
|
## Quick Start
|
|
40
58
|
|
|
@@ -94,14 +112,14 @@ For more information about their behavior, please reference the body mixin from
|
|
|
94
112
|
|
|
95
113
|
## Common API Methods
|
|
96
114
|
|
|
97
|
-
This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [
|
|
115
|
+
This section documents our most commonly used API methods. Additional APIs are documented in their own files within the [docs](./docs/) folder and are accessible via the navigation list on the left side of the docs site.
|
|
98
116
|
|
|
99
117
|
### `undici.request([url, options]): Promise`
|
|
100
118
|
|
|
101
119
|
Arguments:
|
|
102
120
|
|
|
103
121
|
* **url** `string | URL | UrlObject`
|
|
104
|
-
* **options** [`RequestOptions`](./
|
|
122
|
+
* **options** [`RequestOptions`](./docs/api/Dispatcher.md#parameter-requestoptions)
|
|
105
123
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
106
124
|
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
|
107
125
|
* **maxRedirections** `Integer` - Default: `0`
|
|
@@ -110,14 +128,14 @@ Returns a promise with the result of the `Dispatcher.request` method.
|
|
|
110
128
|
|
|
111
129
|
Calls `options.dispatcher.request(options)`.
|
|
112
130
|
|
|
113
|
-
See [Dispatcher.request](./
|
|
131
|
+
See [Dispatcher.request](./docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./examples/README.md) for examples.
|
|
114
132
|
|
|
115
133
|
### `undici.stream([url, options, ]factory): Promise`
|
|
116
134
|
|
|
117
135
|
Arguments:
|
|
118
136
|
|
|
119
137
|
* **url** `string | URL | UrlObject`
|
|
120
|
-
* **options** [`StreamOptions`](./
|
|
138
|
+
* **options** [`StreamOptions`](./docs/api/Dispatcher.md#parameter-streamoptions)
|
|
121
139
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
122
140
|
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
|
123
141
|
* **maxRedirections** `Integer` - Default: `0`
|
|
@@ -127,14 +145,14 @@ Returns a promise with the result of the `Dispatcher.stream` method.
|
|
|
127
145
|
|
|
128
146
|
Calls `options.dispatcher.stream(options, factory)`.
|
|
129
147
|
|
|
130
|
-
See [Dispatcher.stream](
|
|
148
|
+
See [Dispatcher.stream](./docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
|
|
131
149
|
|
|
132
150
|
### `undici.pipeline([url, options, ]handler): Duplex`
|
|
133
151
|
|
|
134
152
|
Arguments:
|
|
135
153
|
|
|
136
154
|
* **url** `string | URL | UrlObject`
|
|
137
|
-
* **options** [`PipelineOptions`](
|
|
155
|
+
* **options** [`PipelineOptions`](./docs/api/Dispatcher.md#parameter-pipelineoptions)
|
|
138
156
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
139
157
|
* **method** `String` - Default: `PUT` if `options.body`, otherwise `GET`
|
|
140
158
|
* **maxRedirections** `Integer` - Default: `0`
|
|
@@ -144,7 +162,7 @@ Returns: `stream.Duplex`
|
|
|
144
162
|
|
|
145
163
|
Calls `options.dispatch.pipeline(options, handler)`.
|
|
146
164
|
|
|
147
|
-
See [Dispatcher.pipeline](
|
|
165
|
+
See [Dispatcher.pipeline](./docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
|
|
148
166
|
|
|
149
167
|
### `undici.connect([url, options]): Promise`
|
|
150
168
|
|
|
@@ -153,7 +171,7 @@ Starts two-way communications with the requested resource using [HTTP CONNECT](h
|
|
|
153
171
|
Arguments:
|
|
154
172
|
|
|
155
173
|
* **url** `string | URL | UrlObject`
|
|
156
|
-
* **options** [`ConnectOptions`](
|
|
174
|
+
* **options** [`ConnectOptions`](./docs/api/Dispatcher.md#parameter-connectoptions)
|
|
157
175
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
158
176
|
* **maxRedirections** `Integer` - Default: `0`
|
|
159
177
|
* **callback** `(err: Error | null, data: ConnectData | null) => void` (optional)
|
|
@@ -162,7 +180,7 @@ Returns a promise with the result of the `Dispatcher.connect` method.
|
|
|
162
180
|
|
|
163
181
|
Calls `options.dispatch.connect(options)`.
|
|
164
182
|
|
|
165
|
-
See [Dispatcher.connect](
|
|
183
|
+
See [Dispatcher.connect](./docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
|
|
166
184
|
|
|
167
185
|
### `undici.fetch(input[, init]): Promise`
|
|
168
186
|
|
|
@@ -301,7 +319,7 @@ Upgrade to a different protocol. See [MDN - HTTP - Protocol upgrade mechanism](h
|
|
|
301
319
|
Arguments:
|
|
302
320
|
|
|
303
321
|
* **url** `string | URL | UrlObject`
|
|
304
|
-
* **options** [`UpgradeOptions`](
|
|
322
|
+
* **options** [`UpgradeOptions`](./docs/api/Dispatcher.md#parameter-upgradeoptions)
|
|
305
323
|
* **dispatcher** `Dispatcher` - Default: [getGlobalDispatcher](#undicigetglobaldispatcher)
|
|
306
324
|
* **maxRedirections** `Integer` - Default: `0`
|
|
307
325
|
* **callback** `(error: Error | null, data: UpgradeData) => void` (optional)
|
|
@@ -310,7 +328,7 @@ Returns a promise with the result of the `Dispatcher.upgrade` method.
|
|
|
310
328
|
|
|
311
329
|
Calls `options.dispatcher.upgrade(options)`.
|
|
312
330
|
|
|
313
|
-
See [Dispatcher.upgrade](
|
|
331
|
+
See [Dispatcher.upgrade](./docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
|
|
314
332
|
|
|
315
333
|
### `undici.setGlobalDispatcher(dispatcher)`
|
|
316
334
|
|
|
@@ -19,9 +19,9 @@ diagnosticsChannel.channel('undici:request:create').subscribe(({ request }) => {
|
|
|
19
19
|
console.log('completed', request.completed)
|
|
20
20
|
console.log('method', request.method)
|
|
21
21
|
console.log('path', request.path)
|
|
22
|
-
console.log('headers') //
|
|
22
|
+
console.log('headers') // array of strings, e.g: ['foo', 'bar']
|
|
23
23
|
request.addHeader('hello', 'world')
|
|
24
|
-
console.log('headers', request.headers) // e.g. '
|
|
24
|
+
console.log('headers', request.headers) // e.g. ['foo', 'bar', 'hello', 'world']
|
|
25
25
|
})
|
|
26
26
|
```
|
|
27
27
|
|
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
|
|
|
@@ -17,6 +17,8 @@ Returns: `ProxyAgent`
|
|
|
17
17
|
Extends: [`AgentOptions`](Agent.md#parameter-agentoptions)
|
|
18
18
|
|
|
19
19
|
* **uri** `string | URL` (required) - The URI of the proxy server. This can be provided as a string, as an instance of the URL class, or as an object with a `uri` property of type string.
|
|
20
|
+
If the `uri` is provided as a string or `uri` is an object with an `uri` property of type string, then it will be parsed into a `URL` object according to the [WHATWG URL Specification](https://url.spec.whatwg.org).
|
|
21
|
+
For detailed information on the parsing process and potential validation errors, please refer to the ["Writing" section](https://url.spec.whatwg.org/#writing) of the WHATWG URL Specification.
|
|
20
22
|
* **token** `string` (optional) - It can be passed by a string of token for authentication.
|
|
21
23
|
* **auth** `string` (**deprecated**) - Use token.
|
|
22
24
|
* **clientFactory** `(origin: URL, opts: Object) => Dispatcher` (optional) - Default: `(origin, opts) => new Pool(origin, opts)`
|
package/lib/api/api-request.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const Readable = require('./readable')
|
|
3
|
+
const { Readable } = require('./readable')
|
|
4
4
|
const {
|
|
5
5
|
InvalidArgumentError,
|
|
6
6
|
RequestAbortedError
|
|
@@ -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,16 +11,18 @@ 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
|
|
|
19
|
-
|
|
20
|
+
class BodyReadable extends Readable {
|
|
20
21
|
constructor ({
|
|
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 @@ module.exports = 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 @@ module.exports = 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 @@ module.exports = 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
|
}
|
|
@@ -284,16 +291,17 @@ function chunksDecode (chunks, length) {
|
|
|
284
291
|
return ''
|
|
285
292
|
}
|
|
286
293
|
const buffer = chunks.length === 1 ? chunks[0] : Buffer.concat(chunks, length)
|
|
294
|
+
const bufferLength = buffer.length
|
|
287
295
|
|
|
296
|
+
// Skip BOM.
|
|
288
297
|
const start =
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
return buffer.utf8Slice(start, buffer.length - start)
|
|
298
|
+
bufferLength > 2 &&
|
|
299
|
+
buffer[0] === 0xef &&
|
|
300
|
+
buffer[1] === 0xbb &&
|
|
301
|
+
buffer[2] === 0xbf
|
|
302
|
+
? 3
|
|
303
|
+
: 0
|
|
304
|
+
return buffer.utf8Slice(start, bufferLength)
|
|
297
305
|
}
|
|
298
306
|
|
|
299
307
|
function consumeEnd (consume) {
|
|
@@ -347,3 +355,5 @@ function consumeFinish (consume, err) {
|
|
|
347
355
|
consume.length = 0
|
|
348
356
|
consume.body = null
|
|
349
357
|
}
|
|
358
|
+
|
|
359
|
+
module.exports = { Readable: BodyReadable, chunksDecode }
|
package/lib/api/util.js
CHANGED
|
@@ -2,45 +2,85 @@ const assert = require('node:assert')
|
|
|
2
2
|
const {
|
|
3
3
|
ResponseStatusCodeError
|
|
4
4
|
} = require('../core/errors')
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
const { chunksDecode } = require('./readable')
|
|
7
|
+
const CHUNK_LIMIT = 128 * 1024
|
|
6
8
|
|
|
7
9
|
async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) {
|
|
8
10
|
assert(body)
|
|
9
11
|
|
|
10
12
|
let chunks = []
|
|
11
|
-
let
|
|
13
|
+
let length = 0
|
|
12
14
|
|
|
13
15
|
for await (const chunk of body) {
|
|
14
16
|
chunks.push(chunk)
|
|
15
|
-
|
|
16
|
-
if (
|
|
17
|
+
length += chunk.length
|
|
18
|
+
if (length > CHUNK_LIMIT) {
|
|
17
19
|
chunks = null
|
|
18
20
|
break
|
|
19
21
|
}
|
|
20
22
|
}
|
|
21
23
|
|
|
24
|
+
const message = `Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`
|
|
25
|
+
|
|
22
26
|
if (statusCode === 204 || !contentType || !chunks) {
|
|
23
|
-
|
|
27
|
+
queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers)))
|
|
24
28
|
return
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
|
|
31
|
-
return
|
|
32
|
-
}
|
|
31
|
+
const stackTraceLimit = Error.stackTraceLimit
|
|
32
|
+
Error.stackTraceLimit = 0
|
|
33
|
+
let payload
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
try {
|
|
36
|
+
if (isContentTypeApplicationJson(contentType)) {
|
|
37
|
+
payload = JSON.parse(chunksDecode(chunks, length))
|
|
38
|
+
} else if (isContentTypeText(contentType)) {
|
|
39
|
+
payload = chunksDecode(chunks, length)
|
|
38
40
|
}
|
|
39
|
-
} catch
|
|
40
|
-
//
|
|
41
|
+
} catch {
|
|
42
|
+
// process in a callback to avoid throwing in the microtask queue
|
|
43
|
+
} finally {
|
|
44
|
+
Error.stackTraceLimit = stackTraceLimit
|
|
41
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
|
+
}
|
|
42
70
|
|
|
43
|
-
|
|
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
|
+
)
|
|
44
80
|
}
|
|
45
81
|
|
|
46
|
-
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,
|
|
@@ -74,7 +74,20 @@ function removeAllListeners (obj) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
async function lazyllhttp () {
|
|
77
|
-
const
|
|
77
|
+
const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
|
|
78
|
+
|
|
79
|
+
let mod
|
|
80
|
+
try {
|
|
81
|
+
mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js'))
|
|
82
|
+
} catch (e) {
|
|
83
|
+
/* istanbul ignore next */
|
|
84
|
+
|
|
85
|
+
// We could check if the error was caused by the simd option not
|
|
86
|
+
// being enabled, but the occurring of this other error
|
|
87
|
+
// * https://github.com/emscripten-core/emscripten/issues/11495
|
|
88
|
+
// got me to remove that check to avoid breaking Node 12.
|
|
89
|
+
mod = await WebAssembly.compile(llhttpWasmData || require('../llhttp/llhttp-wasm.js'))
|
|
90
|
+
}
|
|
78
91
|
|
|
79
92
|
return await WebAssembly.instantiate(mod, {
|
|
80
93
|
env: {
|
|
@@ -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
|
})
|