undici 7.16.0 → 7.18.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.
Files changed (69) hide show
  1. package/README.md +53 -1
  2. package/docs/docs/api/Client.md +1 -0
  3. package/docs/docs/api/DiagnosticsChannel.md +57 -0
  4. package/docs/docs/api/Dispatcher.md +86 -0
  5. package/docs/docs/api/RoundRobinPool.md +145 -0
  6. package/docs/docs/api/WebSocket.md +21 -0
  7. package/docs/docs/best-practices/crawling.md +58 -0
  8. package/index.js +4 -1
  9. package/lib/api/api-upgrade.js +2 -1
  10. package/lib/core/connect.js +4 -1
  11. package/lib/core/diagnostics.js +28 -1
  12. package/lib/core/symbols.js +3 -0
  13. package/lib/core/util.js +29 -31
  14. package/lib/dispatcher/balanced-pool.js +10 -0
  15. package/lib/dispatcher/client-h1.js +0 -16
  16. package/lib/dispatcher/client-h2.js +153 -23
  17. package/lib/dispatcher/client.js +7 -2
  18. package/lib/dispatcher/dispatcher-base.js +11 -12
  19. package/lib/dispatcher/h2c-client.js +7 -78
  20. package/lib/dispatcher/pool-base.js +1 -1
  21. package/lib/dispatcher/proxy-agent.js +13 -2
  22. package/lib/dispatcher/round-robin-pool.js +137 -0
  23. package/lib/encoding/index.js +33 -0
  24. package/lib/handler/cache-handler.js +84 -27
  25. package/lib/handler/deduplication-handler.js +216 -0
  26. package/lib/handler/retry-handler.js +0 -2
  27. package/lib/interceptor/cache.js +35 -17
  28. package/lib/interceptor/decompress.js +2 -1
  29. package/lib/interceptor/deduplicate.js +109 -0
  30. package/lib/interceptor/dns.js +55 -13
  31. package/lib/mock/mock-utils.js +1 -2
  32. package/lib/mock/snapshot-agent.js +11 -5
  33. package/lib/mock/snapshot-recorder.js +12 -4
  34. package/lib/mock/snapshot-utils.js +4 -4
  35. package/lib/util/cache.js +29 -1
  36. package/lib/util/runtime-features.js +124 -0
  37. package/lib/web/cookies/parse.js +1 -1
  38. package/lib/web/fetch/body.js +29 -39
  39. package/lib/web/fetch/data-url.js +12 -160
  40. package/lib/web/fetch/formdata-parser.js +204 -127
  41. package/lib/web/fetch/index.js +18 -6
  42. package/lib/web/fetch/request.js +6 -0
  43. package/lib/web/fetch/response.js +2 -3
  44. package/lib/web/fetch/util.js +2 -65
  45. package/lib/web/infra/index.js +229 -0
  46. package/lib/web/subresource-integrity/subresource-integrity.js +6 -5
  47. package/lib/web/webidl/index.js +4 -2
  48. package/lib/web/websocket/connection.js +31 -21
  49. package/lib/web/websocket/frame.js +9 -15
  50. package/lib/web/websocket/stream/websocketstream.js +1 -1
  51. package/lib/web/websocket/util.js +2 -1
  52. package/package.json +5 -4
  53. package/types/agent.d.ts +1 -1
  54. package/types/api.d.ts +2 -2
  55. package/types/balanced-pool.d.ts +2 -1
  56. package/types/cache-interceptor.d.ts +1 -0
  57. package/types/client.d.ts +1 -1
  58. package/types/connector.d.ts +2 -2
  59. package/types/diagnostics-channel.d.ts +2 -2
  60. package/types/dispatcher.d.ts +12 -12
  61. package/types/fetch.d.ts +4 -4
  62. package/types/formdata.d.ts +1 -1
  63. package/types/h2c-client.d.ts +1 -1
  64. package/types/index.d.ts +9 -1
  65. package/types/interceptors.d.ts +36 -2
  66. package/types/pool.d.ts +1 -1
  67. package/types/readable.d.ts +2 -2
  68. package/types/round-robin-pool.d.ts +41 -0
  69. package/types/websocket.d.ts +9 -9
