undici 7.0.0-alpha.1 → 7.0.0-alpha.2

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 (71) hide show
  1. package/README.md +2 -2
  2. package/docs/docs/api/Client.md +1 -1
  3. package/docs/docs/api/Debug.md +1 -1
  4. package/docs/docs/api/Dispatcher.md +53 -2
  5. package/docs/docs/api/MockAgent.md +2 -0
  6. package/docs/docs/api/MockPool.md +2 -1
  7. package/docs/docs/api/RetryAgent.md +1 -1
  8. package/docs/docs/api/RetryHandler.md +1 -1
  9. package/docs/docs/api/WebSocket.md +45 -3
  10. package/index.js +6 -2
  11. package/lib/api/abort-signal.js +2 -0
  12. package/lib/api/api-pipeline.js +4 -2
  13. package/lib/api/api-request.js +4 -2
  14. package/lib/api/api-stream.js +3 -1
  15. package/lib/api/api-upgrade.js +2 -2
  16. package/lib/api/readable.js +194 -41
  17. package/lib/api/util.js +2 -0
  18. package/lib/core/connect.js +49 -22
  19. package/lib/core/constants.js +11 -9
  20. package/lib/core/diagnostics.js +122 -128
  21. package/lib/core/request.js +4 -4
  22. package/lib/core/symbols.js +2 -0
  23. package/lib/core/tree.js +4 -2
  24. package/lib/core/util.js +220 -39
  25. package/lib/dispatcher/client-h1.js +299 -60
  26. package/lib/dispatcher/client-h2.js +1 -1
  27. package/lib/dispatcher/client.js +24 -7
  28. package/lib/dispatcher/fixed-queue.js +91 -49
  29. package/lib/dispatcher/pool-stats.js +2 -0
  30. package/lib/dispatcher/proxy-agent.js +3 -1
  31. package/lib/handler/redirect-handler.js +2 -2
  32. package/lib/handler/retry-handler.js +2 -2
  33. package/lib/interceptor/dns.js +346 -0
  34. package/lib/mock/mock-agent.js +5 -8
  35. package/lib/mock/mock-client.js +7 -2
  36. package/lib/mock/mock-errors.js +3 -1
  37. package/lib/mock/mock-interceptor.js +8 -6
  38. package/lib/mock/mock-pool.js +7 -2
  39. package/lib/mock/mock-symbols.js +2 -1
  40. package/lib/mock/mock-utils.js +33 -5
  41. package/lib/util/timers.js +50 -6
  42. package/lib/web/cache/cache.js +24 -21
  43. package/lib/web/cache/cachestorage.js +1 -1
  44. package/lib/web/cookies/index.js +6 -4
  45. package/lib/web/fetch/body.js +42 -34
  46. package/lib/web/fetch/constants.js +35 -26
  47. package/lib/web/fetch/formdata-parser.js +14 -3
  48. package/lib/web/fetch/formdata.js +40 -20
  49. package/lib/web/fetch/headers.js +116 -84
  50. package/lib/web/fetch/index.js +65 -59
  51. package/lib/web/fetch/request.js +130 -55
  52. package/lib/web/fetch/response.js +79 -36
  53. package/lib/web/fetch/util.js +104 -57
  54. package/lib/web/fetch/webidl.js +38 -14
  55. package/lib/web/websocket/connection.js +92 -15
  56. package/lib/web/websocket/constants.js +2 -3
  57. package/lib/web/websocket/events.js +4 -2
  58. package/lib/web/websocket/receiver.js +20 -26
  59. package/lib/web/websocket/stream/websocketerror.js +83 -0
  60. package/lib/web/websocket/stream/websocketstream.js +485 -0
  61. package/lib/web/websocket/util.js +115 -10
  62. package/lib/web/websocket/websocket.js +45 -170
  63. package/package.json +6 -6
  64. package/types/interceptors.d.ts +14 -0
  65. package/types/mock-agent.d.ts +3 -0
  66. package/types/readable.d.ts +10 -7
  67. package/types/webidl.d.ts +24 -4
  68. package/types/websocket.d.ts +33 -0
  69. package/lib/mock/pluralizer.js +0 -29
  70. package/lib/web/cache/symbols.js +0 -5
  71. package/lib/web/fetch/symbols.js +0 -8
package/lib/core/util.js CHANGED
@@ -28,6 +28,10 @@ class BodyAsyncIterable {
28
28
  }
29
29
  }
30
30
 
