undici 7.29.0 → 7.30.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.
@@ -35,6 +35,55 @@ function validatePartialResponseContentLength (headers, range, statusCode, retry
35
35
  }
36
36
  }
37
37
 
38
+ // A stable controller handed to the downstream handler for the lifetime of the
39
+ // request. Each transparent retry/resume is a separate dispatch with its own
40
+ // connection controller. The proxy always forwards to the active connection
41
+ // while preserving a downstream pause across controller replacement.
42
+ class RetryController {
43
+ #paused = false
44
+ #target = null
45
+
46
+ set target (target) {
47
+ this.#target = target
48
+ if (this.#paused) {
49
+ target?.pause()
50
+ }
51
+ }
52
+
53
+ get target () { return this.#target }
54
+
55
+ pause () {
56
+ this.#paused = true
57
+ this.#target?.pause()
58
+ }
59
+
60
+ resume () {
61
+ this.#paused = false
62
+ this.#target?.resume()
63
+ }
64
+
65
+ abort (reason) {
66
+ this.#target?.abort(reason)
67
+ }
68
+
69
+ get paused () { return this.#paused || (this.#target?.paused ?? false) }
70
+ get aborted () { return this.#target?.aborted ?? false }
71
+ get reason () { return this.#target?.reason ?? null }
72
+ get rawHeaders () { return this.#target?.rawHeaders ?? null }
73
+ set rawHeaders (value) {
74
+ if (this.#target) {
75
+ this.#target.rawHeaders = value
76
+ }
77
+ }
78
+
79
+ get rawTrailers () { return this.#target?.rawTrailers ?? null }
80
+ set rawTrailers (value) {
81
+ if (this.#target) {
82
+ this.#target.rawTrailers = value
83
+ }
84
+ }
85
+ }
86
+
38
87
  class RetryHandler {
39
88
  constructor (opts, { dispatch, handler }) {
40
89
  const { retryOptions, ...dispatchOpts } = opts
@@ -89,14 +138,23 @@ class RetryHandler {
89
138
  this.start = 0
90
139
  this.end = null
91
140
  this.etag = null
141
+ this.controllerProxy = new RetryController()
92
142
  }
93
143
 
94
144
  onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) {
95
145
  if (this.retryOpts.throwOnError) {
96
146
  // Preserve old behavior for status codes that are not eligible for retry
97
147
  if (this.retryOpts.statusCodes.includes(statusCode) === false) {
98
- this.headersSent = true
99
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
148
+ if (this.headersSent) {
149
+ // The downstream handler already received the response from an
150
+ // earlier attempt. Forwarding this response would replace the
151
+ // downstream body and leave the original body pending forever.
152
+ this.handler.onResponseError?.(this.controllerProxy, err)
153
+ } else {
154
+ this.headersSent = true
155
+ this.checkpointResponseEnd(headers)
156
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
157
+ }
100
158
  } else {
101
159
  this.error = err
102
160
  }
@@ -106,14 +164,23 @@ class RetryHandler {
106
164
 
107
165
  if (isDisturbed(this.opts.body)) {
108
166
  this.headersSent = true
109
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
167
+ this.checkpointResponseEnd(headers)
168
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
110
169
  return
111
170
  }
112
171
 
113
172
  function shouldRetry (passedErr) {
114
173
  if (passedErr) {
115
- this.headersSent = true
116
- this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage)
174
+ if (this.headersSent) {
175
+ // The downstream handler already received the response from an
176
+ // earlier attempt. Forwarding this response would replace the
177
+ // downstream body and leave the original body pending forever.
178
+ this.handler.onResponseError?.(this.controllerProxy, passedErr)
179
+ } else {
180
+ this.headersSent = true
181
+ this.checkpointResponseEnd(headers)
182
+ this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage)
183
+ }
117
184
  controller.resume()
118
185
  return
119
186
  }
@@ -133,14 +200,29 @@ class RetryHandler {
133
200
  )
134
201
  }
135
202
 
203
+ checkpointResponseEnd (headers) {
204
+ if (this.end == null && this.opts.method !== 'HEAD') {
205
+ const contentLength = headers['content-length']
206
+ this.end = contentLength != null ? Number(contentLength) - 1 : null
207
+
208
+ assert(
209
+ this.end == null || Number.isFinite(this.end),
210
+ 'invalid content-length'
211
+ )
212
+
213
+ this.resume = this.end != null
214
+ }
215
+ }
216
+
136
217
  onRequestStart (controller, context) {
218
+ this.controllerProxy.target = controller
137
219
  if (!this.headersSent) {
138
- this.handler.onRequestStart?.(controller, context)
220
+ this.handler.onRequestStart?.(this.controllerProxy, context)
139
221
  }
140
222
  }
141
223
 
142
- onRequestUpgrade (controller, statusCode, headers, socket) {
143
- this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket)
224
+ onRequestUpgrade (_controller, statusCode, headers, socket) {
225
+ this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket)
144
226
  }
145
227
 
146
228
  static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) {
@@ -253,8 +335,12 @@ class RetryHandler {
253
335
 
254
336
  const { start, size, end = size ? size - 1 : null } = contentRange
255
337
 
256
- assert(this.start === start, 'content-range mismatch')
257
- assert(this.end == null || this.end === end, 'content-range mismatch')
338
+ if (this.start !== start || (this.end != null && this.end !== end)) {
339
+ throw new RequestRetryError('Content-Range mismatch', statusCode, {
340
+ headers,
341
+ data: { count: this.retryCount }
342
+ })
343
+ }
258
344
 
259
345
  return
260
346
  }
@@ -267,7 +353,7 @@ class RetryHandler {
267
353
  if (range == null) {
268
354
  this.headersSent = true
269
355
  this.handler.onResponseStart?.(
270
- controller,
356
+ this.controllerProxy,
271
357
  statusCode,
272
358
  headers,
273
359
  statusMessage
@@ -316,7 +402,7 @@ class RetryHandler {
316
402
 
317
403
  this.headersSent = true
318
404
  this.handler.onResponseStart?.(
319
- controller,
405
+ this.controllerProxy,
320
406
  statusCode,
321
407
  headers,
322
408
  statusMessage
@@ -329,30 +415,30 @@ class RetryHandler {
329
415
  }
330
416
  }
331
417
 
332
- onResponseData (controller, chunk) {
418
+ onResponseData (_controller, chunk) {
333
419
  if (this.error) {
334
420
  return
335
421
  }
336
422
 
337
423
  this.start += chunk.length
338
424
 
339
- this.handler.onResponseData?.(controller, chunk)
425
+ this.handler.onResponseData?.(this.controllerProxy, chunk)
340
426
  }
341
427
 
342
- onResponseEnd (controller, trailers) {
428
+ onResponseEnd (_controller, trailers) {
343
429
  if (this.error && this.retryOpts.throwOnError) {
344
430
  throw this.error
345
431
  }
346
432
 
347
433
  if (!this.error) {
348
434
  this.retryCount = 0
349
- return this.handler.onResponseEnd?.(controller, trailers)
435
+ return this.handler.onResponseEnd?.(this.controllerProxy, trailers)
350
436
  }
351
437
 
352
- this.retry(controller)
438
+ this.retry()
353
439
  }
354
440
 
355
- retry (controller) {
441
+ retry () {
356
442
  if (this.start !== 0) {
357
443
  const headers = { range: `bytes=${this.start}-${this.end ?? ''}` }
358
444
 
@@ -374,23 +460,23 @@ class RetryHandler {
374
460
  this.retryCountCheckpoint = this.retryCount
375
461
  this.dispatch(this.opts, this)
376
462
  } catch (err) {
377
- this.handler.onResponseError?.(controller, err)
463
+ this.handler.onResponseError?.(this.controllerProxy, err)
378
464
  }
379
465
  }
380
466
 
381
467
  onResponseError (controller, err) {
382
- if (controller?.aborted || isDisturbed(this.opts.body)) {
383
- this.handler.onResponseError?.(controller, err)
468
+ if (controller?.aborted || isDisturbed(this.opts.body) || (this.headersSent && !this.resume)) {
469
+ this.handler.onResponseError?.(this.controllerProxy, err)
384
470
  return
385
471
  }
386
472
 
387
473
  function shouldRetry (returnedErr) {
388
474
  if (!returnedErr) {
389
- this.retry(controller)
475
+ this.retry()
390
476
  return
391
477
  }
392
478
 
393
- this.handler?.onResponseError?.(controller, returnedErr)
479
+ this.handler?.onResponseError?.(this.controllerProxy, returnedErr)
394
480
  }
395
481
 
396
482
  // We reconcile in case of a mix between network errors
@@ -117,7 +117,10 @@ function staleResponseRequiresRevalidation (result, cacheType) {
117
117
  * @returns {boolean}
118
118
  */
119
119
  function revalidationResponseDisallowsCachedReuse (cacheType, headers) {
120
- if (headers.vary && isInvalidOrWildcardVaryHeader(headers.vary)) {
120
+ if (
121
+ (headers.vary && isInvalidOrWildcardVaryHeader(headers.vary)) ||
122
+ (cacheType === 'shared' && Object.hasOwn(headers, 'set-cookie'))
123
+ ) {
121
124
  return true
122
125
  }
123
126
 
@@ -376,6 +379,17 @@ function handleResult (
376
379
  return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
377
380
  }
378
381
 
382
+ // Shared stores may outlive the Undici version that wrote them. Do not
383
+ // re-serve a Set-Cookie header from an existing shared-cache entry.
384
+ if (globalOpts.type === 'shared' && Object.hasOwn(result.headers, 'set-cookie')) {
385
+ if (util.isStream(result.body)) {
386
+ result.body.on('error', nop).destroy()
387
+ }
388
+
389
+ deleteCachedValue(globalOpts.store, cacheKey)
390
+ return handleUncachedResponse(dispatch, globalOpts, cacheKey, handler, opts, reqCacheControl)
391
+ }
392
+
379
393
  const now = Date.now()
380
394
  if (now > result.deleteAt) {
381
395
  // Response is expired, cache store shouldn't have given this to us
@@ -574,6 +588,11 @@ module.exports = (opts = {}) => {
574
588
  * @type {import('../../types/cache-interceptor.d.ts').default.CacheKey}
575
589
  */
576
590
  const cacheKey = makeCacheKey(opts)
591
+
592
+ if (!arrayIncludes(util.safeHTTPMethods, opts.method)) {
593
+ return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler))
594
+ }
595
+
577
596
  const result = store.get(cacheKey)
578
597
 
579
598
  if (result && typeof result.then === 'function') {