undici 6.16.1 → 6.17.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.
@@ -952,6 +952,38 @@ const client = new Client("http://example.com").compose(
952
952
  );
953
953
  ```
954
954
 
955
+ ##### `dump`
956
+
957
+ The `dump` interceptor enables you to dump the response body from a request upon a given limit.
958
+
959
+ **Options**
960
+ - `maxSize` - The maximum size (in bytes) of the response body to dump. If the size of the request's body exceeds this value then the connection will be closed. Default: `1048576`.
961
+
962
+ > The `Dispatcher#options` also gets extended with the options `dumpMaxSize`, `abortOnDumped`, and `waitForTrailers` which can be used to configure the interceptor at a request-per-request basis.
963
+
964
+ **Example - Basic Dump Interceptor**
965
+
966
+ ```js
967
+ const { Client, interceptors } = require("undici");
968
+ const { dump } = interceptors;
969
+
970
+ const client = new Client("http://example.com").compose(
971
+ dump({
972
+ maxSize: 1024,
973
+ })
974
+ );
975
+
976
+ // or
977
+ client.dispatch(
978
+ {
979
+ path: "/",
980
+ method: "GET",
981
+ dumpMaxSize: 1024,
982
+ },
983
+ handler
984
+ );
985
+ ```
986
+
955
987
  ## Instance Events
956
988
 
957
989
  ### Event: `'connect'`