31
+ /**
32
+ * @param {*} body
33
+ * @returns {*}
34
+ */
31
35
  function wrapRequestBody (body) {
32
36
  if (isStream(body)) {
33
37
  // TODO (fix): Provide some way for the user to cache the file to e.g. /tmp
@@ -67,13 +71,19 @@ function wrapRequestBody (body) {
67
71
  }
68
72
  }
69
73
 
70
- function nop () {}
71
-
74
+ /**
75
+ * @param {*} obj
76
+ * @returns {obj is import('node:stream').Stream}
77
+ */
72
78
  function isStream (obj) {
73
79
  return obj && typeof obj === 'object' && typeof obj.pipe === 'function' && typeof obj.on === 'function'
74
80
  }
75
81
 
76
- // based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
82
+ /**
83
+ * @param {*} object
84
+ * @returns {object is Blob}
85
+ * based on https://github.com/node-fetch/fetch-blob/blob/8ab587d34080de94140b54f07168451e7d0b655e/index.js#L229-L241 (MIT License)
86
+ */
77
87
  function isBlobLike (object) {
78
88
  if (object === null) {
79
89
  return false
@@ -91,7 +101,12 @@ function isBlobLike (object) {
91
101
  }
92
102
  }
93
103
 
94
- function buildURL (url, queryParams) {
104
+ /**
105
+ * @param {string} url The URL to add the query params to
106
+ * @param {import('node:querystring').ParsedUrlQueryInput} queryParams The object to serialize into a URL query string
107
+ * @returns {string} The URL with the query params added
108
+ */
109
+ function serializePathWithQuery (url, queryParams) {
95
110
  if (url.includes('?') || url.includes('#')) {
96
111
  throw new Error('Query params cannot be passed when url already contains "?" or "#".')
97
112
  }
@@ -105,6 +120,10 @@ function buildURL (url, queryParams) {
105
120
  return url
106
121
  }
107
122
 
123
+ /**
124
+ * @param {number|string|undefined} port
125
+ * @returns {boolean}
126
+ */
108
127
  function isValidPort (port) {
109
128
  const value = parseInt(port, 10)
110
129
  return (
@@ -114,6 +133,12 @@ function isValidPort (port) {
114
133
  )
115
134
  }
116
135
 
136
+ /**
137
+ * Check if the value is a valid http or https prefixed string.
138
+ *
139
+ * @param {string} value
140
+ * @returns {boolean}
141
+ */
117
142
  function isHttpOrHttpsPrefixed (value) {
118
143
  return (
119
144
  value != null &&
@@ -131,8 +156,15 @@ function isHttpOrHttpsPrefixed (value) {
131
156
  )
132
157
  }
133
158
 
159
+ /**
160
+ * @param {string|URL|Record<string,string>} url
161
+ * @returns {URL}
162
+ */
134
163
  function parseURL (url) {
135
164
  if (typeof url === 'string') {
165
+ /**
166
+ * @type {URL}
167
+ */
136
168
  url = new URL(url)
137
169
 
138
170
  if (!isHttpOrHttpsPrefixed(url.origin || url.protocol)) {
@@ -202,6 +234,10 @@ function parseURL (url) {
202
234
  return url
203
235
  }
204
236
 
237
+ /**
238
+ * @param {string|URL|Record<string, string>} url
239
+ * @returns {URL}
240
+ */
205
241
  function parseOrigin (url) {
206
242
  url = parseURL(url)
207
243
 
@@ -212,6 +248,10 @@ function parseOrigin (url) {
212
248
  return url
213
249
  }
214
250
 
251
+ /**
252
+ * @param {string} host
253
+ * @returns {string}
254
+ */
215
255
  function getHostname (host) {
216
256
  if (host[0] === '[') {
217
257
  const idx = host.indexOf(']')
@@ -226,14 +266,18 @@ function getHostname (host) {
226
266
  return host.substring(0, idx)
227
267
  }
228
268
 
229
- // IP addresses are not valid server names per RFC6066
230
- // > Currently, the only server names supported are DNS hostnames
269
+ /**
270
+ * IP addresses are not valid server names per RFC6066
271
+ * Currently, the only server names supported are DNS hostnames
272
+ * @param {string|null} host
273
+ * @returns {string|null}
274
+ */
231
275
  function getServerName (host) {
232
276
  if (!host) {
233
277
  return null
234
278
  }
235
279
 
236
- assert.strictEqual(typeof host, 'string')
280
+ assert(typeof host === 'string')
237
281
 
238
282
  const servername = getHostname(host)
239
283
  if (net.isIP(servername)) {
@@ -243,18 +287,36 @@ function getServerName (host) {
243
287
  return servername
244
288
  }
245
289
 
290
+ /**
291
+ * @function
292
+ * @template T
293
+ * @param {T} obj
294
+ * @returns {T}
295
+ */
246
296
  function deepClone (obj) {
247
297
  return JSON.parse(JSON.stringify(obj))
248
298
  }
249
299
 
300
+ /**
301
+ * @param {*} obj
302
+ * @returns {obj is AsyncIterable}
303
+ */
250
304
  function isAsyncIterable (obj) {
251
305
  return !!(obj != null && typeof obj[Symbol.asyncIterator] === 'function')
252
306
  }
253
307
 
308
+ /**
309
+ * @param {*} obj
310
+ * @returns {obj is Iterable}
311
+ */
254
312
  function isIterable (obj) {
255
313
  return !!(obj != null && (typeof obj[Symbol.iterator] === 'function' || typeof obj[Symbol.asyncIterator] === 'function'))
256
314
  }
257
315
 
316
+ /**
317
+ * @param {Blob|Buffer|import ('stream').Stream} body
318
+ * @returns {number|null}
319
+ */
258
320
  function bodyLength (body) {
259
321
  if (body == null) {
260
322
  return 0
@@ -272,10 +334,19 @@ function bodyLength (body) {
272
334
  return null
273
335
  }
274
336
 
337
+ /**
338
+ * @param {import ('stream').Stream} body
339
+ * @returns {boolean}
340
+ */
275
341
  function isDestroyed (body) {
276
342
  return body && !!(body.destroyed || body[kDestroyed] || (stream.isDestroyed?.(body)))
277
343
  }
278
344
 
345
+ /**
346
+ * @param {import ('stream').Stream} stream
347
+ * @param {Error} [err]
348
+ * @returns {void}
349
+ */
279
350
  function destroy (stream, err) {
280
351
  if (stream == null || !isStream(stream) || isDestroyed(stream)) {
281
352
  return
@@ -300,8 +371,12 @@ function destroy (stream, err) {
300
371
  }
301
372
 
302
373
  const KEEPALIVE_TIMEOUT_EXPR = /timeout=(\d+)/
374
+ /**
375
+ * @param {string} val
376
+ * @returns {number | null}
377
+ */
303
378
  function parseKeepAliveTimeout (val) {
304
- const m = val.toString().match(KEEPALIVE_TIMEOUT_EXPR)
379
+ const m = val.match(KEEPALIVE_TIMEOUT_EXPR)
305
380
  return m ? parseInt(m[1], 10) * 1000 : null
306
381
  }
307
382
 
@@ -326,12 +401,13 @@ function bufferToLowerCasedHeaderName (value) {
326
401
  }
327
402
 
328
403
  /**
329
- * @param {Record<string, string | string[]> | (Buffer | string | (Buffer | string)[])[]} headers
404
+ * @param {(Buffer | string)[]} headers
330
405
  * @param {Record<string, string | string[]>} [obj]
331
406
  * @returns {Record<string, string | string[]>}
332
407
  */
333
408
  function parseHeaders (headers, obj) {
334
409
  if (obj === undefined) obj = {}
410
+
335
411
  for (let i = 0; i < headers.length; i += 2) {
336
412
  const key = headerNameToString(headers[i])
337
413
  let val = obj[key]
@@ -360,9 +436,16 @@ function parseHeaders (headers, obj) {
360
436
  return obj
361
437
  }
362
438
 
439
+ /**
440
+ * @param {Buffer[]} headers
441
+ * @returns {string[]}
442
+ */
363
443
  function parseRawHeaders (headers) {
364
- const len = headers.length
365
- const ret = new Array(len)
444
+ const headersLength = headers.length
445
+ /**
446
+ * @type {string[]}
447
+ */
448
+ const ret = new Array(headersLength)
366
449
 
367
450
  let hasContentLength = false
368
451
  let contentDispositionIdx = -1
@@ -370,7 +453,7 @@ function parseRawHeaders (headers) {
370
453
  let val
371
454
  let kLen = 0
372
455
 
373
- for (let n = 0; n < headers.length; n += 2) {
456
+ for (let n = 0; n < headersLength; n += 2) {
374
457
  key = headers[n]
375
458
  val = headers[n + 1]
376
459
 
@@ -395,12 +478,24 @@ function parseRawHeaders (headers) {
395
478
  return ret
396
479
  }
397
480
 
481
+ /**
482
+ * @param {*} buffer
483
+ * @returns {buffer is Buffer}
484
+ */
398
485
  function isBuffer (buffer) {
399
486
  // See, https://github.com/mcollina/undici/pull/319
400
487
  return buffer instanceof Uint8Array || Buffer.isBuffer(buffer)
401
488
  }
402
489
 
403
- function validateHandler (handler, method, upgrade) {
490
+ /**
491
+ * Asserts that the handler object is a request handler.
492
+ *
493
+ * @param {object} handler
494
+ * @param {string} method
495
+ * @param {string} [upgrade]
496
+ * @returns {asserts handler is import('../api/api-request').RequestHandler}
497
+ */
498
+ function assertRequestHandler (handler, method, upgrade) {
404
499
  if (!handler || typeof handler !== 'object') {
405
500
  throw new InvalidArgumentError('handler must be an object')
406
501
  }
@@ -436,13 +531,33 @@ function validateHandler (handler, method, upgrade) {
436
531
  }
437
532
  }
438
533
 
439
- // A body is disturbed if it has been read from and it cannot
440
- // be re-used without losing state or data.
534
+ /**
535
+ * A body is disturbed if it has been read from and it cannot be re-used without
536
+ * losing state or data.
537
+ * @param {import('node:stream').Readable} body
538
+ * @returns {boolean}
539
+ */
441
540
  function isDisturbed (body) {
442
541
  // TODO (fix): Why is body[kBodyUsed] needed?
443
542
  return !!(body && (stream.isDisturbed(body) || body[kBodyUsed]))
444
543
  }
445
544
 
545
+ /**
546
+ * @typedef {object} SocketInfo
547
+ * @property {string} [localAddress]
548
+ * @property {number} [localPort]
549
+ * @property {string} [remoteAddress]
550
+ * @property {number} [remotePort]
551
+ * @property {string} [remoteFamily]
552
+ * @property {number} [timeout]
553
+ * @property {number} bytesWritten
554
+ * @property {number} bytesRead
555
+ */
556
+
557
+ /**
558
+ * @param {import('net').Socket} socket
559
+ * @returns {SocketInfo}
560
+ */
446
561
  function getSocketInfo (socket) {
447
562
  return {
448
563
  localAddress: socket.localAddress,
@@ -456,7 +571,10 @@ function getSocketInfo (socket) {
456
571
  }
457
572
  }
458
573
 
459
- /** @type {globalThis['ReadableStream']} */
574
+ /**
575
+ * @param {Iterable} iterable
576
+ * @returns {ReadableStream}
577
+ */
460
578
  function ReadableStreamFrom (iterable) {
461
579
  // We cannot use ReadableStream.from here because it does not return a byte stream.
462
580
 
@@ -481,7 +599,7 @@ function ReadableStreamFrom (iterable) {
481
599
  }
482
600
  return controller.desiredSize > 0
483
601
  },
484
- async cancel (reason) {
602
+ async cancel () {
485
603
  await iterator.return()
486
604
  },
487
605
  type: 'bytes'
@@ -489,8 +607,12 @@ function ReadableStreamFrom (iterable) {
489
607
  )
490
608
  }
491
609
 
492
- // The chunk should be a FormData instance and contains
493
- // all the required methods.
610
+ /**
611
+ * The object should be a FormData instance and contains all the required
612
+ * methods.
613
+ * @param {*} object
614
+ * @returns {object is FormData}
615
+ */
494
616
  function isFormDataLike (object) {
495
617
  return (
496
618
  object &&
@@ -514,27 +636,52 @@ function addAbortListener (signal, listener) {
514
636
  return () => signal.removeListener('abort', listener)
515
637
  }
516
638
 
517
- const hasToWellFormed = typeof String.prototype.toWellFormed === 'function'
518
- const hasIsWellFormed = typeof String.prototype.isWellFormed === 'function'
519
-
520
639
  /**
521
- * @param {string} val
640
+ * @function
641
+ * @param {string} value
642
+ * @returns {string}
522
643
  */
523
- function toUSVString (val) {
524
- return hasToWellFormed ? `${val}`.toWellFormed() : nodeUtil.toUSVString(val)
525
- }
644
+ const toUSVString = (() => {
645
+ if (typeof String.prototype.toWellFormed === 'function') {
646
+ /**
647
+ * @param {string} value
648
+ * @returns {string}
649
+ */
650
+ return (value) => `${value}`.toWellFormed()
651
+ } else {
652
+ /**
653
+ * @param {string} value
654
+ * @returns {string}
655
+ */
656
+ return nodeUtil.toUSVString
657
+ }
658
+ })()
526
659
 
527
660
  /**
528
- * @param {string} val
661
+ * @param {*} value
662
+ * @returns {boolean}
529
663
  */
530
664
  // TODO: move this to webidl
531
- function isUSVString (val) {
532
- return hasIsWellFormed ? `${val}`.isWellFormed() : toUSVString(val) === `${val}`
533
- }
665
+ const isUSVString = (() => {
666
+ if (typeof String.prototype.isWellFormed === 'function') {
667
+ /**
668
+ * @param {*} value
669
+ * @returns {boolean}
670
+ */
671
+ return (value) => `${value}`.isWellFormed()
672
+ } else {
673
+ /**
674
+ * @param {*} value
675
+ * @returns {boolean}
676
+ */
677
+ return (value) => toUSVString(value) === `${value}`
678
+ }
679
+ })()
534
680
 
535
681
  /**
536
682
  * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
537
683
  * @param {number} c
684
+ * @returns {boolean}
538
685
  */
539
686
  function isTokenCharCode (c) {
540
687
  switch (c) {
@@ -565,6 +712,7 @@ function isTokenCharCode (c) {
565
712
 
566
713
  /**
567
714
  * @param {string} characters
715
+ * @returns {boolean}
568
716
  */
569
717
  function isValidHTTPToken (characters) {
570
718
  if (characters.length === 0) {
@@ -591,17 +739,31 @@ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/
591
739
 
592
740
  /**
593
741
  * @param {string} characters
742
+ * @returns {boolean}
594
743
  */
595
744
  function isValidHeaderValue (characters) {
596
745
  return !headerCharRegex.test(characters)
597
746
  }
598
747
 
599
- // Parsed accordingly to RFC 9110
600
- // https://www.rfc-editor.org/rfc/rfc9110#field.content-range
748
+ const rangeHeaderRegex = /^bytes (\d+)-(\d+)\/(\d+)?$/
749
+
750
+ /**
751
+ * @typedef {object} RangeHeader
752
+ * @property {number} start
753
+ * @property {number | null} end
754
+ * @property {number | null} size
755
+ */
756
+
757
+ /**
758
+ * Parse accordingly to RFC 9110
759
+ * @see https://www.rfc-editor.org/rfc/rfc9110#field.content-range
760
+ * @param {string} [range]
761
+ * @returns {RangeHeader|null}
762
+ */
601
763
  function parseRangeHeader (range) {
602
764
  if (range == null || range === '') return { start: 0, end: null, size: null }
603
765
 
604
- const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null
766
+ const m = range ? range.match(rangeHeaderRegex) : null
605
767
  return m
606
768
  ? {
607
769
  start: parseInt(m[1]),
@@ -611,6 +773,13 @@ function parseRangeHeader (range) {
611
773
  : null
612
774
  }
613
775
 
776
+ /**
777
+ * @template {import("events").EventEmitter} T
778
+ * @param {T} obj
779
+ * @param {string} name
780
+ * @param {(...args: any[]) => void} listener
781
+ * @returns {T}
782
+ */
614
783
  function addListener (obj, name, listener) {
615
784
  const listeners = (obj[kListeners] ??= [])
616
785
  listeners.push([name, listener])
@@ -618,13 +787,26 @@ function addListener (obj, name, listener) {
618
787
  return obj
619
788
  }
620
789
 
790
+ /**
791
+ * @template {import("events").EventEmitter} T
792
+ * @param {T} obj
793
+ * @returns {T}
794
+ */
621
795
  function removeAllListeners (obj) {
622
- for (const [name, listener] of obj[kListeners] ?? []) {
623
- obj.removeListener(name, listener)
796
+ if (obj[kListeners] != null) {
797
+ for (const [name, listener] of obj[kListeners]) {
798
+ obj.removeListener(name, listener)
799
+ }
800
+ obj[kListeners] = null
624
801
  }
625
- obj[kListeners] = null
802
+ return obj
626
803
  }
627
804
 
805
+ /**
806
+ * @param {import ('../dispatcher/client')} client
807
+ * @param {import ('../core/request')} request
808
+ * @param {Error} err
809
+ */
628
810
  function errorRequest (client, request, err) {
629
811
  try {
630
812
  request.onError(err)
@@ -664,7 +846,6 @@ Object.setPrototypeOf(normalizedMethodRecords, null)
664
846
 
665
847
  module.exports = {
666
848
  kEnumerableProperty,
667
- nop,
668
849
  isDisturbed,
669
850
  toUSVString,
670
851
  isUSVString,
@@ -689,10 +870,10 @@ module.exports = {
689
870
  deepClone,
690
871
  ReadableStreamFrom,
691
872
  isBuffer,
692
- validateHandler,
873
+ assertRequestHandler,
693
874
  getSocketInfo,
694
875
  isFormDataLike,
695
- buildURL,
876
+ serializePathWithQuery,
696
877
  addAbortListener,
697
878
  isValidHTTPToken,
698
879
  isValidHeaderValue,