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.
@@ -17,24 +17,42 @@ npm i undici
17
17
 
18
18
  ## Benchmarks
19
19
 
20
- The benchmark is a simple `hello world` [example](benchmarks/benchmark.js) using a
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
- Tests │ Samples │ Result Tolerance Difference with slowest
25
- |─────────────────────|─────────|─────────────────|───────────|─────────────────────────|
26
- got │ 45 1661.71 req/sec ± 2.93 % │ -
27
- node-fetch 20 │ 2164.81 req/sec ± 2.63 %+ 30.28 %
28
- undici - fetch │ 35 │ 2274.27 req/sec ± 2.70 %+ 36.86 %
29
- http - no keepalive │ 15 │ 2376.04 req/sec ± 2.99 %+ 42.99 %
30
- axios │ 25 │ 2612.93 req/sec ± 2.89 %+ 57.24 %
31
- │ request │ 40 │ 2712.19 req/sec ± 2.92 %+ 63.22 %
32
- http - keepalive │ 45 │ 4393.25 req/sec ± 2.86 %+ 164.38 %
33
- undici - pipeline │ 45 │ 5484.69 req/sec ± 2.87 %+ 230.06 %
34
- undici - request │ 55 │ 7773.98 req/sec ± 2.93 %+ 367.83 %
35
- undici - stream │ 70 │ 8425.96 req/sec ± 2.91 %+ 407.07 %
36
- undici - dispatch │ 50 │ 9488.99 req/sec ± 2.85 %+ 471.04 %
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 [documentation](./documentation/) folder and are accessible via the navigation list on the left side of the docs site.
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`](./documentation/docs/api/Dispatcher.md#parameter-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](./documentation/docs/api/Dispatcher.md#dispatcherrequestoptions-callback) for more details, and [request examples](./documentation/examples/README.md) for examples.
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`](./documentation/docs/api/Dispatcher.md#parameter-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](documentation/docs/api/Dispatcher.md#dispatcherstreamoptions-factory-callback) for more details.
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`](documentation/docs/api/Dispatcher.md#parameter-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](documentation/docs/api/Dispatcher.md#dispatcherpipelineoptions-handler) for more details.
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`](documentation/docs/api/Dispatcher.md#parameter-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](documentation/docs/api/Dispatcher.md#dispatcherconnectoptions-callback) for more details.
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`](documentation/docs/api/Dispatcher.md#parameter-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](documentation/docs/api/Dispatcher.md#dispatcherupgradeoptions-callback) for more details.
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') // raw text, e.g: 'bar: bar\r\n'
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. 'bar: bar\r\nhello: world\r\n'
24
+ console.log('headers', request.headers) // e.g. ['foo', 'bar', 'hello', 'world']
25
25
  })
26
26
  ```
27
27
 
@@ -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)`
@@ -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 body = new Readable({ resume, abort, contentType, highWaterMark })
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
@@ -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('abort')
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
- module.exports = class BodyReadable extends Readable {
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 : 262144
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
- buffer.length >= 3 &&
290
- // Skip BOM.
291
- buffer[0] === 0xef &&
292
- buffer[1] === 0xbb &&
293
- buffer[2] === 0xbf
294
- ? 3
295
- : 0
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
- const { toUSVString } = require('../core/util')
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 limit = 0
13
+ let length = 0
12
14
 
13
15
  for await (const chunk of body) {
14
16
  chunks.push(chunk)
15
- limit += chunk.length
16
- if (limit > 128 * 1024) {
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
- process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
27
+ queueMicrotask(() => callback(new ResponseStatusCodeError(message, statusCode, headers)))
24
28
  return
25
29
  }
26
30
 
27
- try {
28
- if (contentType.startsWith('application/json')) {
29
- const payload = JSON.parse(toUSVString(Buffer.concat(chunks)))
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
- if (contentType.startsWith('text/')) {
35
- const payload = toUSVString(Buffer.concat(chunks))
36
- process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload))
37
- return
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 (err) {
40
- // Process in a fallback if error
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
- process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers))
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 = { getResolveErrorBodyCallback }
82
+ module.exports = {
83
+ getResolveErrorBodyCallback,
84
+ isContentTypeApplicationJson,
85
+ isContentTypeText
86
+ }
@@ -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
  }
@@ -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 = !!String.prototype.toWellFormed
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 mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js'))
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
- callback(err)
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
  })