package/index-fetch.js CHANGED
@@ -7,7 +7,7 @@ const fetchImpl = require('./lib/web/fetch').fetch
7
7
  module.exports.fetch = function fetch (resource, init = undefined) {
8
8
  return fetchImpl(resource, init).catch((err) => {
9
9
  if (err && typeof err === 'object') {
10
- Error.captureStackTrace(err, this)
10
+ Error.captureStackTrace(err)
11
11
  }
12
12
  throw err
13
13
  })
package/index.js CHANGED
@@ -40,7 +40,8 @@ module.exports.RedirectHandler = RedirectHandler
40
40
  module.exports.createRedirectInterceptor = createRedirectInterceptor
41
41
  module.exports.interceptors = {
42
42
  redirect: require('./lib/interceptor/redirect'),
43
- retry: require('./lib/interceptor/retry')
43
+ retry: require('./lib/interceptor/retry'),
44
+ dump: require('./lib/interceptor/dump')
44
45
  }
45
46
 
46
47
  module.exports.buildConnector = buildConnector
@@ -108,7 +109,7 @@ module.exports.fetch = async function fetch (init, options = undefined) {
108
109
  return await fetchImpl(init, options)
109
110
  } catch (err) {
110
111
  if (err && typeof err === 'object') {
111
- Error.captureStackTrace(err, this)
112
+ Error.captureStackTrace(err)
112
113
  }
113
114
 
114
115
  throw err
@@ -8,7 +8,6 @@ module.exports = {
8
8
  kQueue: Symbol('queue'),
9
9
  kConnect: Symbol('connect'),
10
10
  kConnecting: Symbol('connecting'),
11
- kHeadersList: Symbol('headers list'),
12
11
  kKeepAliveDefaultTimeout: Symbol('default keep alive timeout'),
13
12
  kKeepAliveMaxTimeout: Symbol('max keep alive timeout'),
14
13
  kKeepAliveTimeoutThreshold: Symbol('keep alive timeout threshold'),
@@ -1101,7 +1101,7 @@ function writeStream ({ abort, body, client, request, socket, contentLength, hea
1101
1101
  }
1102
1102
  }
1103
1103
 
1104
- async function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
1104
+ function writeBuffer ({ abort, body, client, request, socket, contentLength, header, expectsPayload }) {
1105
1105
  try {
1106
1106
  if (!body) {
1107
1107
  if (contentLength === 0) {
@@ -0,0 +1,123 @@
1
+ 'use strict'
2
+
3
+ const util = require('../core/util')
4
+ const { InvalidArgumentError, RequestAbortedError } = require('../core/errors')
5
+ const DecoratorHandler = require('../handler/decorator-handler')
6
+
7
+ class DumpHandler extends DecoratorHandler {
8
+ #maxSize = 1024 * 1024
9
+ #abort = null
10
+ #dumped = false
11
+ #aborted = false
12
+ #size = 0
13
+ #reason = null
14
+ #handler = null
15
+
16
+ constructor ({ maxSize }, handler) {
17
+ super(handler)
18
+
19
+ if (maxSize != null && (!Number.isFinite(maxSize) || maxSize < 1)) {
20
+ throw new InvalidArgumentError('maxSize must be a number greater than 0')
21
+ }
22
+
23
+ this.#maxSize = maxSize ?? this.#maxSize
24
+ this.#handler = handler
25
+ }
26
+
27
+ onConnect (abort) {
28
+ this.#abort = abort
29
+
30
+ this.#handler.onConnect(this.#customAbort.bind(this))
31
+ }
32
+
33
+ #customAbort (reason) {
34
+ this.#aborted = true
35
+ this.#reason = reason
36
+ }
37
+
38
+ // TODO: will require adjustment after new hooks are out
39
+ onHeaders (statusCode, rawHeaders, resume, statusMessage) {
40
+ const headers = util.parseHeaders(rawHeaders)
41
+ const contentLength = headers['content-length']
42
+
43
+ if (contentLength != null && contentLength > this.#maxSize) {
44
+ throw new RequestAbortedError(
45
+ `Response size (${contentLength}) larger than maxSize (${
46
+ this.#maxSize
47
+ })`
48
+ )
49
+ }
50
+
51
+ if (this.#aborted) {
52
+ return true
53
+ }
54
+
55
+ return this.#handler.onHeaders(
56
+ statusCode,
57
+ rawHeaders,
58
+ resume,
59
+ statusMessage
60
+ )
61
+ }
62
+
63
+ onError (err) {
64
+ if (this.#dumped) {
65
+ return
66
+ }
67
+
68
+ err = this.#reason ?? err
69
+
70
+ this.#handler.onError(err)
71
+ }
72
+
73
+ onData (chunk) {
74
+ this.#size = this.#size + chunk.length
75
+
76
+ if (this.#size >= this.#maxSize) {
77
+ this.#dumped = true
78
+
79
+ if (this.#aborted) {
80
+ this.#handler.onError(this.#reason)
81
+ } else {
82
+ this.#handler.onComplete([])
83
+ }
84
+ }
85
+
86
+ return true
87
+ }
88
+
89
+ onComplete (trailers) {
90
+ if (this.#dumped) {
91
+ return
92
+ }
93
+
94
+ if (this.#aborted) {
95
+ this.#handler.onError(this.reason)
96
+ return
97
+ }
98
+
99
+ this.#handler.onComplete(trailers)
100
+ }
101
+ }
102
+
103
+ function createDumpInterceptor (
104
+ { maxSize: defaultMaxSize } = {
105
+ maxSize: 1024 * 1024
106
+ }
107
+ ) {
108
+ return dispatch => {
109
+ return function Intercept (opts, handler) {
110
+ const { dumpMaxSize = defaultMaxSize } =
111
+ opts
112
+
113
+ const dumpHandler = new DumpHandler(
114
+ { maxSize: dumpMaxSize },
115
+ handler
116
+ )
117
+
118
+ return dispatch(opts, dumpHandler)
119
+ }
120
+ }
121
+ }
122
+
123
+ module.exports = createDumpInterceptor
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const assert = require('node:assert')
4
- const { kHeadersList } = require('../../core/symbols')
4
+ const { getHeadersList: internalGetHeadersList } = require('../fetch/headers')
5
5
 
6
6
  /**
7
7
  * @param {string} value
@@ -278,8 +278,10 @@ function stringify (cookie) {
278
278
  let kHeadersListNode
279
279
 
280
280
  function getHeadersList (headers) {
281
- if (headers[kHeadersList]) {
282
- return headers[kHeadersList]
281
+ try {
282
+ return internalGetHeadersList(headers)
283
+ } catch {
284
+ // fall-through
283
285
  }
284
286
 
285
287
  if (!kHeadersListNode) {
@@ -385,6 +385,15 @@ function bodyMixinMethods (instance) {
385
385
  'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
386
386
  )
387
387
  }, instance, false)
388
+ },
389
+
390
+ bytes () {
391
+ // The bytes() method steps are to return the result of running consume body
392
+ // with this and the following step given a byte sequence bytes: return the
393
+ // result of creating a Uint8Array from bytes in this’s relevant realm.
394
+ return consumeBody(this, (bytes) => {
395
+ return new Uint8Array(bytes.buffer, 0, bytes.byteLength)
396
+ }, instance, true)
388
397
  }
389
398
  }
390
399
 
@@ -2,8 +2,7 @@
2
2
 
3
3
  'use strict'
4
4
 
5
- const { kHeadersList, kConstruct } = require('../../core/symbols')
6
- const { kGuard } = require('./symbols')
5
+ const { kConstruct } = require('../../core/symbols')
7
6
  const { kEnumerableProperty } = require('../../core/util')
8
7
  const {
9
8
  iteratorMixin,
@@ -103,19 +102,18 @@ function appendHeader (headers, name, value) {
103
102
  // 3. If headers’s guard is "immutable", then throw a TypeError.
104
103
  // 4. Otherwise, if headers’s guard is "request" and name is a
105
104
  // forbidden header name, return.
105
+ // 5. Otherwise, if headers’s guard is "request-no-cors":
106
+ // TODO
106
107
  // Note: undici does not implement forbidden header names
107
- if (headers[kGuard] === 'immutable') {
108
+ if (getHeadersGuard(headers) === 'immutable') {
108
109
  throw new TypeError('immutable')
109
- } else if (headers[kGuard] === 'request-no-cors') {
110
- // 5. Otherwise, if headers’s guard is "request-no-cors":
111
- // TODO
112
110
  }
113
111
 
114
112
  // 6. Otherwise, if headers’s guard is "response" and name is a
115
113
  // forbidden response-header name, return.
116
114
 
117
115
  // 7. Append (name, value) to headers’s header list.
118
- return headers[kHeadersList].append(name, value, false)
116
+ return getHeadersList(headers).append(name, value, false)
119
117
 
120
118
  // 8. If headers’s guard is "request-no-cors", then remove
121
119
  // privileged no-CORS request headers from headers
@@ -357,16 +355,20 @@ class HeadersList {
357
355
 
358
356
  // https://fetch.spec.whatwg.org/#headers-class
359
357
  class Headers {
358
+ #guard
359
+ #headersList
360
+
360
361
  constructor (init = undefined) {
361
362
  if (init === kConstruct) {
362
363
  return
363
364
  }
364
- this[kHeadersList] = new HeadersList()
365
+
366
+ this.#headersList = new HeadersList()
365
367
 
366
368
  // The new Headers(init) constructor steps are:
367
369
 
368
370
  // 1. Set this’s guard to "none".
369
- this[kGuard] = 'none'
371
+ this.#guard = 'none'
370
372
 
371
373
  // 2. If init is given, then fill this with init.
372
374
  if (init !== undefined) {
@@ -416,22 +418,20 @@ class Headers {
416
418
  // 5. Otherwise, if this’s guard is "response" and name is
417
419
  // a forbidden response-header name, return.
418
420
  // Note: undici does not implement forbidden header names
419
- if (this[kGuard] === 'immutable') {
421
+ if (this.#guard === 'immutable') {
420
422
  throw new TypeError('immutable')
421
- } else if (this[kGuard] === 'request-no-cors') {
422
- // TODO
423
423
  }
424
424
 
425
425
  // 6. If this’s header list does not contain name, then
426
426
  // return.
427
- if (!this[kHeadersList].contains(name, false)) {
427
+ if (!this.#headersList.contains(name, false)) {
428
428
  return
429
429
  }
430
430
 
431
431
  // 7. Delete name from this’s header list.
432
432
  // 8. If this’s guard is "request-no-cors", then remove
433
433
  // privileged no-CORS request headers from this.
434
- this[kHeadersList].delete(name, false)
434
+ this.#headersList.delete(name, false)
435
435
  }
436
436
 
437
437
  // https://fetch.spec.whatwg.org/#dom-headers-get
@@ -454,7 +454,7 @@ class Headers {
454
454
 
455
455
  // 2. Return the result of getting name from this’s header
456
456
  // list.
457
- return this[kHeadersList].get(name, false)
457
+ return this.#headersList.get(name, false)
458
458
  }
459
459
 
460
460
  // https://fetch.spec.whatwg.org/#dom-headers-has
@@ -477,7 +477,7 @@ class Headers {
477
477
 
478
478
  // 2. Return true if this’s header list contains name;
479
479
  // otherwise false.
480
- return this[kHeadersList].contains(name, false)
480
+ return this.#headersList.contains(name, false)
481
481
  }
482
482
 
483
483
  // https://fetch.spec.whatwg.org/#dom-headers-set
@@ -518,16 +518,14 @@ class Headers {
518
518
  // 6. Otherwise, if this’s guard is "response" and name is a
519
519
  // forbidden response-header name, return.
520
520
  // Note: undici does not implement forbidden header names
521
- if (this[kGuard] === 'immutable') {
521
+ if (this.#guard === 'immutable') {
522
522
  throw new TypeError('immutable')
523
- } else if (this[kGuard] === 'request-no-cors') {
524
- // TODO
525
523
  }
526
524
 
527
525
  // 7. Set (name, value) in this’s header list.
528
526
  // 8. If this’s guard is "request-no-cors", then remove
529
527
  // privileged no-CORS request headers from this
530
- this[kHeadersList].set(name, value, false)
528
+ this.#headersList.set(name, value, false)
531
529
  }
532
530
 
533
531
  // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
@@ -538,7 +536,7 @@ class Headers {
538
536
  // 2. Return the values of all headers in this’s header list whose name is
539
537
  // a byte-case-insensitive match for `Set-Cookie`, in order.
540
538
 
541
- const list = this[kHeadersList].cookies
539
+ const list = this.#headersList.cookies
542
540
 
543
541
  if (list) {
544
542
  return [...list]
@@ -549,8 +547,8 @@ class Headers {
549
547
 
550
548
  // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
551
549
  get [kHeadersSortedMap] () {
552
- if (this[kHeadersList][kHeadersSortedMap]) {
553
- return this[kHeadersList][kHeadersSortedMap]
550
+ if (this.#headersList[kHeadersSortedMap]) {
551
+ return this.#headersList[kHeadersSortedMap]
554
552
  }
555
553
 
556
554
  // 1. Let headers be an empty list of headers with the key being the name
@@ -559,14 +557,14 @@ class Headers {
559
557
 
560
558
  // 2. Let names be the result of convert header names to a sorted-lowercase
561
559
  // set with all the names of the headers in list.
562
- const names = this[kHeadersList].toSortedArray()
560
+ const names = this.#headersList.toSortedArray()
563
561
 
564
- const cookies = this[kHeadersList].cookies
562
+ const cookies = this.#headersList.cookies
565
563
 
566
564
  // fast-path
567
565
  if (cookies === null || cookies.length === 1) {
568
566
  // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
569
- return (this[kHeadersList][kHeadersSortedMap] = names)
567
+ return (this.#headersList[kHeadersSortedMap] = names)
570
568
  }
571
569
 
572
570
  // 3. For each name of names:
@@ -596,16 +594,38 @@ class Headers {
596
594
  }
597
595
 
598
596
  // 4. Return headers.
599
- return (this[kHeadersList][kHeadersSortedMap] = headers)
597
+ return (this.#headersList[kHeadersSortedMap] = headers)
600
598
  }
601
599
 
602
600
  [util.inspect.custom] (depth, options) {
603
601
  options.depth ??= depth
604
602
 
605
- return `Headers ${util.formatWithOptions(options, this[kHeadersList].entries)}`
603
+ return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
604
+ }
605
+
606
+ static getHeadersGuard (o) {
607
+ return o.#guard
608
+ }
609
+
610
+ static setHeadersGuard (o, guard) {
611
+ o.#guard = guard
612
+ }
613
+
614
+ static getHeadersList (o) {
615
+ return o.#headersList
616
+ }
617
+
618
+ static setHeadersList (o, list) {
619
+ o.#headersList = list
606
620
  }
607
621
  }
608
622
 
623
+ const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
624
+ Reflect.deleteProperty(Headers, 'getHeadersGuard')
625
+ Reflect.deleteProperty(Headers, 'setHeadersGuard')
626
+ Reflect.deleteProperty(Headers, 'getHeadersList')
627
+ Reflect.deleteProperty(Headers, 'setHeadersList')
628
+
609
629
  Object.defineProperty(Headers.prototype, util.inspect.custom, {
610
630
  enumerable: false
611
631
  })
@@ -631,8 +651,12 @@ webidl.converters.HeadersInit = function (V, prefix, argument) {
631
651
 
632
652
  // A work-around to ensure we send the properly-cased Headers when V is a Headers object.
633
653
  // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
634
- if (!util.types.isProxy(V) && kHeadersList in V && iterator === Headers.prototype.entries) { // Headers object
635
- return V[kHeadersList].entriesList
654
+ if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
655
+ try {
656
+ return getHeadersList(V).entriesList
657
+ } catch {
658
+ // fall-through
659
+ }
636
660
  }
637
661
 
638
662
  if (typeof iterator === 'function') {
@@ -654,5 +678,9 @@ module.exports = {
654
678
  // for test.
655
679
  compareHeaderName,
656
680
  Headers,
657
- HeadersList
681
+ HeadersList,
682
+ getHeadersGuard,
683
+ setHeadersGuard,
684
+ setHeadersList,
685
+ getHeadersList
658
686
  }
@@ -3,7 +3,7 @@
3
3
  'use strict'
4
4
 
5
5
  const { extractBody, mixinBody, cloneBody } = require('./body')
6
- const { Headers, fill: fillHeaders, HeadersList } = require('./headers')
6
+ const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers')
7
7
  const { FinalizationRegistry } = require('./dispatcher-weakref')()
8
8
  const util = require('../../core/util')
9
9
  const nodeUtil = require('node:util')
@@ -25,10 +25,10 @@ const {
25
25
  requestDuplex
26
26
  } = require('./constants')
27
27
  const { kEnumerableProperty } = util
28
- const { kHeaders, kSignal, kState, kGuard, kDispatcher } = require('./symbols')
28
+ const { kHeaders, kSignal, kState, kDispatcher } = require('./symbols')
29
29
  const { webidl } = require('./webidl')
30
30
  const { URLSerializer } = require('./data-url')
31
- const { kHeadersList, kConstruct } = require('../../core/symbols')
31
+ const { kConstruct } = require('../../core/symbols')
32
32
  const assert = require('node:assert')
33
33
  const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require('node:events')
34
34
 
@@ -445,8 +445,8 @@ class Request {
445
445
  // Realm, whose header list is request’s header list and guard is
446
446
  // "request".
447
447
  this[kHeaders] = new Headers(kConstruct)
448
- this[kHeaders][kHeadersList] = request.headersList
449
- this[kHeaders][kGuard] = 'request'
448
+ setHeadersList(this[kHeaders], request.headersList)
449
+ setHeadersGuard(this[kHeaders], 'request')
450
450
 
451
451
  // 31. If this’s request’s mode is "no-cors", then:
452
452
  if (mode === 'no-cors') {
@@ -459,13 +459,13 @@ class Request {
459
459
  }
460
460
 
461
461
  // 2. Set this’s headers’s guard to "request-no-cors".
462
- this[kHeaders][kGuard] = 'request-no-cors'
462
+ setHeadersGuard(this[kHeaders], 'request-no-cors')
463
463
  }
464
464
 
465
465
  // 32. If init is not empty, then:
466
466
  if (initHasKey) {
467
467
  /** @type {HeadersList} */
468
- const headersList = this[kHeaders][kHeadersList]
468
+ const headersList = getHeadersList(this[kHeaders])
469
469
  // 1. Let headers be a copy of this’s headers and its associated header
470
470
  // list.
471
471
  // 2. If init["headers"] exists, then set headers to init["headers"].
@@ -519,7 +519,7 @@ class Request {
519
519
  // 3, If Content-Type is non-null and this’s headers’s header list does
520
520
  // not contain `Content-Type`, then append `Content-Type`/Content-Type to
521
521
  // this’s headers.
522
- if (contentType && !this[kHeaders][kHeadersList].contains('content-type', true)) {
522
+ if (contentType && !getHeadersList(this[kHeaders]).contains('content-type', true)) {
523
523
  this[kHeaders].append('content-type', contentType)
524
524
  }
525
525
  }
@@ -785,7 +785,7 @@ class Request {
785
785
  }
786
786
 
787
787
  // 4. Return clonedRequestObject.
788
- return fromInnerRequest(clonedRequest, ac.signal, this[kHeaders][kGuard])
788
+ return fromInnerRequest(clonedRequest, ac.signal, getHeadersGuard(this[kHeaders]))
789
789
  }
790
790
 
791
791
  [nodeUtil.inspect.custom] (depth, options) {
@@ -894,8 +894,8 @@ function fromInnerRequest (innerRequest, signal, guard) {
894
894
  request[kState] = innerRequest
895
895
  request[kSignal] = signal
896
896
  request[kHeaders] = new Headers(kConstruct)
897
- request[kHeaders][kHeadersList] = innerRequest.headersList
898
- request[kHeaders][kGuard] = guard
897
+ setHeadersList(request[kHeaders], innerRequest.headersList)
898
+ setHeadersGuard(request[kHeaders], guard)
899
899
  return request
900
900
  }
901
901
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { Headers, HeadersList, fill } = require('./headers')
3
+ const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
4
4
  const { extractBody, cloneBody, mixinBody } = require('./body')
5
5
  const util = require('../../core/util')
6
6
  const nodeUtil = require('node:util')
@@ -19,11 +19,11 @@ const {
19
19
  redirectStatusSet,
20
20
  nullBodyStatus
21
21
  } = require('./constants')
22
- const { kState, kHeaders, kGuard } = require('./symbols')
22
+ const { kState, kHeaders } = require('./symbols')
23
23
  const { webidl } = require('./webidl')
24
24
  const { FormData } = require('./formdata')
25
25
  const { URLSerializer } = require('./data-url')
26
- const { kHeadersList, kConstruct } = require('../../core/symbols')
26
+ const { kConstruct } = require('../../core/symbols')
27
27
  const assert = require('node:assert')
28
28
  const { types } = require('node:util')
29
29
  const { isDisturbed, isErrored } = require('node:stream')
@@ -141,8 +141,8 @@ class Response {
141
141
  // Realm, whose header list is this’s response’s header list and guard
142
142
  // is "response".
143
143
  this[kHeaders] = new Headers(kConstruct)
144
- this[kHeaders][kGuard] = 'response'
145
- this[kHeaders][kHeadersList] = this[kState].headersList
144
+ setHeadersGuard(this[kHeaders], 'response')
145
+ setHeadersList(this[kHeaders], this[kState].headersList)
146
146
 
147
147
  // 3. Let bodyWithType be null.
148
148
  let bodyWithType = null
@@ -255,7 +255,7 @@ class Response {
255
255
 
256
256
  // 3. Return the result of creating a Response object, given
257
257
  // clonedResponse, this’s headers’s guard, and this’s relevant Realm.
258
- return fromInnerResponse(clonedResponse, this[kHeaders][kGuard])
258
+ return fromInnerResponse(clonedResponse, getHeadersGuard(this[kHeaders]))
259
259
  }
260
260
 
261
261
  [nodeUtil.inspect.custom] (depth, options) {
@@ -522,8 +522,8 @@ function fromInnerResponse (innerResponse, guard) {
522
522
  const response = new Response(kConstruct)
523
523
  response[kState] = innerResponse
524
524
  response[kHeaders] = new Headers(kConstruct)
525
- response[kHeaders][kHeadersList] = innerResponse.headersList
526
- response[kHeaders][kGuard] = guard
525
+ setHeadersList(response[kHeaders], innerResponse.headersList)
526
+ setHeadersGuard(response[kHeaders], guard)
527
527
 
528
528
  if (hasFinalizationRegistry && innerResponse.body?.stream) {
529
529
  registry.register(response, innerResponse.body.stream)
@@ -5,6 +5,5 @@ module.exports = {
5
5
  kHeaders: Symbol('headers'),
6
6
  kSignal: Symbol('signal'),
7
7
  kState: Symbol('state'),
8
- kGuard: Symbol('guard'),
9
8
  kDispatcher: Symbol('dispatcher')
10
9
  }
@@ -13,9 +13,8 @@ const { channels } = require('../../core/diagnostics')
13
13
  const { CloseEvent } = require('./events')
14
14
  const { makeRequest } = require('../fetch/request')
15
15
  const { fetching } = require('../fetch/index')
16
- const { Headers } = require('../fetch/headers')
16
+ const { Headers, getHeadersList } = require('../fetch/headers')
17
17
  const { getDecodeSplit } = require('../fetch/util')
18
- const { kHeadersList } = require('../../core/symbols')
19
18
  const { WebsocketFrameSend } = require('./frame')
20
19
 
21
20
  /** @type {import('crypto')} */
@@ -59,7 +58,7 @@ function establishWebSocketConnection (url, protocols, client, ws, onEstablish,
59
58
 
60
59
  // Note: undici extension, allow setting custom headers.
61
60
  if (options.headers) {
62
- const headersList = new Headers(options.headers)[kHeadersList]
61
+ const headersList = getHeadersList(new Headers(options.headers))
63
62
 
64
63
  request.headersList = headersList
65
64
  }
@@ -261,13 +260,9 @@ function closeWebSocketConnection (ws, code, reason, reasonByteLength) {
261
260
  /** @type {import('stream').Duplex} */
262
261
  const socket = ws[kResponse].socket
263
262
 
264
- socket.write(frame.createFrame(opcodes.CLOSE), (err) => {
265
- if (!err) {
266
- ws[kSentClose] = sentCloseFrameState.SENT
267
- }
268
- })
263
+ socket.write(frame.createFrame(opcodes.CLOSE))
269
264
 
270
- ws[kSentClose] = sentCloseFrameState.PROCESSING
265
+ ws[kSentClose] = sentCloseFrameState.SENT
271
266
 
272
267
  // Upon either sending or receiving a Close control frame, it is said
273
268
  // that _The WebSocket Closing Handshake is Started_ and that the
@@ -1,12 +1,22 @@
1
1
  'use strict'
2
2
 
3
3
  const { Writable } = require('node:stream')
4
+ const assert = require('node:assert')
4
5
  const { parserStates, opcodes, states, emptyBuffer, sentCloseFrameState } = require('./constants')
5
6
  const { kReadyState, kSentClose, kResponse, kReceivedClose } = require('./symbols')
6
7
  const { channels } = require('../../core/diagnostics')
7
- const { isValidStatusCode, failWebsocketConnection, websocketMessageReceived, utf8Decode } = require('./util')
8
+ const {
9
+ isValidStatusCode,
10
+ isValidOpcode,
11
+ failWebsocketConnection,
12
+ websocketMessageReceived,
13
+ utf8Decode,
14
+ isControlFrame,
15
+ isTextBinaryFrame,
16
+ isContinuationFrame
17
+ } = require('./util')
8
18
  const { WebsocketFrameSend } = require('./frame')
9
- const { CloseEvent } = require('./events')
19
+ const { closeWebSocketConnection } = require('./connection')
10
20
 
11
21
  // This code was influenced by ws released under the MIT license.
12
22
  // Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
@@ -16,6 +26,7 @@ const { CloseEvent } = require('./events')
16
26
  class ByteParser extends Writable {
17
27
  #buffers = []
18
28
  #byteOffset = 0
29
+ #loop = false
19
30
 
20
31
  #state = parserStates.INFO
21
32
 
@@ -35,6 +46,7 @@ class ByteParser extends Writable {
35
46
  _write (chunk, _, callback) {
36
47
  this.#buffers.push(chunk)
37
48
  this.#byteOffset += chunk.length
49
+ this.#loop = true
38
50
 
39
51
  this.run(callback)
40
52
  }
@@ -45,7 +57,7 @@ class ByteParser extends Writable {
45
57
  * or not enough bytes are buffered to parse.
46
58
  */
47
59
  run (callback) {
48
- while (true) {
60
+ while (this.#loop) {
49
61
  if (this.#state === parserStates.INFO) {
50
62
  // If there aren't enough bytes to parse the payload length, etc.
51
63
  if (this.#byteOffset < 2) {
@@ -53,148 +65,85 @@ class ByteParser extends Writable {
53
65
  }
54
66
 
55
67
  const buffer = this.consume(2)
68
+ const fin = (buffer[0] & 0x80) !== 0
69
+ const opcode = buffer[0] & 0x0F
70
+ const masked = (buffer[1] & 0x80) === 0x80
56
71
 
57
- this.#info.fin = (buffer[0] & 0x80) !== 0
58
- this.#info.opcode = buffer[0] & 0x0F
59
- this.#info.masked = (buffer[1] & 0x80) === 0x80
72
+ const fragmented = !fin && opcode !== opcodes.CONTINUATION
73
+ const payloadLength = buffer[1] & 0x7F
60
74
 
61
- if (this.#info.masked) {
62
- failWebsocketConnection(this.ws, 'Frame cannot be masked')
75
+ const rsv1 = buffer[0] & 0x40
76
+ const rsv2 = buffer[0] & 0x20
77
+ const rsv3 = buffer[0] & 0x10
78
+
79
+ if (!isValidOpcode(opcode)) {
80
+ failWebsocketConnection(this.ws, 'Invalid opcode received')
63
81
  return callback()
64
82
  }
65
83
 
66
- // If we receive a fragmented message, we use the type of the first
67
- // frame to parse the full message as binary/text, when it's terminated
68
- this.#info.originalOpcode ??= this.#info.opcode
84
+ if (masked) {
85
+ failWebsocketConnection(this.ws, 'Frame cannot be masked')
86
+ return callback()
87
+ }
69
88
 
70
- this.#info.fragmented = !this.#info.fin && this.#info.opcode !== opcodes.CONTINUATION
89
+ // MUST be 0 unless an extension is negotiated that defines meanings
90
+ // for non-zero values. If a nonzero value is received and none of
91
+ // the negotiated extensions defines the meaning of such a nonzero
92
+ // value, the receiving endpoint MUST _Fail the WebSocket
93
+ // Connection_.
94
+ if (rsv1 !== 0 || rsv2 !== 0 || rsv3 !== 0) {
95
+ failWebsocketConnection(this.ws, 'RSV1, RSV2, RSV3 must be clear')
96
+ return
97
+ }
71
98
 
72
- if (this.#info.fragmented && this.#info.opcode !== opcodes.BINARY && this.#info.opcode !== opcodes.TEXT) {
99
+ if (fragmented && !isTextBinaryFrame(opcode)) {
73
100
  // Only text and binary frames can be fragmented
74
101
  failWebsocketConnection(this.ws, 'Invalid frame type was fragmented.')
75
102
  return
76
103
  }
77
104
 
78
- const payloadLength = buffer[1] & 0x7F
79
-
80
- if (payloadLength <= 125) {
81
- this.#info.payloadLength = payloadLength
82
- this.#state = parserStates.READ_DATA
83
- } else if (payloadLength === 126) {
84
- this.#state = parserStates.PAYLOADLENGTH_16
85
- } else if (payloadLength === 127) {
86
- this.#state = parserStates.PAYLOADLENGTH_64
105
+ // If we are already parsing a text/binary frame and do not receive either
106
+ // a continuation frame or close frame, fail the connection.
107
+ if (isTextBinaryFrame(opcode) && this.#fragments.length > 0) {
108
+ failWebsocketConnection(this.ws, 'Expected continuation frame')
109
+ return
87
110
  }
88
111
 
89
- if (this.#info.fragmented && payloadLength > 125) {
112
+ if (this.#info.fragmented && fragmented) {
90
113
  // A fragmented frame can't be fragmented itself
91
114
  failWebsocketConnection(this.ws, 'Fragmented frame exceeded 125 bytes.')
92
115
  return
93
- } else if (
94
- (this.#info.opcode === opcodes.PING ||
95
- this.#info.opcode === opcodes.PONG ||
96
- this.#info.opcode === opcodes.CLOSE) &&
97
- payloadLength > 125
98
- ) {
99
- // Control frames can have a payload length of 125 bytes MAX
100
- failWebsocketConnection(this.ws, 'Payload length for control frame exceeded 125 bytes.')
101
- return
102
- } else if (this.#info.opcode === opcodes.CLOSE) {
103
- if (payloadLength === 1) {
104
- failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
105
- return
106
- }
107
-
108
- const body = this.consume(payloadLength)
109
-
110
- this.#info.closeInfo = this.parseCloseBody(body)
111
-
112
- if (this.#info.closeInfo.error) {
113
- const { code, reason } = this.#info.closeInfo
114
-
115
- callback(new CloseEvent('close', { wasClean: false, reason, code }))
116
- return
117
- }
118
-
119
- if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
120
- // If an endpoint receives a Close frame and did not previously send a
121
- // Close frame, the endpoint MUST send a Close frame in response. (When
122
- // sending a Close frame in response, the endpoint typically echos the
123
- // status code it received.)
124
- let body = emptyBuffer
125
- if (this.#info.closeInfo.code) {
126
- body = Buffer.allocUnsafe(2)
127
- body.writeUInt16BE(this.#info.closeInfo.code, 0)
128
- }
129
- const closeFrame = new WebsocketFrameSend(body)
130
-
131
- this.ws[kResponse].socket.write(
132
- closeFrame.createFrame(opcodes.CLOSE),
133
- (err) => {
134
- if (!err) {
135
- this.ws[kSentClose] = sentCloseFrameState.SENT
136
- }
137
- }
138
- )
139
- }
140
-
141
- // Upon either sending or receiving a Close control frame, it is said
142
- // that _The WebSocket Closing Handshake is Started_ and that the
143
- // WebSocket connection is in the CLOSING state.
144
- this.ws[kReadyState] = states.CLOSING
145
- this.ws[kReceivedClose] = true
146
-
147
- this.end()
116
+ }
148
117
 
118
+ // "All control frames MUST have a payload length of 125 bytes or less
119
+ // and MUST NOT be fragmented."
120
+ if ((payloadLength > 125 || fragmented) && isControlFrame(opcode)) {
121
+ failWebsocketConnection(this.ws, 'Control frame either too large or fragmented')
149
122
  return
150
- } else if (this.#info.opcode === opcodes.PING) {
151
- // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
152
- // response, unless it already received a Close frame.
153
- // A Pong frame sent in response to a Ping frame must have identical
154
- // "Application data"
155
-
156
- const body = this.consume(payloadLength)
157
-
158
- if (!this.ws[kReceivedClose]) {
159
- const frame = new WebsocketFrameSend(body)
160
-
161
- this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
162
-
163
- if (channels.ping.hasSubscribers) {
164
- channels.ping.publish({
165
- payload: body
166
- })
167
- }
168
- }
169
-
170
- this.#state = parserStates.INFO
171
-
172
- if (this.#byteOffset > 0) {
173
- continue
174
- } else {
175
- callback()
176
- return
177
- }
178
- } else if (this.#info.opcode === opcodes.PONG) {
179
- // A Pong frame MAY be sent unsolicited. This serves as a
180
- // unidirectional heartbeat. A response to an unsolicited Pong frame is
181
- // not expected.
123
+ }
182
124
 
183
- const body = this.consume(payloadLength)
125
+ if (isContinuationFrame(opcode) && this.#fragments.length === 0) {
126
+ failWebsocketConnection(this.ws, 'Unexpected continuation frame')
127
+ return
128
+ }
184
129
 
185
- if (channels.pong.hasSubscribers) {
186
- channels.pong.publish({
187
- payload: body
188
- })
189
- }
130
+ if (payloadLength <= 125) {
131
+ this.#info.payloadLength = payloadLength
132
+ this.#state = parserStates.READ_DATA
133
+ } else if (payloadLength === 126) {
134
+ this.#state = parserStates.PAYLOADLENGTH_16
135
+ } else if (payloadLength === 127) {
136
+ this.#state = parserStates.PAYLOADLENGTH_64
137
+ }
190
138
 
191
- if (this.#byteOffset > 0) {
192
- continue
193
- } else {
194
- callback()
195
- return
196
- }
139
+ if (isTextBinaryFrame(opcode)) {
140
+ this.#info.binaryType = opcode
197
141
  }
142
+
143
+ this.#info.opcode = opcode
144
+ this.#info.masked = masked
145
+ this.#info.fin = fin
146
+ this.#info.fragmented = fragmented
198
147
  } else if (this.#state === parserStates.PAYLOADLENGTH_16) {
199
148
  if (this.#byteOffset < 2) {
200
149
  return callback()
@@ -229,33 +178,28 @@ class ByteParser extends Writable {
229
178
  this.#state = parserStates.READ_DATA
230
179
  } else if (this.#state === parserStates.READ_DATA) {
231
180
  if (this.#byteOffset < this.#info.payloadLength) {
232
- // If there is still more data in this chunk that needs to be read
233
181
  return callback()
234
- } else if (this.#byteOffset >= this.#info.payloadLength) {
235
- // If the server sent multiple frames in a single chunk
182
+ }
236
183
 
237
- const body = this.consume(this.#info.payloadLength)
184
+ const body = this.consume(this.#info.payloadLength)
238
185
 
186
+ if (isControlFrame(this.#info.opcode)) {
187
+ this.#loop = this.parseControlFrame(body)
188
+ } else {
239
189
  this.#fragments.push(body)
240
190
 
241
- // If the frame is unfragmented, or a fragmented frame was terminated,
242
- // a message was received
243
- if (!this.#info.fragmented || (this.#info.fin && this.#info.opcode === opcodes.CONTINUATION)) {
191
+ // If the frame is not fragmented, a message has been received.
192
+ // If the frame is fragmented, it will terminate with a fin bit set
193
+ // and an opcode of 0 (continuation), therefore we handle that when
194
+ // parsing continuation frames, not here.
195
+ if (!this.#info.fragmented && this.#info.fin) {
244
196
  const fullMessage = Buffer.concat(this.#fragments)
245
-
246
- websocketMessageReceived(this.ws, this.#info.originalOpcode, fullMessage)
247
-
248
- this.#info = {}
197
+ websocketMessageReceived(this.ws, this.#info.binaryType, fullMessage)
249
198
  this.#fragments.length = 0
250
199
  }
251
-
252
- this.#state = parserStates.INFO
253
200
  }
254
- }
255
201
 
256
- if (this.#byteOffset === 0 && this.#info.payloadLength !== 0) {
257
- callback()
258
- break
202
+ this.#state = parserStates.INFO
259
203
  }
260
204
  }
261
205
  }
@@ -263,11 +207,11 @@ class ByteParser extends Writable {
263
207
  /**
264
208
  * Take n bytes from the buffered Buffers
265
209
  * @param {number} n
266
- * @returns {Buffer|null}
210
+ * @returns {Buffer}
267
211
  */
268
212
  consume (n) {
269
213
  if (n > this.#byteOffset) {
270
- return null
214
+ throw new Error('Called consume() before buffers satiated.')
271
215
  } else if (n === 0) {
272
216
  return emptyBuffer
273
217
  }
@@ -303,6 +247,8 @@ class ByteParser extends Writable {
303
247
  }
304
248
 
305
249
  parseCloseBody (data) {
250
+ assert(data.length !== 1)
251
+
306
252
  // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
307
253
  /** @type {number|undefined} */
308
254
  let code
@@ -336,6 +282,91 @@ class ByteParser extends Writable {
336
282
  return { code, reason, error: false }
337
283
  }
338
284
 
285
+ /**
286
+ * Parses control frames.
287
+ * @param {Buffer} body
288
+ */
289
+ parseControlFrame (body) {
290
+ const { opcode, payloadLength } = this.#info
291
+
292
+ if (opcode === opcodes.CLOSE) {
293
+ if (payloadLength === 1) {
294
+ failWebsocketConnection(this.ws, 'Received close frame with a 1-byte body.')
295
+ return false
296
+ }
297
+
298
+ this.#info.closeInfo = this.parseCloseBody(body)
299
+
300
+ if (this.#info.closeInfo.error) {
301
+ const { code, reason } = this.#info.closeInfo
302
+
303
+ closeWebSocketConnection(this.ws, code, reason, reason.length)
304
+ failWebsocketConnection(this.ws, reason)
305
+ return false
306
+ }
307
+
308
+ if (this.ws[kSentClose] !== sentCloseFrameState.SENT) {
309
+ // If an endpoint receives a Close frame and did not previously send a
310
+ // Close frame, the endpoint MUST send a Close frame in response. (When
311
+ // sending a Close frame in response, the endpoint typically echos the
312
+ // status code it received.)
313
+ let body = emptyBuffer
314
+ if (this.#info.closeInfo.code) {
315
+ body = Buffer.allocUnsafe(2)
316
+ body.writeUInt16BE(this.#info.closeInfo.code, 0)
317
+ }
318
+ const closeFrame = new WebsocketFrameSend(body)
319
+
320
+ this.ws[kResponse].socket.write(
321
+ closeFrame.createFrame(opcodes.CLOSE),
322
+ (err) => {
323
+ if (!err) {
324
+ this.ws[kSentClose] = sentCloseFrameState.SENT
325
+ }
326
+ }
327
+ )
328
+ }
329
+
330
+ // Upon either sending or receiving a Close control frame, it is said
331
+ // that _The WebSocket Closing Handshake is Started_ and that the
332
+ // WebSocket connection is in the CLOSING state.
333
+ this.ws[kReadyState] = states.CLOSING
334
+ this.ws[kReceivedClose] = true
335
+
336
+ this.end()
337
+ return false
338
+ } else if (opcode === opcodes.PING) {
339
+ // Upon receipt of a Ping frame, an endpoint MUST send a Pong frame in
340
+ // response, unless it already received a Close frame.
341
+ // A Pong frame sent in response to a Ping frame must have identical
342
+ // "Application data"
343
+
344
+ if (!this.ws[kReceivedClose]) {
345
+ const frame = new WebsocketFrameSend(body)
346
+
347
+ this.ws[kResponse].socket.write(frame.createFrame(opcodes.PONG))
348
+
349
+ if (channels.ping.hasSubscribers) {
350
+ channels.ping.publish({
351
+ payload: body
352
+ })
353
+ }
354
+ }
355
+ } else if (opcode === opcodes.PONG) {
356
+ // A Pong frame MAY be sent unsolicited. This serves as a
357
+ // unidirectional heartbeat. A response to an unsolicited Pong frame is
358
+ // not expected.
359
+
360
+ if (channels.pong.hasSubscribers) {
361
+ channels.pong.publish({
362
+ payload: body
363
+ })
364
+ }
365
+ }
366
+
367
+ return true
368
+ }
369
+
339
370
  get closingInfo () {
340
371
  return this.#info.closeInfo
341
372
  }
@@ -210,6 +210,30 @@ function failWebsocketConnection (ws, reason) {
210
210
  }
211
211
  }
212
212
 
213
+ /**
214
+ * @see https://datatracker.ietf.org/doc/html/rfc6455#section-5.5
215
+ * @param {number} opcode
216
+ */
217
+ function isControlFrame (opcode) {
218
+ return (
219
+ opcode === opcodes.CLOSE ||
220
+ opcode === opcodes.PING ||
221
+ opcode === opcodes.PONG
222
+ )
223
+ }
224
+
225
+ function isContinuationFrame (opcode) {
226
+ return opcode === opcodes.CONTINUATION
227
+ }
228
+
229
+ function isTextBinaryFrame (opcode) {
230
+ return opcode === opcodes.TEXT || opcode === opcodes.BINARY
231
+ }
232
+
233
+ function isValidOpcode (opcode) {
234
+ return isTextBinaryFrame(opcode) || isContinuationFrame(opcode) || isControlFrame(opcode)
235
+ }
236
+
213
237
  // https://nodejs.org/api/intl.html#detecting-internationalization-support
214
238
  const hasIntl = typeof process.versions.icu === 'string'
215
239
  const fatalDecoder = hasIntl ? new TextDecoder('utf-8', { fatal: true }) : undefined
@@ -237,5 +261,9 @@ module.exports = {
237
261
  isValidStatusCode,
238
262
  failWebsocketConnection,
239
263
  websocketMessageReceived,
240
- utf8Decode
264
+ utf8Decode,
265
+ isControlFrame,
266
+ isContinuationFrame,
267
+ isTextBinaryFrame,
268
+ isValidOpcode
241
269
  }
@@ -26,7 +26,7 @@ const { ByteParser } = require('./receiver')
26
26
  const { kEnumerableProperty, isBlobLike } = require('../../core/util')
27
27
  const { getGlobalDispatcher } = require('../../global')
28
28
  const { types } = require('node:util')
29
- const { ErrorEvent } = require('./events')
29
+ const { ErrorEvent, CloseEvent } = require('./events')
30
30
 
31
31
  let experimentalWarned = false
32
32
 
@@ -287,7 +287,7 @@ class WebSocket extends EventTarget {
287
287
  // not throw an exception must increase the bufferedAmount attribute
288
288
  // by the length of data’s buffer in bytes.
289
289
 
290
- const ab = new FastBuffer(data, data.byteOffset, data.byteLength)
290
+ const ab = new FastBuffer(data.buffer, data.byteOffset, data.byteLength)
291
291
 
292
292
  const frame = new WebsocketFrameSend(ab)
293
293
  const buffer = frame.createFrame(opcodes.BINARY)
@@ -594,9 +594,19 @@ function onParserDrain () {
594
594
  }
595
595
 
596
596
  function onParserError (err) {
597
- fireEvent('error', this, () => new ErrorEvent('error', { error: err, message: err.reason }))
597
+ let message
598
+ let code
599
+
600
+ if (err instanceof CloseEvent) {
601
+ message = err.reason
602
+ code = err.code
603
+ } else {
604
+ message = err.message
605
+ }
606
+
607
+ fireEvent('error', this, () => new ErrorEvent('error', { error: err, message }))
598
608
 
599
- closeWebSocketConnection(this, err.code)
609
+ closeWebSocketConnection(this, code)
600
610
  }
601
611
 
602
612
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "undici",
3
- "version": "6.16.1",
3
+ "version": "6.17.0",
4
4
  "description": "An HTTP/1.1 client, written from scratch for Node.js",
5
5
  "homepage": "https://undici.nodejs.org",
6
6
  "bugs": {
@@ -88,6 +88,8 @@
88
88
  "test:typescript": "tsd && tsc --skipLibCheck test/imports/undici-import.ts",
89
89
  "test:webidl": "borp -p \"test/webidl/*.js\"",
90
90
  "test:websocket": "borp -p \"test/websocket/*.js\"",
91
+ "test:websocket:autobahn": "node test/autobahn/client.js",
92
+ "test:websocket:autobahn:report": "node test/autobahn/report.js",
91
93
  "test:wpt": "node test/wpt/start-fetch.mjs && node test/wpt/start-FileAPI.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-websockets.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
92
94
  "test:wpt:withoutintl": "node test/wpt/start-fetch.mjs && node test/wpt/start-mimesniff.mjs && node test/wpt/start-xhr.mjs && node test/wpt/start-cacheStorage.mjs && node test/wpt/start-eventsource.mjs",
93
95
  "coverage": "npm run coverage:clean && cross-env NODE_V8_COVERAGE=./coverage/tmp npm run test:javascript && npm run coverage:report",
@@ -135,7 +137,7 @@
135
137
  "ignore": [
136
138
  "lib/llhttp/constants.js",
137
139
  "lib/llhttp/utils.js",
138
- "test/wpt/tests"
140
+ "test/fixtures/wpt"
139
141
  ]
140
142
  },
141
143
  "tsd": {
@@ -19,8 +19,8 @@ declare class Dispatcher extends EventEmitter {
19
19
  connect(options: Dispatcher.ConnectOptions): Promise<Dispatcher.ConnectData>;
20
20
  connect(options: Dispatcher.ConnectOptions, callback: (err: Error | null, data: Dispatcher.ConnectData) => void): void;
21
21
  /** Compose a chain of dispatchers */
22
- compose(dispatchers: Dispatcher.DispatcherInterceptor[]): Dispatcher.ComposedDispatcher;
23
- compose(...dispatchers: Dispatcher.DispatcherInterceptor[]): Dispatcher.ComposedDispatcher;
22
+ compose(dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher;
23
+ compose(...dispatchers: Dispatcher.DispatcherComposeInterceptor[]): Dispatcher.ComposedDispatcher;
24
24
  /** Performs an HTTP request. */
25
25
  request(options: Dispatcher.RequestOptions): Promise<Dispatcher.ResponseData>;
26
26
  request(options: Dispatcher.RequestOptions, callback: (err: Error | null, data: Dispatcher.ResponseData) => void): void;
@@ -97,7 +97,7 @@ declare class Dispatcher extends EventEmitter {
97
97
 
98
98
  declare namespace Dispatcher {
99
99
  export interface ComposedDispatcher extends Dispatcher {}
100
- export type DispatcherInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch'];
100
+ export type DispatcherComposeInterceptor = (dispatch: Dispatcher['dispatch']) => Dispatcher['dispatch'];
101
101
  export interface DispatchOptions {
102
102
  origin?: string | URL;
103
103
  path: string;
package/types/index.d.ts CHANGED
@@ -66,4 +66,9 @@ declare namespace Undici {
66
66
  var File: typeof import('./file').File;
67
67
  var FileReader: typeof import('./filereader').FileReader;
68
68
  var caches: typeof import('./cache').caches;
69
+ var interceptors: {
70
+ dump: typeof import('./interceptors').dump;
71
+ retry: typeof import('./interceptors').retry;
72
+ redirect: typeof import('./interceptors').redirect;
73
+ }
69
74
  }
@@ -1,5 +1,11 @@
1
1
  import Dispatcher from "./dispatcher";
2
+ import RetryHandler from "./retry-handler";
2
3
 
3
- type RedirectInterceptorOpts = { maxRedirections?: number }
4
+ export type DumpInterceptorOpts = { maxSize?: number }
5
+ export type RetryInterceptorOpts = RetryHandler.RetryOptions
6
+ export type RedirectInterceptorOpts = { maxRedirections?: number }
4
7
 
5
- export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.DispatchInterceptor
8
+ export declare function createRedirectInterceptor (opts: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
9
+ export declare function dump(opts?: DumpInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
10
+ export declare function retry(opts?: RetryInterceptorOpts): Dispatcher.DispatcherComposeInterceptor
11
+ export declare function redirect(opts?: RedirectInterceptorOpts): Dispatcher.DispatcherComposeInterceptor