undici 7.10.0 → 7.12.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 +159 -0
  2. package/docs/docs/api/CacheStore.md +3 -3
  3. package/docs/docs/api/Debug.md +13 -13
  4. package/docs/docs/api/DiagnosticsChannel.md +32 -4
  5. package/docs/docs/api/Dispatcher.md +22 -3
  6. package/docs/docs/api/GlobalInstallation.md +91 -0
  7. package/docs/docs/api/MockClient.md +4 -0
  8. package/docs/docs/api/MockPool.md +6 -0
  9. package/docs/docs/api/ProxyAgent.md +2 -0
  10. package/docs/docs/api/RetryAgent.md +6 -1
  11. package/docs/docs/api/RetryHandler.md +1 -0
  12. package/docs/docs/api/WebSocket.md +27 -0
  13. package/index.js +18 -1
  14. package/lib/api/api-stream.js +1 -1
  15. package/lib/api/readable.js +1 -3
  16. package/lib/cache/memory-cache-store.js +3 -3
  17. package/lib/cache/sqlite-cache-store.js +1 -1
  18. package/lib/core/connect.js +21 -51
  19. package/lib/core/diagnostics.js +6 -4
  20. package/lib/core/request.js +12 -1
  21. package/lib/core/tree.js +1 -1
  22. package/lib/core/util.js +0 -45
  23. package/lib/dispatcher/client-h1.js +9 -18
  24. package/lib/dispatcher/proxy-agent.js +2 -1
  25. package/lib/handler/cache-handler.js +4 -1
  26. package/lib/handler/redirect-handler.js +2 -2
  27. package/lib/handler/retry-handler.js +110 -56
  28. package/lib/interceptor/cache.js +2 -2
  29. package/lib/interceptor/redirect.js +1 -1
  30. package/lib/mock/mock-client.js +4 -0
  31. package/lib/mock/mock-pool.js +4 -0
  32. package/lib/util/cache.js +12 -2
  33. package/lib/util/promise.js +28 -0
  34. package/lib/util/timers.js +11 -9
  35. package/lib/web/cache/cache.js +11 -9
  36. package/lib/web/cache/cachestorage.js +1 -1
  37. package/lib/web/cookies/index.js +1 -1
  38. package/lib/web/eventsource/eventsource.js +3 -6
  39. package/lib/web/eventsource/util.js +1 -1
  40. package/lib/web/fetch/body.js +36 -24
  41. package/lib/web/fetch/formdata-parser.js +4 -4
  42. package/lib/web/fetch/formdata.js +1 -1
  43. package/lib/web/fetch/headers.js +1 -1
  44. package/lib/web/fetch/index.js +228 -226
  45. package/lib/web/fetch/request.js +16 -8
  46. package/lib/web/fetch/response.js +6 -4
  47. package/lib/web/fetch/util.js +23 -25
  48. package/lib/web/{fetch/webidl.js → webidl/index.js} +57 -9
  49. package/lib/web/websocket/connection.js +4 -12
  50. package/lib/web/websocket/events.js +1 -1
  51. package/lib/web/websocket/frame.js +2 -1
  52. package/lib/web/websocket/receiver.js +2 -12
  53. package/lib/web/websocket/stream/websocketerror.js +1 -1
  54. package/lib/web/websocket/stream/websocketstream.js +8 -5
  55. package/lib/web/websocket/websocket.js +61 -5
  56. package/package.json +5 -5
  57. package/types/diagnostics-channel.d.ts +9 -0
  58. package/types/dispatcher.d.ts +3 -2
  59. package/types/env-http-proxy-agent.d.ts +2 -1
  60. package/types/eventsource.d.ts +3 -3
  61. package/types/fetch.d.ts +1 -0
  62. package/types/handlers.d.ts +1 -1
  63. package/types/mock-client.d.ts +2 -0
  64. package/types/mock-interceptor.d.ts +2 -0
  65. package/types/mock-pool.d.ts +2 -0
  66. package/types/retry-handler.d.ts +9 -0
  67. package/types/webidl.d.ts +29 -15
  68. package/types/websocket.d.ts +3 -1
  69. package/lib/web/fetch/dispatcher-weakref.js +0 -46
