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
@@ -281,7 +281,7 @@ function writeH2 (client, request) {
281
281
  }
282
282
 
283
283
  // We do not destroy the socket as we can continue using the session
284
- // the stream get's destroyed and the session remains to create new streams
284
+ // the stream gets destroyed and the session remains to create new streams
285
285
  util.destroy(body, err)
286
286
  }
287
287
 
@@ -1,5 +1,3 @@
1
- // @ts-check
2
-
3
1
  'use strict'
4
2
 
5
3
  const assert = require('node:assert')
@@ -60,6 +58,15 @@ const connectH2 = require('./client-h2.js')
60
58
 
61
59
  const kClosedResolve = Symbol('kClosedResolve')
62
60
 
61
+ const getDefaultNodeMaxHeaderSize = http &&
62
+ http.maxHeaderSize &&
63
+ Number.isInteger(http.maxHeaderSize) &&
64
+ http.maxHeaderSize > 0
65
+ ? () => http.maxHeaderSize
66
+ : () => { throw new InvalidArgumentError('http module not available or http.maxHeaderSize invalid') }
67
+
68
+ const noop = () => {}
69
+
63
70
  function getPipelining (client) {
64
71
  return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1
65
72
  }
@@ -121,8 +128,14 @@ class Client extends DispatcherBase {
121
128
  throw new InvalidArgumentError('unsupported maxKeepAliveTimeout, use keepAliveMaxTimeout instead')
122
129
  }
123
130
 
