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.
- package/docs/docs/api/DiagnosticsChannel.md +3 -1
- package/docs/docs/api/Dispatcher.md +1 -0
- package/lib/core/request.js +23 -2
- package/lib/dispatcher/balanced-pool.js +4 -2
- package/lib/dispatcher/client-h1.js +23 -8
- package/lib/dispatcher/client-h2.js +84 -19
- package/lib/dispatcher/client.js +6 -2
- package/lib/handler/cache-handler.js +21 -5
- package/lib/handler/retry-handler.js +109 -23
- package/lib/interceptor/cache.js +20 -1
- package/lib/interceptor/decompress.js +338 -46
- package/lib/interceptor/dump.js +10 -23
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- package/lib/web/websocket/connection.js +19 -9
- package/lib/web/websocket/permessage-deflate.js +5 -0
- package/lib/web/websocket/stream/websocketstream.js +8 -10
- package/package.json +1 -1
- package/types/interceptors.d.ts +2 -0
|
@@ -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
|
|
99
|
-
|
|
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.
|
|
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
|
|
116
|
-
|
|
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?.(
|
|
220
|
+
this.handler.onRequestStart?.(this.controllerProxy, context)
|
|
139
221
|
}
|
|
140
222
|
}
|
|
141
223
|
|
|
142
|
-
onRequestUpgrade (
|
|
143
|
-
this.handler.onRequestUpgrade?.(
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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?.(
|
|
425
|
+
this.handler.onResponseData?.(this.controllerProxy, chunk)
|
|
340
426
|
}
|
|
341
427
|
|
|
342
|
-
onResponseEnd (
|
|
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?.(
|
|
435
|
+
return this.handler.onResponseEnd?.(this.controllerProxy, trailers)
|
|
350
436
|
}
|
|
351
437
|
|
|
352
|
-
this.retry(
|
|
438
|
+
this.retry()
|
|
353
439
|
}
|
|
354
440
|
|
|
355
|
-
retry (
|
|
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?.(
|
|
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?.(
|
|
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(
|
|
475
|
+
this.retry()
|
|
390
476
|
return
|
|
391
477
|
}
|
|
392
478
|
|
|
393
|
-
this.handler?.onResponseError?.(
|
|
479
|
+
this.handler?.onResponseError?.(this.controllerProxy, returnedErr)
|
|
394
480
|
}
|
|
395
481
|
|
|
396
482
|
// We reconcile in case of a mix between network errors
|
package/lib/interceptor/cache.js
CHANGED
|
@@ -117,7 +117,10 @@ function staleResponseRequiresRevalidation (result, cacheType) {
|
|
|
117
117
|
* @returns {boolean}
|
|
118
118
|
*/
|
|
119
119
|
function revalidationResponseDisallowsCachedReuse (cacheType, headers) {
|
|
120
|
-
if (
|
|
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') {
|