@@ -12,64 +12,34 @@ let tls // include tls conditionally since it is not always available
12
12
  // resolve the same servername multiple times even when
13
13
  // re-use is enabled.
14
14
 
15
- let SessionCache
16
- // FIXME: remove workaround when the Node bug is fixed
17
- // https://github.com/nodejs/node/issues/49344#issuecomment-1741776308
18
- if (global.FinalizationRegistry && !(process.env.NODE_V8_COVERAGE || process.env.UNDICI_NO_FG)) {
19
- SessionCache = class WeakSessionCache {
20
- constructor (maxCachedSessions) {
21
- this._maxCachedSessions = maxCachedSessions
22
- this._sessionCache = new Map()
23
- this._sessionRegistry = new global.FinalizationRegistry((key) => {
24
- if (this._sessionCache.size < this._maxCachedSessions) {
25
- return
26
- }
27
-
28
- const ref = this._sessionCache.get(key)
29
- if (ref !== undefined && ref.deref() === undefined) {
30
- this._sessionCache.delete(key)
31
- }
32
- })
33
- }
34
-
35
- get (sessionKey) {
36
- const ref = this._sessionCache.get(sessionKey)
37
- return ref ? ref.deref() : null
38
- }
39
-
40
- set (sessionKey, session) {
41
- if (this._maxCachedSessions === 0) {
15
+ const SessionCache = class WeakSessionCache {
16
+ constructor (maxCachedSessions) {
17
+ this._maxCachedSessions = maxCachedSessions
18
+ this._sessionCache = new Map()
19
+ this._sessionRegistry = new FinalizationRegistry((key) => {
20
+ if (this._sessionCache.size < this._maxCachedSessions) {
42
21
  return
43
22
  }
44
23
 
45
- this._sessionCache.set(sessionKey, new WeakRef(session))
46
- this._sessionRegistry.register(session, sessionKey)
47
- }
48
- }
49
- } else {
50
- SessionCache = class SimpleSessionCache {
51
- constructor (maxCachedSessions) {
52
- this._maxCachedSessions = maxCachedSessions
53
- this._sessionCache = new Map()
54
- }
55
-
56
- get (sessionKey) {
57
- return this._sessionCache.get(sessionKey)
58
- }
59
-
60
- set (sessionKey, session) {
61
- if (this._maxCachedSessions === 0) {
62
- return
24
+ const ref = this._sessionCache.get(key)
25
+ if (ref !== undefined && ref.deref() === undefined) {
26
+ this._sessionCache.delete(key)
63
27
  }
28
+ })
29
+ }
64
30
 
65
- if (this._sessionCache.size >= this._maxCachedSessions) {
66
- // remove the oldest session
67
- const { value: oldestKey } = this._sessionCache.keys().next()
68
- this._sessionCache.delete(oldestKey)
69
- }
31
+ get (sessionKey) {
32
+ const ref = this._sessionCache.get(sessionKey)
33
+ return ref ? ref.deref() : null
34
+ }
70
35
 
71
- this._sessionCache.set(sessionKey, session)
36
+ set (sessionKey, session) {
37
+ if (this._maxCachedSessions === 0) {
38
+ return
72
39
  }
40
+
41
+ this._sessionCache.set(sessionKey, new WeakRef(session))
42
+ this._sessionRegistry.register(session, sessionKey)
73
43
  }
74
44
  }
75
45
 
@@ -16,6 +16,8 @@ const channels = {
16
16
  // Request
17
17
  create: diagnosticsChannel.channel('undici:request:create'),
18
18
  bodySent: diagnosticsChannel.channel('undici:request:bodySent'),
19
+ bodyChunkSent: diagnosticsChannel.channel('undici:request:bodyChunkSent'),
20
+ bodyChunkReceived: diagnosticsChannel.channel('undici:request:bodyChunkReceived'),
19
21
  headers: diagnosticsChannel.channel('undici:request:headers'),
20
22
  trailers: diagnosticsChannel.channel('undici:request:trailers'),
21
23
  error: diagnosticsChannel.channel('undici:request:error'),
@@ -85,7 +87,7 @@ function trackClientEvents (debugLog = undiciDebugLog) {
85
87
  const {
86
88
  request: { method, path, origin }
87
89
  } = evt
88
- debugLog('sending request to %s %s/%s', method, origin, path)
90
+ debugLog('sending request to %s %s%s', method, origin, path)
89
91
  })
90
92
  }
91
93
 
@@ -105,7 +107,7 @@ function trackRequestEvents (debugLog = undiciDebugLog) {
105
107
  response: { statusCode }
106
108
  } = evt
107
109
  debugLog(
108
- 'received response to %s %s/%s - HTTP %d',
110
+ 'received response to %s %s%s - HTTP %d',
109
111
  method,
110
112
  origin,
111
113
  path,
@@ -118,7 +120,7 @@ function trackRequestEvents (debugLog = undiciDebugLog) {
118
120
  const {
119
121
  request: { method, path, origin }
120
122
  } = evt
121
- debugLog('trailers received from %s %s/%s', method, origin, path)
123
+ debugLog('trailers received from %s %s%s', method, origin, path)
122
124
  })
123
125
 
124
126
  diagnosticsChannel.subscribe('undici:request:error',
@@ -128,7 +130,7 @@ function trackRequestEvents (debugLog = undiciDebugLog) {
128
130
  error
129
131
  } = evt
130
132
  debugLog(
131
- 'request to %s %s/%s errored - %s',
133
+ 'request to %s %s%s errored - %s',
132
134
  method,
133
135
  origin,
134
136
  path,
@@ -42,7 +42,8 @@ class Request {
42
42
  reset,
43
43
  expectContinue,
44
44
  servername,
45
- throwOnError
45
+ throwOnError,
46
+ maxRedirections
46
47
  }, handler) {
47
48
  if (typeof path !== 'string') {
48
49
  throw new InvalidArgumentError('path must be a string')
@@ -86,6 +87,10 @@ class Request {
86
87
  throw new InvalidArgumentError('invalid throwOnError')
87
88
  }
88
89
 
90
+ if (maxRedirections != null && maxRedirections !== 0) {
91
+ throw new InvalidArgumentError('maxRedirections is not supported, use the redirect interceptor')
92
+ }
93
+
89
94
  this.headersTimeout = headersTimeout
90
95
 
91
96
  this.bodyTimeout = bodyTimeout
@@ -194,6 +199,9 @@ class Request {
194
199
  }
195
200
 
196
201
  onBodySent (chunk) {
202
+ if (channels.bodyChunkSent.hasSubscribers) {
203
+ channels.bodyChunkSent.publish({ request: this, chunk })
204
+ }
197
205
  if (this[kHandler].onBodySent) {
198
206
  try {
199
207
  return this[kHandler].onBodySent(chunk)
@@ -252,6 +260,9 @@ class Request {
252
260
  assert(!this.aborted)
253
261
  assert(!this.completed)
254
262
 
263
+ if (channels.bodyChunkReceived.hasSubscribers) {
264
+ channels.bodyChunkReceived.publish({ request: this, chunk })
265
+ }
255
266
  try {
256
267
  return this[kHandler].onData(chunk)
257
268
  } catch (err) {
package/lib/core/tree.js CHANGED
@@ -86,7 +86,7 @@ class TstNode {
86
86
 
87
87
  /**
88
88
  * @param {Uint8Array} key
89
- * @return {TstNode | null}
89
+ * @returns {TstNode | null}
90
90
  */
91
91
  search (key) {
92
92
  const keylength = key.length
package/lib/core/util.js CHANGED
@@ -6,7 +6,6 @@ const { IncomingMessage } = require('node:http')
6
6
  const stream = require('node:stream')
7
7
  const net = require('node:net')
8
8
  const { Blob } = require('node:buffer')
9
- const nodeUtil = require('node:util')
10
9
  const { stringify } = require('node:querystring')
11
10
  const { EventEmitter: EE } = require('node:events')
12
11
  const timers = require('../util/timers')
@@ -660,48 +659,6 @@ function addAbortListener (signal, listener) {
660
659
  return () => signal.removeListener('abort', listener)
661
660
  }
662
661
 
663
- /**
664
- * @function
665
- * @param {string} value
666
- * @returns {string}
667
- */
668
- const toUSVString = (() => {
669
- if (typeof String.prototype.toWellFormed === 'function') {
670
- /**
671
- * @param {string} value
672
- * @returns {string}
673
- */
674
- return (value) => `${value}`.toWellFormed()
675
- } else {
676
- /**
677
- * @param {string} value
678
- * @returns {string}
679
- */
680
- return nodeUtil.toUSVString
681
- }
682
- })()
683
-
684
- /**
685
- * @param {*} value
686
- * @returns {boolean}
687
- */
688
- // TODO: move this to webidl
689
- const isUSVString = (() => {
690
- if (typeof String.prototype.isWellFormed === 'function') {
691
- /**
692
- * @param {*} value
693
- * @returns {boolean}
694
- */
695
- return (value) => `${value}`.isWellFormed()
696
- } else {
697
- /**
698
- * @param {*} value
699
- * @returns {boolean}
700
- */
701
- return (value) => toUSVString(value) === `${value}`
702
- }
703
- })()
704
-
705
662
  /**
706
663
  * @see https://tools.ietf.org/html/rfc7230#section-3.2.6
707
664
  * @param {number} c
@@ -943,8 +900,6 @@ Object.setPrototypeOf(normalizedMethodRecords, null)
943
900
  module.exports = {
944
901
  kEnumerableProperty,
945
902
  isDisturbed,
946
- toUSVString,
947
- isUSVString,
948
903
  isBlobLike,
949
904
  parseOrigin,
950
905
  parseURL,
@@ -60,12 +60,12 @@ const removeAllListeners = util.removeAllListeners
60
60
 
61
61
  let extractBody
62
62
 
63
- async function lazyllhttp () {
63
+ function lazyllhttp () {
64
64
  const llhttpWasmData = process.env.JEST_WORKER_ID ? require('../llhttp/llhttp-wasm.js') : undefined
65
65
 
66
66
  let mod
67
67
  try {
68
- mod = await WebAssembly.compile(require('../llhttp/llhttp_simd-wasm.js'))
68
+ mod = new WebAssembly.Module(require('../llhttp/llhttp_simd-wasm.js'))
69
69
  } catch (e) {
70
70
  /* istanbul ignore next */
71
71
 
@@ -73,10 +73,10 @@ async function lazyllhttp () {
73
73
  // being enabled, but the occurring of this other error
74
74
  // * https://github.com/emscripten-core/emscripten/issues/11495
75
75
  // got me to remove that check to avoid breaking Node 12.
76
- mod = await WebAssembly.compile(llhttpWasmData || require('../llhttp/llhttp-wasm.js'))
76
+ mod = new WebAssembly.Module(llhttpWasmData || require('../llhttp/llhttp-wasm.js'))
77
77
  }
78
78
 
79
- return await WebAssembly.instantiate(mod, {
79
+ return new WebAssembly.Instance(mod, {
80
80
  env: {
81
81
  /**
82
82
  * @param {number} p
@@ -165,11 +165,6 @@ async function lazyllhttp () {
165
165
  }
166
166
 
167
167
  let llhttpInstance = null
168
- /**
169
- * @type {Promise<WebAssembly.Instance>|null}
170
- */
171
- let llhttpPromise = lazyllhttp()
172
- llhttpPromise.catch()
173
168
 
174
169
  /**
175
170
  * @type {Parser|null}
@@ -249,7 +244,7 @@ class Parser {
249
244
  this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this))
250
245
  } else {
251
246
  this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this))
252
- this.timeout.unref()
247
+ this.timeout?.unref()
253
248
  }
254
249
  }
255
250
 
@@ -732,7 +727,7 @@ class Parser {
732
727
  // We must wait a full event loop cycle to reuse this socket to make sure
733
728
  // that non-spec compliant servers are not closing the connection even if they
734
729
  // said they won't.
735
- setImmediate(() => client[kResume]())
730
+ setImmediate(client[kResume])
736
731
  } else {
737
732
  client[kResume]()
738
733
  }
@@ -769,11 +764,7 @@ async function connectH1 (client, socket) {
769
764
  client[kSocket] = socket
770
765
 
771
766
  if (!llhttpInstance) {
772
- const noop = () => {}
773
- socket.on('error', noop)
774
- llhttpInstance = await llhttpPromise
775
- llhttpPromise = null
776
- socket.off('error', noop)
767
+ llhttpInstance = lazyllhttp()
777
768
  }
778
769
 
779
770
  if (socket.errored) {
@@ -1297,9 +1288,9 @@ function writeStream (abort, body, client, request, socket, contentLength, heade
1297
1288
  .on('error', onFinished)
1298
1289
 
1299
1290
  if (body.errorEmitted ?? body.errored) {
1300
- setImmediate(() => onFinished(body.errored))
1291
+ setImmediate(onFinished, body.errored)
1301
1292
  } else if (body.endEmitted ?? body.readableEnded) {
1302
- setImmediate(() => onFinished(null))
1293
+ setImmediate(onFinished, null)
1303
1294
  }
1304
1295
 
1305
1296
  if (body.closeEmitted ?? body.closed) {
@@ -144,7 +144,8 @@ class ProxyAgent extends DispatcherBase {
144
144
  signal: opts.signal,
145
145
  headers: {
146
146
  ...this[kProxyHeaders],
147
- host: opts.host
147
+ host: opts.host,
148
+ ...(opts.connections == null || opts.connections > 0 ? { 'proxy-connection': 'keep-alive' } : {})
148
149
  },
149
150
  servername: this[kProxyTls]?.servername || proxyHostname
150
151
  })
@@ -241,7 +241,10 @@ class CacheHandler {
241
241
  * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
242
242
  */
243
243
  function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives) {
244
- if (statusCode !== 200 && statusCode !== 307) {
244
+ // Allow caching for status codes 200 and 307 (original behavior)
245
+ // Also allow caching for other status codes that are heuristically cacheable
246
+ // when they have explicit cache directives
247
+ if (statusCode !== 200 && statusCode !== 307 && !HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode)) {
245
248
  return false
246
249
  }
247
250
 
@@ -42,7 +42,8 @@ class RedirectHandler {
42
42
 
43
43
  this.dispatch = dispatch
44
44
  this.location = null
45
- this.opts = { ...opts, maxRedirections: 0 } // opts must be a copy
45
+ const { maxRedirections: _, ...cleanOpts } = opts
46
+ this.opts = cleanOpts // opts must be a copy, exclude maxRedirections
46
47
  this.maxRedirections = maxRedirections
47
48
  this.handler = handler
48
49
  this.history = []
@@ -138,7 +139,6 @@ class RedirectHandler {
138
139
  this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin)
139
140
  this.opts.path = path
140
141
  this.opts.origin = origin
141
- this.opts.maxRedirections = 0
142
142
  this.opts.query = null
143
143
  }
144
144
 
@@ -29,13 +29,16 @@ class RetryHandler {
29
29
  methods,
30
30
  errorCodes,
31
31
  retryAfter,
32
- statusCodes
32
+ statusCodes,
33
+ throwOnError
33
34
  } = retryOptions ?? {}
34
35
 
36
+ this.error = null
35
37
  this.dispatch = dispatch
36
38
  this.handler = WrapHandler.wrap(handler)
37
39
  this.opts = { ...dispatchOpts, body: wrapRequestBody(opts.body) }
38
40
  this.retryOpts = {
41
+ throwOnError: throwOnError ?? true,
39
42
  retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry],
40
43
  retryAfter: retryAfter ?? true,
41
44
  maxTimeout: maxTimeout ?? 30 * 1000, // 30s,
@@ -68,6 +71,50 @@ class RetryHandler {
68
71
  this.etag = null
69
72
  }
70
73
 
74
+ onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
75
+ if (this.retryOpts.throwOnError) {
76
+ // Preserve old behavior for status codes that are not eligible for retry
77
+ if (this.retryOpts.statusCodes.includes(statusCode) === false) {
78
+ this.headersSent = true
79
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
80
+ } else {
81
+ this.error = err
82
+ }
83
+
84
+ return
85
+ }
86
+
87
+ if (isDisturbed(this.opts.body)) {
88
+ this.headersSent = true
89
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
90
+ return
91
+ }
92
+
93
+ function shouldRetry (passedErr) {
94
+ if (passedErr) {
95
+ this.headersSent = true
96
+
97
+ this.headersSent = true
98
+ this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
99
+ controller.resume()
100
+ return
101
+ }
102
+
103
+ this.error = err
104
+ controller.resume()
105
+ }
106
+
107
+ controller.pause()
108
+ this.retryOpts.retry(
109
+ err,
110
+ {
111
+ state: { counter: this.retryCount },
112
+ opts: { retryOptions: this.retryOpts, ...this.opts }
113
+ },
114
+ shouldRetry.bind(this)
115
+ )
116
+ }
117
+
71
118
  onRequestStart (controller, context) {
72
119
  if (!this.headersSent) {
73
120
  this.handler.onRequestStart?.(controller, context)
@@ -137,26 +184,19 @@ class RetryHandler {
137
184
  }
138
185
 
139
186
  onResponseStart (controller, statusCode, headers, statusMessage) {
187
+ this.error = null
140
188
  this.retryCount += 1
141
189
 
142
190
  if (statusCode >= 300) {
143
- if (this.retryOpts.statusCodes.includes(statusCode) === false) {
144
- this.headersSent = true
145
- this.handler.onResponseStart?.(
146
- controller,
147
- statusCode,
148
- headers,
149
- statusMessage
150
- )
151
- return
152
- } else {
153
- throw new RequestRetryError('Request failed', statusCode, {
154
- headers,
155
- data: {
156
- count: this.retryCount
157
- }
158
- })
159
- }
191
+ const err = new RequestRetryError('Request failed', statusCode, {
192
+ headers,
193
+ data: {
194
+ count: this.retryCount
195
+ }
196
+ })
197
+
198
+ this.onResponseStartWithRetry(controller, statusCode, headers, statusMessage, err)
199
+ return
160
200
  }
161
201
 
162
202
  // Checkpoint for resume from where we left it
@@ -175,6 +215,7 @@ class RetryHandler {
175
215
  const contentRange = parseRangeHeader(headers['content-range'])
176
216
  // If no content range
177
217
  if (!contentRange) {
218
+ // We always throw here as we want to indicate that we entred unexpected path
178
219
  throw new RequestRetryError('Content-Range mismatch', statusCode, {
179
220
  headers,
180
221
  data: { count: this.retryCount }
@@ -183,6 +224,7 @@ class RetryHandler {
183
224
 
184
225
  // Let's start with a weak etag check
185
226
  if (this.etag != null && this.etag !== headers.etag) {
227
+ // We always throw here as we want to indicate that we entred unexpected path
186
228
  throw new RequestRetryError('ETag mismatch', statusCode, {
187
229
  headers,
188
230
  data: { count: this.retryCount }
@@ -266,14 +308,52 @@ class RetryHandler {
266
308
  }
267
309
 
268
310
  onResponseData (controller, chunk) {
311
+ if (this.error) {
312
+ return
313
+ }
314
+
269
315
  this.start += chunk.length
270
316
 
271
317
  this.handler.onResponseData?.(controller, chunk)
272
318
  }
273
319
 
274
320
  onResponseEnd (controller, trailers) {
275
- this.retryCount = 0
276
- return this.handler.onResponseEnd?.(controller, trailers)
321
+ if (this.error && this.retryOpts.throwOnError) {
322
+ throw this.error
323
+ }
324
+
325
+ if (!this.error) {
326
+ this.retryCount = 0
327
+ return this.handler.onResponseEnd?.(controller, trailers)
328
+ }
329
+
330
+ this.retry(controller)
331
+ }
332
+
333
+ retry (controller) {
334
+ if (this.start !== 0) {
335
+ const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
336
+
337
+ // Weak etag check - weak etags will make comparison algorithms never match
338
+ if (this.etag != null) {
339
+ headers['if-match'] = this.etag
340
+ }
341
+
342
+ this.opts = {
343
+ ...this.opts,
344
+ headers: {
345
+ ...this.opts.headers,
346
+ ...headers
347
+ }
348
+ }
349
+ }
350
+
351
+ try {
352
+ this.retryCountCheckpoint = this.retryCount
353
+ this.dispatch(this.opts, this)
354
+ } catch (err) {
355
+ this.handler.onResponseError?.(controller, err)
356
+ }
277
357
  }
278
358
 
279
359
  onResponseError (controller, err) {
@@ -282,6 +362,15 @@ class RetryHandler {
282
362
  return
283
363
  }
284
364
 
365
+ function shouldRetry (returnedErr) {
366
+ if (!returnedErr) {
367
+ this.retry(controller)
368
+ return
369
+ }
370
+
371
+ this.handler?.onResponseError?.(controller, returnedErr)
372
+ }
373
+
285
374
  // We reconcile in case of a mix between network errors
286
375
  // and server error response
287
376
  if (this.retryCount - this.retryCountCheckpoint > 0) {
@@ -299,43 +388,8 @@ class RetryHandler {
299
388
  state: { counter: this.retryCount },
300
389
  opts: { retryOptions: this.retryOpts, ...this.opts }
301
390
  },
302
- onRetry.bind(this)
391
+ shouldRetry.bind(this)
303
392
  )
304
-
305
- /**
306
- * @this {RetryHandler}
307
- * @param {Error} [err]
308
- * @returns
309
- */
310
- function onRetry (err) {
311
- if (err != null || controller?.aborted || isDisturbed(this.opts.body)) {
312
- return this.handler.onResponseError?.(controller, err)
313
- }
314
-
315
- if (this.start !== 0) {
316
- const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
317
-
318
- // Weak etag check - weak etags will make comparison algorithms never match
319
- if (this.etag != null) {
320
- headers['if-match'] = this.etag
321
- }
322
-
323
- this.opts = {
324
- ...this.opts,
325
- headers: {
326
- ...this.opts.headers,
327
- ...headers
328
- }
329
- }
330
- }
331
-
332
- try {
333
- this.retryCountCheckpoint = this.retryCount
334
- this.dispatch(this.opts, this)
335
- } catch (err) {
336
- this.handler.onResponseError?.(controller, err)
337
- }
338
- }
339
393
  }
340
394
  }
341
395
 
@@ -301,11 +301,11 @@ module.exports = (opts = {}) => {
301
301
  assertCacheMethods(methods, 'opts.methods')
302
302
 
303
303
  if (typeof cacheByDefault !== 'undefined' && typeof cacheByDefault !== 'number') {
304
- throw new TypeError(`exepcted opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
304
+ throw new TypeError(`expected opts.cacheByDefault to be number or undefined, got ${typeof cacheByDefault}`)
305
305
  }
306
306
 
307
307
  if (typeof type !== 'undefined' && type !== 'shared' && type !== 'private') {
308
- throw new TypeError(`exepcted opts.type to be shared, private, or undefined, got ${typeof type}`)
308
+ throw new TypeError(`expected opts.type to be shared, private, or undefined, got ${typeof type}`)
309
309
  }
310
310
 
311
311
  const globalOpts = {
@@ -11,7 +11,7 @@ function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections }
11
11
  return dispatch(opts, handler)
12
12
  }
13
13
 
14
- const dispatchOpts = { ...rest, maxRedirections: 0 } // Stop sub dispatcher from also redirecting.
14
+ const dispatchOpts = { ...rest } // Stop sub dispatcher from also redirecting.
15
15
  const redirectHandler = new RedirectHandler(dispatch, maxRedirections, dispatchOpts, handler)
16
16
  return dispatch(dispatchOpts, redirectHandler)
17
17
  }
@@ -54,6 +54,10 @@ class MockClient extends Client {
54
54
  )
55
55
  }
56
56
 
57
+ cleanMocks () {
58
+ this[kDispatches] = []
59
+ }
60
+
57
61
  async [kClose] () {
58
62
  await promisify(this[kOriginalClose])()
59
63
  this[kConnected] = 0
@@ -54,6 +54,10 @@ class MockPool extends Pool {
54
54
  )
55
55
  }
56
56
 
57
+ cleanMocks () {
58
+ this[kDispatches] = []
59
+ }
60
+
57
61
  async [kClose] () {
58
62
  await promisify(this[kOriginalClose])()
59
63
  this[kConnected] = 0