124
- if (maxHeaderSize != null && !Number.isFinite(maxHeaderSize)) {
125
- throw new InvalidArgumentError('invalid maxHeaderSize')
131
+ if (maxHeaderSize != null) {
132
+ if (!Number.isInteger(maxHeaderSize) || maxHeaderSize < 1) {
133
+ throw new InvalidArgumentError('invalid maxHeaderSize')
134
+ }
135
+ } else {
136
+ // If maxHeaderSize is not provided, use the default value from the http module
137
+ // or if that is not available, throw an error.
138
+ maxHeaderSize = getDefaultNodeMaxHeaderSize()
126
139
  }
127
140
 
128
141
  if (socketPath != null && typeof socketPath !== 'string') {
@@ -202,7 +215,7 @@ class Client extends DispatcherBase {
202
215
  this[kUrl] = util.parseOrigin(url)
203
216
  this[kConnector] = connect
204
217
  this[kPipelining] = pipelining != null ? pipelining : 1
205
- this[kMaxHeadersSize] = maxHeaderSize || http.maxHeaderSize
218
+ this[kMaxHeadersSize] = maxHeaderSize
206
219
  this[kKeepAliveDefaultTimeout] = keepAliveTimeout == null ? 4e3 : keepAliveTimeout
207
220
  this[kKeepAliveMaxTimeout] = keepAliveMaxTimeout == null ? 600e3 : keepAliveMaxTimeout
208
221
  this[kKeepAliveTimeoutThreshold] = keepAliveTimeoutThreshold == null ? 2e3 : keepAliveTimeoutThreshold
@@ -361,6 +374,10 @@ function onError (client, err) {
361
374
  }
362
375
  }
363
376
 
377
+ /**
378
+ * @param {Client} client
379
+ * @returns
380
+ */
364
381
  async function connect (client) {
365
382
  assert(!client[kConnecting])
366
383
  assert(!client[kHTTPContext])
@@ -414,7 +431,7 @@ async function connect (client) {
414
431
  })
415
432
 
416
433
  if (client.destroyed) {
417
- util.destroy(socket.on('error', () => {}), new ClientDestroyedError())
434
+ util.destroy(socket.on('error', noop), new ClientDestroyedError())
418
435
  return
419
436
  }
420
437
 
@@ -425,7 +442,7 @@ async function connect (client) {
425
442
  ? await connectH2(client, socket)
426
443
  : await connectH1(client, socket)
427
444
  } catch (err) {
428
- socket.destroy().on('error', () => {})
445
+ socket.destroy().on('error', noop)
429
446
  throw err
430
447
  }
431
448
 
@@ -1,12 +1,10 @@
1
- /* eslint-disable */
2
-
3
1
  'use strict'
4
2
 
5
3
  // Extracted from node/lib/internal/fixed_queue.js
6
4
 
7
5
  // Currently optimal queue size, tested on V8 6.0 - 6.6. Must be power of two.
8
- const kSize = 2048;
9
- const kMask = kSize - 1;
6
+ const kSize = 2048
7
+ const kMask = kSize - 1
10
8
 
11
9
  // The FixedQueue is implemented as a singly-linked list of fixed-size
12
10
  // circular buffers. It looks something like this:
@@ -17,18 +15,18 @@ const kMask = kSize - 1;
17
15
  // +-----------+ <-----\ +-----------+ <------\ +-----------+
18
16
  // | [null] | \----- | next | \------- | next |
19
17
  // +-----------+ +-----------+ +-----------+
20
- // | item | <-- bottom | item | <-- bottom | [empty] |
21
- // | item | | item | | [empty] |
22
- // | item | | item | | [empty] |
23
- // | item | | item | | [empty] |
18
+ // | item | <-- bottom | item | <-- bottom | undefined |
19
+ // | item | | item | | undefined |
20
+ // | item | | item | | undefined |
21
+ // | item | | item | | undefined |
24
22
  // | item | | item | bottom --> | item |
25
23
  // | item | | item | | item |
26
24
  // | ... | | ... | | ... |
27
25
  // | item | | item | | item |
28
26
  // | item | | item | | item |
29
- // | [empty] | <-- top | item | | item |
30
- // | [empty] | | item | | item |
31
- // | [empty] | | [empty] | <-- top top --> | [empty] |
27
+ // | undefined | <-- top | item | | item |
28
+ // | undefined | | item | | item |
29
+ // | undefined | | undefined | <-- top top --> | undefined |
32
30
  // +-----------+ +-----------+ +-----------+
33
31
  //
34
32
  // Or, if there is only one circular buffer, it looks something
@@ -40,12 +38,12 @@ const kMask = kSize - 1;
40
38
  // +-----------+ +-----------+
41
39
  // | [null] | | [null] |
42
40
  // +-----------+ +-----------+
43
- // | [empty] | | item |
44
- // | [empty] | | item |
45
- // | item | <-- bottom top --> | [empty] |
46
- // | item | | [empty] |
47
- // | [empty] | <-- top bottom --> | item |
48
- // | [empty] | | item |
41
+ // | undefined | | item |
42
+ // | undefined | | item |
43
+ // | item | <-- bottom top --> | undefined |
44
+ // | item | | undefined |
45
+ // | undefined | <-- top bottom --> | item |
46
+ // | undefined | | item |
49
47
  // +-----------+ +-----------+
50
48
  //
51
49
  // Adding a value means moving `top` forward by one, removing means
@@ -56,62 +54,106 @@ const kMask = kSize - 1;
56
54
  // `top + 1 === bottom` it's full. This wastes a single space of storage
57
55
  // but allows much quicker checks.
58
56
 
57
+ /**
58
+ * @type {FixedCircularBuffer}
59
+ * @template T
60
+ */
59
61
  class FixedCircularBuffer {
60
- constructor() {
61
- this.bottom = 0;
62
- this.top = 0;
63
- this.list = new Array(kSize);
64
- this.next = null;
62
+ constructor () {
63
+ /**
64
+ * @type {number}
65
+ */
66
+ this.bottom = 0
67
+ /**
68
+ * @type {number}
69
+ */
70
+ this.top = 0
71
+ /**
72
+ * @type {Array<T|undefined>}
73
+ */
74
+ this.list = new Array(kSize).fill(undefined)
75
+ /**
76
+ * @type {T|null}
77
+ */
78
+ this.next = null
65
79
  }
66
80
 
67
- isEmpty() {
68
- return this.top === this.bottom;
81
+ /**
82
+ * @returns {boolean}
83
+ */
84
+ isEmpty () {
85
+ return this.top === this.bottom
69
86
  }
70
87
 
71
- isFull() {
72
- return ((this.top + 1) & kMask) === this.bottom;
88
+ /**
89
+ * @returns {boolean}
90
+ */
91
+ isFull () {
92
+ return ((this.top + 1) & kMask) === this.bottom
73
93
  }
74
94
 
75
- push(data) {
76
- this.list[this.top] = data;
77
- this.top = (this.top + 1) & kMask;
95
+ /**
96
+ * @param {T} data
97
+ * @returns {void}
98
+ */
99
+ push (data) {
100
+ this.list[this.top] = data
101
+ this.top = (this.top + 1) & kMask
78
102
  }
79
103
 
80
- shift() {
81
- const nextItem = this.list[this.bottom];
82
- if (nextItem === undefined)
83
- return null;
84
- this.list[this.bottom] = undefined;
85
- this.bottom = (this.bottom + 1) & kMask;
86
- return nextItem;
104
+ /**
105
+ * @returns {T|null}
106
+ */
107
+ shift () {
108
+ const nextItem = this.list[this.bottom]
109
+ if (nextItem === undefined) { return null }
110
+ this.list[this.bottom] = undefined
111
+ this.bottom = (this.bottom + 1) & kMask
112
+ return nextItem
87
113
  }
88
114
  }
89
115
 
116
+ /**
117
+ * @template T
118
+ */
90
119
  module.exports = class FixedQueue {
91
- constructor() {
92
- this.head = this.tail = new FixedCircularBuffer();
120
+ constructor () {
121
+ /**
122
+ * @type {FixedCircularBuffer<T>}
123
+ */
124
+ this.head = this.tail = new FixedCircularBuffer()
93
125
  }
94
126
 
95
- isEmpty() {
96
- return this.head.isEmpty();
127
+ /**
128
+ * @returns {boolean}
129
+ */
130
+ isEmpty () {
131
+ return this.head.isEmpty()
97
132
  }
98
133
 
99
- push(data) {
134
+ /**
135
+ * @param {T} data
136
+ */
137
+ push (data) {
100
138
  if (this.head.isFull()) {
101
139
  // Head is full: Creates a new queue, sets the old queue's `.next` to it,
102
140
  // and sets it as the new main queue.
103
- this.head = this.head.next = new FixedCircularBuffer();
141
+ this.head = this.head.next = new FixedCircularBuffer()
104
142
  }
105
- this.head.push(data);
143
+ this.head.push(data)
106
144
  }
107
145
 
108
- shift() {
109
- const tail = this.tail;
110
- const next = tail.shift();
146
+ /**
147
+ * @returns {T|null}
148
+ */
149
+ shift () {
150
+ const tail = this.tail
151
+ const next = tail.shift()
111
152
  if (tail.isEmpty() && tail.next !== null) {
112
153
  // If there is another queue, it forms the new tail.
113
- this.tail = tail.next;
154
+ this.tail = tail.next
155
+ tail.next = null
114
156
  }
115
- return next;
157
+ return next
116
158
  }
117
- };
159
+ }
@@ -1,3 +1,5 @@
1
+ 'use strict'
2
+
1
3
  const { kFree, kConnected, kPending, kQueued, kRunning, kSize } = require('../core/symbols')
2
4
  const kPool = Symbol('pool')
3
5
 
@@ -23,6 +23,8 @@ function defaultFactory (origin, opts) {
23
23
  return new Pool(origin, opts)
24
24
  }
25
25
 
26
+ const noop = () => {}
27
+
26
28
  class ProxyAgent extends DispatcherBase {
27
29
  constructor (opts) {
28
30
  if (!opts || (typeof opts === 'object' && !(opts instanceof URL) && !opts.uri)) {
@@ -78,7 +80,7 @@ class ProxyAgent extends DispatcherBase {
78
80
  servername: this[kProxyTls]?.servername || proxyHostname
79
81
  })
80
82
  if (statusCode !== 200) {
81
- socket.on('error', () => {}).destroy()
83
+ socket.on('error', noop).destroy()
82
84
  callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`))
83
85
  }
84
86
  if (opts.protocol !== 'https:') {
@@ -38,7 +38,7 @@ class RedirectHandler {
38
38
  throw new InvalidArgumentError('maxRedirections must be a positive number')
39
39
  }
40
40
 
41
- util.validateHandler(handler, opts.method, opts.upgrade)
41
+ util.assertRequestHandler(handler, opts.method, opts.upgrade)
42
42
 
43
43
  this.dispatch = dispatch
44
44
  this.location = null
@@ -146,7 +146,7 @@ class RedirectHandler {
146
146
 
147
147
  TLDR: undici always ignores 3xx response bodies.
148
148
 
149
- Redirection is used to serve the requested resource from another URL, so it is assumes that
149
+ Redirection is used to serve the requested resource from another URL, so it assumes that
150
150
  no body is generated (and thus can be ignored). Even though generating a body is not prohibited.
151
151
 
152
152
  For status 301, 302, 303, 307 and 308 (the latter from RFC 7238), the specs mention that the body usually
@@ -194,8 +194,8 @@ class RetryHandler {
194
194
 
195
195
  // Only Partial Content 206 supposed to provide Content-Range,
196
196
  // any other status code that partially consumed the payload
197
- // should not be retry because it would result in downstream
198
- // wrongly concatanete multiple responses.
197
+ // should not be retried because it would result in downstream
198
+ // wrongly concatenate multiple responses.
199
199
  if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) {
200
200
  this.abort(
201
201
  new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, {
@@ -0,0 +1,346 @@
1
+ 'use strict'
2
+ const { isIP } = require('node:net')
3
+ const { lookup } = require('node:dns')
4
+ const DecoratorHandler = require('../handler/decorator-handler')
5
+ const { InvalidArgumentError, InformationalError } = require('../core/errors')
6
+ const maxInt = Math.pow(2, 31) - 1
7
+
8
+ class DNSInstance {
9
+ #maxTTL = 0
10
+ #maxItems = 0
11
+ #records = new Map()
12
+ dualStack = true
13
+ affinity = null
14
+ lookup = null
15
+ pick = null
16
+ lastIpFamily = null
17
+
18
+ constructor (opts) {
19
+ this.#maxTTL = opts.maxTTL
20
+ this.#maxItems = opts.maxItems
21
+ this.dualStack = opts.dualStack
22
+ this.affinity = opts.affinity
23
+ this.lookup = opts.lookup ?? this.#defaultLookup
24
+ this.pick = opts.pick ?? this.#defaultPick
25
+ }
26
+
27
+ get full () {
28
+ return this.#records.size === this.#maxItems
29
+ }
30
+
31
+ runLookup (origin, opts, cb) {
32
+ const ips = this.#records.get(origin.hostname)
33
+
34
+ // If full, we just return the origin
35
+ if (ips == null && this.full) {
36
+ cb(null, origin.origin)
37
+ return
38
+ }
39
+
40
+ const newOpts = {
41
+ affinity: this.affinity,
42
+ dualStack: this.dualStack,
43
+ lookup: this.lookup,
44
+ pick: this.pick,
45
+ ...opts.dns,
46
+ maxTTL: this.#maxTTL,
47
+ maxItems: this.#maxItems
48
+ }
49
+
50
+ // If no IPs we lookup
51
+ if (ips == null) {
52
+ this.lookup(origin, newOpts, (err, addresses) => {
53
+ if (err || addresses == null || addresses.length === 0) {
54
+ cb(err ?? new InformationalError('No DNS entries found'))
55
+ return
56
+ }
57
+
58
+ this.setRecords(origin, addresses)
59
+ const records = this.#records.get(origin.hostname)
60
+
61
+ const ip = this.pick(
62
+ origin,
63
+ records,
64
+ // Only set affinity if dual stack is disabled
65
+ // otherwise let it go through normal flow
66
+ !newOpts.dualStack && newOpts.affinity
67
+ )
68
+
69
+ cb(
70
+ null,
71
+ `${origin.protocol}//${
72
+ ip.family === 6 ? `[${ip.address}]` : ip.address
73
+ }${origin.port === '' ? '' : `:${origin.port}`}`
74
+ )
75
+ })
76
+ } else {
77
+ // If there's IPs we pick
78
+ const ip = this.pick(
79
+ origin,
80
+ ips,
81
+ // Only set affinity if dual stack is disabled
82
+ // otherwise let it go through normal flow
83
+ !newOpts.dualStack && newOpts.affinity
84
+ )
85
+
86
+ // If no IPs we lookup - deleting old records
87
+ if (ip == null) {
88
+ this.#records.delete(origin.hostname)
89
+ this.runLookup(origin, opts, cb)
90
+ return
91
+ }
92
+
93
+ cb(
94
+ null,
95
+ `${origin.protocol}//${
96
+ ip.family === 6 ? `[${ip.address}]` : ip.address
97
+ }${origin.port === '' ? '' : `:${origin.port}`}`
98
+ )
99
+ }
100
+ }
101
+
102
+ #defaultLookup (origin, opts, cb) {
103
+ lookup(
104
+ origin.hostname,
105
+ { all: true, family: this.dualStack === false ? this.affinity : 0 },
106
+ (err, addresses) => {
107
+ if (err) {
108
+ return cb(err)
109
+ }
110
+
111
+ const results = new Map()
112
+
113
+ for (const addr of addresses) {
114
+ const record = {
115
+ address: addr.address,
116
+ ttl: opts.maxTTL,
117
+ family: addr.family
118
+ }
119
+
120
+ // On linux we found duplicates, we attempt to remove them with
121
+ // the latest record
122
+ results.set(`${record.address}:${record.family}`, record)
123
+ }
124
+
125
+ cb(null, results.values())
126
+ }
127
+ )
128
+ }
129
+
130
+ #defaultPick (origin, hostnameRecords, affinity) {
131
+ let ip = null
132
+ const { records, offset = 0 } = hostnameRecords
133
+ let newOffset = 0
134
+
135
+ if (offset === maxInt) {
136
+ newOffset = 0
137
+ } else {
138
+ newOffset = offset + 1
139
+ }
140
+
141
+ // We balance between the two IP families
142
+ // If dual-stack disabled, we automatically pick the affinity
143
+ const newIpFamily = (newOffset & 1) === 1 ? 4 : 6
144
+ const family =
145
+ this.dualStack === false
146
+ ? records[this.affinity] // If dual-stack is disabled, we pick the default affiniy
147
+ : records[affinity] ?? records[newIpFamily]
148
+
149
+ // If no IPs and we have tried both families or dual stack is disabled, we return null
150
+ if (
151
+ (family == null || family.ips.length === 0) &&
152
+ // eslint-disable-next-line eqeqeq
153
+ (this.dualStack === false || this.lastIpFamily != newIpFamily)
154
+ ) {
155
+ return ip
156
+ }
157
+
158
+ family.offset = family.offset ?? 0
159
+ hostnameRecords.offset = newOffset
160
+
161
+ if (family.offset === maxInt) {
162
+ family.offset = 0
163
+ } else {
164
+ family.offset++
165
+ }
166
+
167
+ const position = family.offset % family.ips.length
168
+ ip = family.ips[position] ?? null
169
+
170
+ if (ip == null) {
171
+ return ip
172
+ }
173
+
174
+ const timestamp = Date.now()
175
+ // Record TTL is already in ms
176
+ if (ip.timestamp != null && timestamp - ip.timestamp > ip.ttl) {
177
+ // We delete expired records
178
+ // It is possible that they have different TTL, so we manage them individually
179
+ family.ips.splice(position, 1)
180
+ return this.pick(origin, hostnameRecords, affinity)
181
+ }
182
+
183
+ ip.timestamp = timestamp
184
+
185
+ this.lastIpFamily = newIpFamily
186
+ return ip
187
+ }
188
+
189
+ setRecords (origin, addresses) {
190
+ const records = { records: { 4: null, 6: null } }
191
+ for (const record of addresses) {
192
+ const familyRecords = records.records[record.family] ?? { ips: [] }
193
+
194
+ familyRecords.ips.push(record)
195
+ records.records[record.family] = familyRecords
196
+ }
197
+
198
+ this.#records.set(origin.hostname, records)
199
+ }
200
+
201
+ getHandler (meta, opts) {
202
+ return new DNSDispatchHandler(this, meta, opts)
203
+ }
204
+ }
205
+
206
+ class DNSDispatchHandler extends DecoratorHandler {
207
+ #state = null
208
+ #opts = null
209
+ #dispatch = null
210
+ #handler = null
211
+ #origin = null
212
+
213
+ constructor (state, { origin, handler, dispatch }, opts) {
214
+ super(handler)
215
+ this.#origin = origin
216
+ this.#handler = handler
217
+ this.#opts = { ...opts }
218
+ this.#state = state
219
+ this.#dispatch = dispatch
220
+ }
221
+
222
+ onError (err) {
223
+ switch (err.code) {
224
+ case 'ETIMEDOUT':
225
+ case 'ECONNREFUSED': {
226
+ if (this.#state.dualStack) {
227
+ // We delete the record and retry
228
+ this.#state.runLookup(this.#origin, this.#opts, (err, newOrigin) => {
229
+ if (err) {
230
+ return this.#handler.onError(err)
231
+ }
232
+
233
+ const dispatchOpts = {
234
+ ...this.#opts,
235
+ origin: newOrigin
236
+ }
237
+
238
+ this.#dispatch(dispatchOpts, this)
239
+ })
240
+
241
+ // if dual-stack disabled, we error out
242
+ return
243
+ }
244
+
245
+ this.#handler.onError(err)
246
+ return
247
+ }
248
+ case 'ENOTFOUND':
249
+ this.#state.deleteRecord(this.#origin)
250
+ // eslint-disable-next-line no-fallthrough
251
+ default:
252
+ this.#handler.onError(err)
253
+ break
254
+ }
255
+ }
256
+ }
257
+
258
+ module.exports = interceptorOpts => {
259
+ if (
260
+ interceptorOpts?.maxTTL != null &&
261
+ (typeof interceptorOpts?.maxTTL !== 'number' || interceptorOpts?.maxTTL < 0)
262
+ ) {
263
+ throw new InvalidArgumentError('Invalid maxTTL. Must be a positive number')
264
+ }
265
+
266
+ if (
267
+ interceptorOpts?.maxItems != null &&
268
+ (typeof interceptorOpts?.maxItems !== 'number' ||
269
+ interceptorOpts?.maxItems < 1)
270
+ ) {
271
+ throw new InvalidArgumentError(
272
+ 'Invalid maxItems. Must be a positive number and greater than zero'
273
+ )
274
+ }
275
+
276
+ if (
277
+ interceptorOpts?.affinity != null &&
278
+ interceptorOpts?.affinity !== 4 &&
279
+ interceptorOpts?.affinity !== 6
280
+ ) {
281
+ throw new InvalidArgumentError('Invalid affinity. Must be either 4 or 6')
282
+ }
283
+
284
+ if (
285
+ interceptorOpts?.dualStack != null &&
286
+ typeof interceptorOpts?.dualStack !== 'boolean'
287
+ ) {
288
+ throw new InvalidArgumentError('Invalid dualStack. Must be a boolean')
289
+ }
290
+
291
+ if (
292
+ interceptorOpts?.lookup != null &&
293
+ typeof interceptorOpts?.lookup !== 'function'
294
+ ) {
295
+ throw new InvalidArgumentError('Invalid lookup. Must be a function')
296
+ }
297
+
298
+ if (
299
+ interceptorOpts?.pick != null &&
300
+ typeof interceptorOpts?.pick !== 'function'
301
+ ) {
302
+ throw new InvalidArgumentError('Invalid pick. Must be a function')
303
+ }
304
+
305
+ const opts = {
306
+ maxTTL: interceptorOpts?.maxTTL ?? 10e3, // Expressed in ms
307
+ lookup: interceptorOpts?.lookup ?? null,
308
+ pick: interceptorOpts?.pick ?? null,
309
+ dualStack: interceptorOpts?.dualStack ?? true,
310
+ affinity: interceptorOpts?.affinity ?? 4,
311
+ maxItems: interceptorOpts?.maxItems ?? Infinity
312
+ }
313
+
314
+ const instance = new DNSInstance(opts)
315
+
316
+ return dispatch => {
317
+ return function dnsInterceptor (origDispatchOpts, handler) {
318
+ const origin =
319
+ origDispatchOpts.origin.constructor === URL
320
+ ? origDispatchOpts.origin
321
+ : new URL(origDispatchOpts.origin)
322
+
323
+ if (isIP(origin.hostname) !== 0) {
324
+ return dispatch(origDispatchOpts, handler)
325
+ }
326
+
327
+ instance.runLookup(origin, origDispatchOpts, (err, newOrigin) => {
328
+ if (err) {
329
+ return handler.onError(err)
330
+ }
331
+
332
+ const dispatchOpts = {
333
+ ...origDispatchOpts,
334
+ origin: newOrigin
335
+ }
336
+
337
+ dispatch(
338
+ dispatchOpts,
339
+ instance.getHandler({ origin, dispatch, handler }, origDispatchOpts)
340
+ )
341
+ })
342
+
343
+ return true
344
+ }
345
+ }
346
+ }