@@ -1,16 +1,15 @@
1
1
  'use strict'
2
2
 
3
3
  const { bufferToLowerCasedHeaderName } = require('../../core/util')
4
- const { utf8DecodeBytes } = require('./util')
5
- const { HTTP_TOKEN_CODEPOINTS, isomorphicDecode } = require('./data-url')
4
+ const { HTTP_TOKEN_CODEPOINTS } = require('./data-url')
6
5
  const { makeEntry } = require('./formdata')
7
6
  const { webidl } = require('../webidl')
8
7
  const assert = require('node:assert')
8
+ const { isomorphicDecode } = require('../infra')
9
+ const { utf8DecodeBytes } = require('../../encoding')
9
10
 
10
- const formDataNameBuffer = Buffer.from('form-data; name="')
11
- const filenameBuffer = Buffer.from('filename')
12
11
  const dd = Buffer.from('--')
13
- const ddcrlf = Buffer.from('--\r\n')
12
+ const decoder = new TextDecoder()
14
13
 
15
14
  /**
16
15
  * @param {string} chars
@@ -84,20 +83,16 @@ function multipartFormDataParser (input, mimeType) {
84
83
  // the first byte.
85
84
  const position = { position: 0 }
86
85
 
87
- // Note: undici addition, allows leading and trailing CRLFs.
88
- while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
89
- position.position += 2
90
- }
91
-
92
- let trailing = input.length
86
+ // Note: Per RFC 2046 Section 5.1.1, we must ignore anything before the
87
+ // first boundary delimiter line (preamble). Search for the first boundary.
88
+ const firstBoundaryIndex = input.indexOf(boundary)
93
89
 
94
- while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) {
95
- trailing -= 2
90
+ if (firstBoundaryIndex === -1) {
91
+ throw parsingError('no boundary found in multipart body')
96
92
  }
97
93
 
98
- if (trailing !== input.length) {
99
- input = input.subarray(0, trailing)
100
- }
94
+ // Start parsing from the first boundary, ignoring any preamble
95
+ position.position = firstBoundaryIndex
101
96
 
102
97
  // 5. While true:
103
98
  while (true) {
@@ -113,11 +108,11 @@ function multipartFormDataParser (input, mimeType) {
113
108
 
114
109
  // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
115
110
  // (`--` followed by CR LF) followed by the end of input, return entry list.
116
- // Note: a body does NOT need to end with CRLF. It can end with --.
117
- if (
118
- (position.position === input.length - 2 && bufferStartsWith(input, dd, position)) ||
119
- (position.position === input.length - 4 && bufferStartsWith(input, ddcrlf, position))
120
- ) {
111
+ // Note: Per RFC 2046 Section 5.1.1, we must ignore anything after the
112
+ // final boundary delimiter (epilogue). Check for -- or --CRLF and return
113
+ // regardless of what follows.
114
+ if (bufferStartsWith(input, dd, position)) {
115
+ // Found closing boundary delimiter (--), ignore any epilogue
121
116
  return entryList
122
117
  }
123
118
 
@@ -205,6 +200,113 @@ function multipartFormDataParser (input, mimeType) {
205
200
  }
206
201
  }
207
202
 
203
+ /**
204
+ * Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
205
+ * @param {Buffer} input
206
+ * @param {{ position: number }} position
207
+ * @returns {{ name: string, value: string }}
208
+ */
209
+ function parseContentDispositionAttribute (input, position) {
210
+ // Skip leading semicolon and whitespace
211
+ if (input[position.position] === 0x3b /* ; */) {
212
+ position.position++
213
+ }
214
+
215
+ // Skip whitespace
216
+ collectASequenceOfBytes(
217
+ (char) => char === 0x20 || char === 0x09,
218
+ input,
219
+ position
220
+ )
221
+
222
+ // Collect attribute name (token characters)
223
+ const attributeName = collectASequenceOfBytes(
224
+ (char) => isToken(char) && char !== 0x3d && char !== 0x2a, // not = or *
225
+ input,
226
+ position
227
+ )
228
+
229
+ if (attributeName.length === 0) {
230
+ return null
231
+ }
232
+
233
+ const attrNameStr = attributeName.toString('ascii').toLowerCase()
234
+
235
+ // Check for extended notation (attribute*)
236
+ const isExtended = input[position.position] === 0x2a /* * */
237
+ if (isExtended) {
238
+ position.position++ // skip *
239
+ }
240
+
241
+ // Expect = sign
242
+ if (input[position.position] !== 0x3d /* = */) {
243
+ return null
244
+ }
245
+ position.position++ // skip =
246
+
247
+ // Skip whitespace
248
+ collectASequenceOfBytes(
249
+ (char) => char === 0x20 || char === 0x09,
250
+ input,
251
+ position
252
+ )
253
+
254
+ let value
255
+
256
+ if (isExtended) {
257
+ // Extended attribute format: charset'language'encoded-value
258
+ const headerValue = collectASequenceOfBytes(
259
+ (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a && char !== 0x3b, // not space, CRLF, or ;
260
+ input,
261
+ position
262
+ )
263
+
264
+ // Check for utf-8'' prefix (case insensitive)
265
+ if (
266
+ (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
267
+ (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
268
+ (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
269
+ headerValue[3] !== 0x2d || // -
270
+ headerValue[4] !== 0x38 // 8
271
+ ) {
272
+ throw parsingError('unknown encoding, expected utf-8\'\'')
273
+ }
274
+
275
+ // Skip utf-8'' and decode the rest
276
+ value = decodeURIComponent(decoder.decode(headerValue.subarray(7)))
277
+ } else if (input[position.position] === 0x22 /* " */) {
278
+ // Quoted string
279
+ position.position++ // skip opening quote
280
+
281
+ const quotedValue = collectASequenceOfBytes(
282
+ (char) => char !== 0x0a && char !== 0x0d && char !== 0x22, // not LF, CR, or "
283
+ input,
284
+ position
285
+ )
286
+
287
+ if (input[position.position] !== 0x22) {
288
+ throw parsingError('Closing quote not found')
289
+ }
290
+ position.position++ // skip closing quote
291
+
292
+ value = decoder.decode(quotedValue)
293
+ .replace(/%0A/ig, '\n')
294
+ .replace(/%0D/ig, '\r')
295
+ .replace(/%22/g, '"')
296
+ } else {
297
+ // Token value (no quotes)
298
+ const tokenValue = collectASequenceOfBytes(
299
+ (char) => isToken(char) && char !== 0x3b, // not ;
300
+ input,
301
+ position
302
+ )
303
+
304
+ value = decoder.decode(tokenValue)
305
+ }
306
+
307
+ return { name: attrNameStr, value }
308
+ }
309
+
208
310
  /**
209
311
  * @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
210
312
  * @param {Buffer} input
@@ -265,80 +367,40 @@ function parseMultipartFormDataHeaders (input, position) {
265
367
  // 2.8. Byte-lowercase header name and switch on the result:
266
368
  switch (bufferToLowerCasedHeaderName(headerName)) {
267
369
  case 'content-disposition': {
268
- // 1. Set name and filename to null.
269
370
  name = filename = null
270
371
 
271
- // 2. If position does not point to a sequence of bytes starting with
272
- // `form-data; name="`, return failure.
273
- if (!bufferStartsWith(input, formDataNameBuffer, position)) {
274
- throw parsingError('expected form-data; name=" for content-disposition header')
372
+ // Collect the disposition type (should be "form-data")
373
+ const dispositionType = collectASequenceOfBytes(
374
+ (char) => isToken(char),
375
+ input,
376
+ position
377
+ )
378
+
379
+ if (dispositionType.toString('ascii').toLowerCase() !== 'form-data') {
380
+ throw parsingError('expected form-data for content-disposition header')
275
381
  }
276
382
 
277
- // 3. Advance position so it points at the byte after the next 0x22 (")
278
- // byte (the one in the sequence of bytes matched above).
279
- position.position += 17
280
-
281
- // 4. Set name to the result of parsing a multipart/form-data name given
282
- // input and position, if the result is not failure. Otherwise, return
283
- // failure.
284
- name = parseMultipartFormDataName(input, position)
285
-
286
- // 5. If position points to a sequence of bytes starting with `; filename="`:
287
- if (input[position.position] === 0x3b /* ; */ && input[position.position + 1] === 0x20 /* ' ' */) {
288
- const at = { position: position.position + 2 }
289
-
290
- if (bufferStartsWith(input, filenameBuffer, at)) {
291
- if (input[at.position + 8] === 0x2a /* '*' */) {
292
- at.position += 10 // skip past filename*=
293
-
294
- // Remove leading http tab and spaces. See RFC for examples.
295
- // https://datatracker.ietf.org/doc/html/rfc6266#section-5
296
- collectASequenceOfBytes(
297
- (char) => char === 0x20 || char === 0x09,
298
- input,
299
- at
300
- )
301
-
302
- const headerValue = collectASequenceOfBytes(
303
- (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a, // ' ' or CRLF
304
- input,
305
- at
306
- )
307
-
308
- if (
309
- (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
310
- (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
311
- (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
312
- headerValue[3] !== 0x2d || // -
313
- headerValue[4] !== 0x38 // 8
314
- ) {
315
- throw parsingError('unknown encoding, expected utf-8\'\'')
316
- }
317
-
318
- // skip utf-8''
319
- filename = decodeURIComponent(new TextDecoder().decode(headerValue.subarray(7)))
320
-
321
- position.position = at.position
322
- } else {
323
- // 1. Advance position so it points at the byte after the next 0x22 (") byte
324
- // (the one in the sequence of bytes matched above).
325
- position.position += 11
326
-
327
- // Remove leading http tab and spaces. See RFC for examples.
328
- // https://datatracker.ietf.org/doc/html/rfc6266#section-5
329
- collectASequenceOfBytes(
330
- (char) => char === 0x20 || char === 0x09,
331
- input,
332
- position
333
- )
334
-
335
- position.position++ // skip past " after removing whitespace
336
-
337
- // 2. Set filename to the result of parsing a multipart/form-data name given
338
- // input and position, if the result is not failure. Otherwise, return failure.
339
- filename = parseMultipartFormDataName(input, position)
340
- }
383
+ // Parse attributes recursively until CRLF
384
+ while (
385
+ position.position < input.length &&
386
+ input[position.position] !== 0x0d &&
387
+ input[position.position + 1] !== 0x0a
388
+ ) {
389
+ const attribute = parseContentDispositionAttribute(input, position)
390
+
391
+ if (!attribute) {
392
+ break
341
393
  }
394
+
395
+ if (attribute.name === 'name') {
396
+ name = attribute.value
397
+ } else if (attribute.name === 'filename') {
398
+ filename = attribute.value
399
+ }
400
+ }
401
+
402
+ if (name === null) {
403
+ throw parsingError('name attribute is required in content-disposition header')
342
404
  }
343
405
 
344
406
  break
@@ -394,43 +456,6 @@ function parseMultipartFormDataHeaders (input, position) {
394
456
  }
395
457
  }
396
458
 
397
- /**
398
- * @see https://andreubotella.github.io/multipart-form-data/#parse-a-multipart-form-data-name
399
- * @param {Buffer} input
400
- * @param {{ position: number }} position
401
- */
402
- function parseMultipartFormDataName (input, position) {
403
- // 1. Assert: The byte at (position - 1) is 0x22 (").
404
- assert(input[position.position - 1] === 0x22)
405
-
406
- // 2. Let name be the result of collecting a sequence of bytes that are not 0x0A (LF), 0x0D (CR) or 0x22 ("), given position.
407
- /** @type {string | Buffer} */
408
- let name = collectASequenceOfBytes(
409
- (char) => char !== 0x0a && char !== 0x0d && char !== 0x22,
410
- input,
411
- position
412
- )
413
-
414
- // 3. If the byte at position is not 0x22 ("), return failure. Otherwise, advance position by 1.
415
- if (input[position.position] !== 0x22) {
416
- throw parsingError('expected "')
417
- } else {
418
- position.position++
419
- }
420
-
421
- // 4. Replace any occurrence of the following subsequences in name with the given byte:
422
- // - `%0A`: 0x0A (LF)
423
- // - `%0D`: 0x0D (CR)
424
- // - `%22`: 0x22 (")
425
- name = new TextDecoder().decode(name)
426
- .replace(/%0A/ig, '\n')
427
- .replace(/%0D/ig, '\r')
428
- .replace(/%22/g, '"')
429
-
430
- // 5. Return the UTF-8 decoding without BOM of name.
431
- return name
432
- }
433
-
434
459
  /**
435
460
  * @param {(char: number) => boolean} condition
436
461
  * @param {Buffer} input
@@ -492,6 +517,58 @@ function parsingError (cause) {
492
517
  return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
493
518
  }
494
519
 
520
+ /**
521
+ * CTL = <any US-ASCII control character
522
+ * (octets 0 - 31) and DEL (127)>
523
+ * @param {number} char
524
+ */
525
+ function isCTL (char) {
526
+ return char <= 0x1f || char === 0x7f
527
+ }
528
+
529
+ /**
530
+ * tspecials := "(" / ")" / "<" / ">" / "@" /
531
+ * "," / ";" / ":" / "\" / <">
532
+ * "/" / "[" / "]" / "?" / "="
533
+ * ; Must be in quoted-string,
534
+ * ; to use within parameter values
535
+ * @param {number} char
536
+ */
537
+ function isTSpecial (char) {
538
+ return (
539
+ char === 0x28 || // (
540
+ char === 0x29 || // )
541
+ char === 0x3c || // <
542
+ char === 0x3e || // >
543
+ char === 0x40 || // @
544
+ char === 0x2c || // ,
545
+ char === 0x3b || // ;
546
+ char === 0x3a || // :
547
+ char === 0x5c || // \
548
+ char === 0x22 || // "
549
+ char === 0x2f || // /
550
+ char === 0x5b || // [
551
+ char === 0x5d || // ]
552
+ char === 0x3f || // ?
553
+ char === 0x3d // +
554
+ )
555
+ }
556
+
557
+ /**
558
+ * token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
559
+ * or tspecials>
560
+ * @param {number} char
561
+ */
562
+ function isToken (char) {
563
+ return (
564
+ char <= 0x7f && // ascii
565
+ char !== 0x20 && // space
566
+ char !== 0x09 &&
567
+ !isCTL(char) &&
568
+ !isTSpecial(char)
569
+ )
570
+ }
571
+
495
572
  module.exports = {
496
573
  multipartFormDataParser,
497
574
  validateBoundary
@@ -35,7 +35,6 @@ const {
35
35
  isErrorLike,
36
36
  fullyReadBody,
37
37
  readableStreamClose,
38
- isomorphicEncode,
39
38
  urlIsLocal,
40
39
  urlIsHttpHttpsScheme,
41
40
  urlHasHttpsScheme,
@@ -63,8 +62,11 @@ const { webidl } = require('../webidl')
63
62
  const { STATUS_CODES } = require('node:http')
64
63
  const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
65
64
  const { createDeferredPromise } = require('../../util/promise')
65
+ const { isomorphicEncode } = require('../infra')
66
+ const { runtimeFeatures } = require('../../util/runtime-features')
66
67
 
67
- const hasZstd = typeof zlib.createZstdDecompress === 'function'
68
+ // Node.js v23.8.0+ and v22.15.0+ supports Zstandard
69
+ const hasZstd = runtimeFeatures.has('zstd')
68
70
 
69
71
  const GET_OR_HEAD = ['GET', 'HEAD']
70
72
 
@@ -887,7 +889,7 @@ function schemeFetch (fetchParams) {
887
889
 
888
890
  // 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart,
889
891
  // rangeEnd + 1, and type.
890
- const slicedBlob = blob.slice(rangeStart, rangeEnd, type)
892
+ const slicedBlob = blob.slice(rangeStart, rangeEnd + 1, type)
891
893
 
892
894
  // 9. Let slicedBodyWithType be the result of safely extracting slicedBlob.
893
895
  // Note: same reason as mentioned above as to why we use extractBody
@@ -2128,6 +2130,15 @@ async function httpNetworkFetch (
2128
2130
  // "All content-coding values are case-insensitive..."
2129
2131
  /** @type {string[]} */
2130
2132
  const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
2133
+
2134
+ // Limit the number of content-encodings to prevent resource exhaustion.
2135
+ // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
2136
+ const maxContentEncodings = 5
2137
+ if (codings.length > maxContentEncodings) {
2138
+ reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
2139
+ return true
2140
+ }
2141
+
2131
2142
  for (let i = codings.length - 1; i >= 0; --i) {
2132
2143
  const coding = codings[i].trim()
2133
2144
  // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
@@ -2151,7 +2162,6 @@ async function httpNetworkFetch (
2151
2162
  finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
2152
2163
  }))
2153
2164
  } else if (coding === 'zstd' && hasZstd) {
2154
- // Node.js v23.8.0+ and v22.15.0+ supports Zstandard
2155
2165
  decoders.push(zlib.createZstdDecompress({
2156
2166
  flush: zlib.constants.ZSTD_e_continue,
2157
2167
  finishFlush: zlib.constants.ZSTD_e_end
@@ -2227,8 +2237,10 @@ async function httpNetworkFetch (
2227
2237
  },
2228
2238
 
2229
2239
  onUpgrade (status, rawHeaders, socket) {
2230
- if (status !== 101) {
2231
- return
2240
+ // We need to support 200 for websocket over h2 as per RFC-8441
2241
+ // Absence of session means H1
2242
+ if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
2243
+ return false
2232
2244
  }
2233
2245
 
2234
2246
  const headersList = new HeadersList()
@@ -1094,6 +1094,12 @@ webidl.converters.RequestInit = webidl.dictionaryConverter([
1094
1094
  {
1095
1095
  key: 'dispatcher', // undici specific option
1096
1096
  converter: webidl.converters.any
1097
+ },
1098
+ {
1099
+ key: 'priority',
1100
+ converter: webidl.converters.DOMString,
1101
+ allowedValues: ['high', 'low', 'auto'],
1102
+ defaultValue: () => 'auto'
1097
1103
  }
1098
1104
  ])
1099
1105
 
@@ -9,9 +9,7 @@ const {
9
9
  isValidReasonPhrase,
10
10
  isCancelled,
11
11
  isAborted,
12
- serializeJavascriptValueToJSONString,
13
12
  isErrorLike,
14
- isomorphicEncode,
15
13
  environmentSettingsObject: relevantRealm
16
14
  } = require('./util')
17
15
  const {
@@ -22,6 +20,7 @@ const { webidl } = require('../webidl')
22
20
  const { URLSerializer } = require('./data-url')
23
21
  const { kConstruct } = require('../../core/symbols')
24
22
  const assert = require('node:assert')
23
+ const { isomorphicEncode, serializeJavascriptValueToJSONString } = require('../infra')
25
24
 
26
25
  const textEncoder = new TextEncoder('utf-8')
27
26
 
@@ -454,7 +453,7 @@ function filterResponse (response, type) {
454
453
 
455
454
  return makeFilteredResponse(response, {
456
455
  type: 'opaque',
457
- urlList: Object.freeze([]),
456
+ urlList: [],
458
457
  status: 0,
459
458
  statusText: '',
460
459
  body: null
@@ -4,12 +4,13 @@ const { Transform } = require('node:stream')
4
4
  const zlib = require('node:zlib')
5
5
  const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants')
6
6
  const { getGlobalOrigin } = require('./global')
7
- const { collectASequenceOfCodePoints, collectAnHTTPQuotedString, removeChars, parseMIMEType } = require('./data-url')
7
+ const { collectAnHTTPQuotedString, parseMIMEType } = require('./data-url')
8
8
  const { performance } = require('node:perf_hooks')
9
9
  const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
10
10
  const assert = require('node:assert')
11
11
  const { isUint8Array } = require('node:util/types')
12
12
  const { webidl } = require('../webidl')
13
+ const { isomorphicEncode, collectASequenceOfCodePoints, removeChars } = require('../infra')
13
14
 
14
15
  function responseURL (response) {
15
16
  // https://fetch.spec.whatwg.org/#responses
@@ -721,23 +722,6 @@ function normalizeMethod (method) {
721
722
  return normalizedMethodRecordsBase[method.toLowerCase()] ?? method
722
723
  }
723
724
 
724
- // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string
725
- function serializeJavascriptValueToJSONString (value) {
726
- // 1. Let result be ? Call(%JSON.stringify%, undefined, « value »).
727
- const result = JSON.stringify(value)
728
-
729
- // 2. If result is undefined, then throw a TypeError.
730
- if (result === undefined) {
731
- throw new TypeError('Value is not JSON serializable')
732
- }
733
-
734
- // 3. Assert: result is a string.
735
- assert(typeof result === 'string')
736
-
737
- // 4. Return result.
738
- return result
739
- }
740
-
741
725
  // https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
742
726
  const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
743
727
 
@@ -993,22 +977,6 @@ function readableStreamClose (controller) {
993
977
  }
994
978
  }
995
979
 
996
- const invalidIsomorphicEncodeValueRegex = /[^\x00-\xFF]/ // eslint-disable-line
997
-
998
- /**
999
- * @see https://infra.spec.whatwg.org/#isomorphic-encode
1000
- * @param {string} input
1001
- */
1002
- function isomorphicEncode (input) {
1003
- // 1. Assert: input contains no code points greater than U+00FF.
1004
- assert(!invalidIsomorphicEncodeValueRegex.test(input))
1005
-
1006
- // 2. Return a byte sequence whose length is equal to input’s code
1007
- // point length and whose bytes have the same values as the
1008
- // values of input’s code points, in the same order
1009
- return input
1010
- }
1011
-
1012
980
  /**
1013
981
  * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
1014
982
  * @see https://streams.spec.whatwg.org/#read-loop
@@ -1461,34 +1429,6 @@ function getDecodeSplit (name, list) {
1461
1429
  return gettingDecodingSplitting(value)
1462
1430
  }
1463
1431
 
1464
- const textDecoder = new TextDecoder()
1465
-
1466
- /**
1467
- * @see https://encoding.spec.whatwg.org/#utf-8-decode
1468
- * @param {Buffer} buffer
1469
- */
1470
- function utf8DecodeBytes (buffer) {
1471
- if (buffer.length === 0) {
1472
- return ''
1473
- }
1474
-
1475
- // 1. Let buffer be the result of peeking three bytes from
1476
- // ioQueue, converted to a byte sequence.
1477
-
1478
- // 2. If buffer is 0xEF 0xBB 0xBF, then read three
1479
- // bytes from ioQueue. (Do nothing with those bytes.)
1480
- if (buffer[0] === 0xEF && buffer[1] === 0xBB && buffer[2] === 0xBF) {
1481
- buffer = buffer.subarray(3)
1482
- }
1483
-
1484
- // 3. Process a queue with an instance of UTF-8’s
1485
- // decoder, ioQueue, output, and "replacement".
1486
- const output = textDecoder.decode(buffer)
1487
-
1488
- // 4. Return output.
1489
- return output
1490
- }
1491
-
1492
1432
  class EnvironmentSettingsObjectBase {
1493
1433
  get baseUrl () {
1494
1434
  return getGlobalOrigin()
@@ -1534,7 +1474,6 @@ module.exports = {
1534
1474
  isValidReasonPhrase,
1535
1475
  sameOrigin,
1536
1476
  normalizeMethod,
1537
- serializeJavascriptValueToJSONString,
1538
1477
  iteratorMixin,
1539
1478
  createIterator,
1540
1479
  isValidHeaderName,
@@ -1542,7 +1481,6 @@ module.exports = {
1542
1481
  isErrorLike,
1543
1482
  fullyReadBody,
1544
1483
  readableStreamClose,
1545
- isomorphicEncode,
1546
1484
  urlIsLocal,
1547
1485
  urlHasHttpsScheme,
1548
1486
  urlIsHttpHttpsScheme,
@@ -1552,7 +1490,6 @@ module.exports = {
1552
1490
  createInflate,
1553
1491
  extractMimeType,
1554
1492
  getDecodeSplit,
1555
- utf8DecodeBytes,
1556
1493
  environmentSettingsObject,
1557
1494
  isOriginIPPotentiallyTrustworthy
1558
1495
  }