undici 7.28.0 → 7.29.1
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/lib/cache/memory-cache-store.js +54 -9
- package/lib/cache/sqlite-cache-store.js +7 -1
- package/lib/core/request.js +12 -1
- package/lib/dispatcher/balanced-pool.js +4 -2
- package/lib/dispatcher/client-h1.js +23 -6
- package/lib/dispatcher/client-h2.js +70 -14
- package/lib/dispatcher/client.js +6 -2
- package/lib/handler/cache-handler.js +266 -68
- package/lib/handler/cache-revalidation-handler.js +3 -3
- package/lib/handler/retry-handler.js +66 -7
- package/lib/interceptor/cache.js +178 -52
- package/lib/interceptor/decompress.js +146 -14
- package/lib/interceptor/dump.js +10 -23
- package/lib/util/cache.js +324 -62
- package/lib/util/date.js +23 -6
- package/lib/web/cookies/util.js +79 -9
- package/lib/web/eventsource/eventsource-stream.js +245 -150
- package/lib/web/websocket/connection.js +1 -1
- 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
|
@@ -218,17 +218,62 @@ class MemoryCacheStore extends EventEmitter {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
function findEntry (key, entries, now) {
|
|
221
|
-
|
|
222
|
-
entry
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
221
|
+
for (let i = 0; i < entries.length; i++) {
|
|
222
|
+
const entry = entries[i]
|
|
223
|
+
if (
|
|
224
|
+
entry.deleteAt > now &&
|
|
225
|
+
entry.method === key.method &&
|
|
226
|
+
varyMatches(key, entry)
|
|
227
|
+
) {
|
|
228
|
+
return entry
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function varyMatches (key, entry) {
|
|
234
|
+
if (entry.vary == null) {
|
|
235
|
+
return true
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
for (const headerName in entry.vary) {
|
|
239
|
+
if (Object.hasOwn(entry.vary, headerName) && !headerValueEquals(key.headers?.[headerName], entry.vary[headerName])) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return true
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* @param {string|string[]|null|undefined} lhs
|
|
249
|
+
* @param {string|string[]|null|undefined} rhs
|
|
250
|
+
* @returns {boolean}
|
|
251
|
+
*/
|
|
252
|
+
function headerValueEquals (lhs, rhs) {
|
|
253
|
+
if (lhs == null && rhs == null) {
|
|
254
|
+
return true
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if ((lhs == null && rhs != null) ||
|
|
258
|
+
(lhs != null && rhs == null)) {
|
|
259
|
+
return false
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (Array.isArray(lhs) && Array.isArray(rhs)) {
|
|
263
|
+
if (lhs.length !== rhs.length) {
|
|
264
|
+
return false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
268
|
+
if (lhs[i] !== rhs[i]) {
|
|
269
|
+
return false
|
|
227
270
|
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return true
|
|
274
|
+
}
|
|
228
275
|
|
|
229
|
-
|
|
230
|
-
}))
|
|
231
|
-
))
|
|
276
|
+
return lhs === rhs
|
|
232
277
|
}
|
|
233
278
|
|
|
234
279
|
module.exports = MemoryCacheStore
|
|
@@ -454,7 +454,13 @@ function headerValueEquals (lhs, rhs) {
|
|
|
454
454
|
return false
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
|
|
457
|
+
for (let i = 0; i < lhs.length; i++) {
|
|
458
|
+
if (lhs[i] !== rhs[i]) {
|
|
459
|
+
return false
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return true
|
|
458
464
|
}
|
|
459
465
|
|
|
460
466
|
return lhs === rhs
|
package/lib/core/request.js
CHANGED
|
@@ -390,7 +390,13 @@ function processHeader (request, key, val) {
|
|
|
390
390
|
} else if (typeof val[i] === 'object') {
|
|
391
391
|
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
392
392
|
} else {
|
|
393
|
-
|
|
393
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
394
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
395
|
+
const str = `${val[i]}`
|
|
396
|
+
if (!isValidHeaderValue(str)) {
|
|
397
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
398
|
+
}
|
|
399
|
+
arr.push(str)
|
|
394
400
|
}
|
|
395
401
|
}
|
|
396
402
|
val = arr
|
|
@@ -401,7 +407,12 @@ function processHeader (request, key, val) {
|
|
|
401
407
|
} else if (val === null) {
|
|
402
408
|
val = ''
|
|
403
409
|
} else {
|
|
410
|
+
// Coerce primitives (and reject unsafe coercions such as functions
|
|
411
|
+
// with a crafted toString/Symbol.toPrimitive).
|
|
404
412
|
val = `${val}`
|
|
413
|
+
if (!isValidHeaderValue(val)) {
|
|
414
|
+
throw new InvalidArgumentError(`invalid ${key} header`)
|
|
415
|
+
}
|
|
405
416
|
}
|
|
406
417
|
|
|
407
418
|
if (headerName === 'host') {
|
|
@@ -49,14 +49,16 @@ function defaultFactory (origin, opts) {
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
class BalancedPool extends PoolBase {
|
|
52
|
-
constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
|
|
52
|
+
constructor (upstreams = [], { factory = defaultFactory, connect, tls, ...opts } = {}) {
|
|
53
53
|
if (typeof factory !== 'function') {
|
|
54
54
|
throw new InvalidArgumentError('factory must be a function.')
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
super(opts)
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
if (connect && typeof connect !== 'function') connect = { ...connect }
|
|
60
|
+
if (tls && typeof tls !== 'function') tls = { ...tls }
|
|
61
|
+
this[kOptions] = { ...util.deepClone(opts), connect, tls }
|
|
60
62
|
this[kOptions].interceptors = opts.interceptors
|
|
61
63
|
? { ...opts.interceptors }
|
|
62
64
|
: undefined
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
RequestContentLengthMismatchError,
|
|
11
11
|
ResponseContentLengthMismatchError,
|
|
12
12
|
RequestAbortedError,
|
|
13
|
+
InvalidArgumentError,
|
|
13
14
|
HeadersTimeoutError,
|
|
14
15
|
HeadersOverflowError,
|
|
15
16
|
SocketError,
|
|
@@ -1011,7 +1012,7 @@ function onSocketClose () {
|
|
|
1011
1012
|
|
|
1012
1013
|
function clearIdleSocketValidation (socket) {
|
|
1013
1014
|
if (socket[kIdleSocketValidationTimeout]) {
|
|
1014
|
-
|
|
1015
|
+
clearImmediate(socket[kIdleSocketValidationTimeout])
|
|
1015
1016
|
socket[kIdleSocketValidationTimeout] = null
|
|
1016
1017
|
}
|
|
1017
1018
|
|
|
@@ -1020,15 +1021,23 @@ function clearIdleSocketValidation (socket) {
|
|
|
1020
1021
|
|
|
1021
1022
|
function scheduleIdleSocketValidation (client, socket) {
|
|
1022
1023
|
socket[kIdleSocketValidation] = 1
|
|
1023
|
-
|
|
1024
|
+
// Yield to the check phase (after poll) so unsolicited bytes / FIN / RST
|
|
1025
|
+
// already pending on this idle keep-alive socket are processed before the
|
|
1026
|
+
// next request is written (GHSA-35p6-xmwp-9g52).
|
|
1027
|
+
//
|
|
1028
|
+
// setTimeout(0) pays Node's ~1ms timer floor on every sequential reuse
|
|
1029
|
+
// (#5493). setImmediate avoids that, but an *unref'd* Immediate lets poll
|
|
1030
|
+
// block for ~500ms when the event loop is otherwise idle (#5600 / #5606).
|
|
1031
|
+
// A ref'd Immediate both keeps the pending request alive and makes poll
|
|
1032
|
+
// return immediately — the hybrid those issues asked for.
|
|
1033
|
+
socket[kIdleSocketValidationTimeout] = setImmediate(() => {
|
|
1024
1034
|
socket[kIdleSocketValidationTimeout] = null
|
|
1025
1035
|
socket[kIdleSocketValidation] = 2
|
|
1026
1036
|
|
|
1027
1037
|
if (client[kSocket] === socket && !socket.destroyed) {
|
|
1028
1038
|
client[kResume]()
|
|
1029
1039
|
}
|
|
1030
|
-
}
|
|
1031
|
-
socket[kIdleSocketValidationTimeout].unref?.()
|
|
1040
|
+
})
|
|
1032
1041
|
}
|
|
1033
1042
|
|
|
1034
1043
|
/**
|
|
@@ -1134,8 +1143,16 @@ function writeH1 (client, request) {
|
|
|
1134
1143
|
}
|
|
1135
1144
|
body = bodyStream.stream
|
|
1136
1145
|
contentLength = bodyStream.length
|
|
1137
|
-
} else if (util.isBlobLike(body) && request.contentType == null
|
|
1138
|
-
|
|
1146
|
+
} else if (util.isBlobLike(body) && request.contentType == null) {
|
|
1147
|
+
const contentType = body.type
|
|
1148
|
+
if (contentType) {
|
|
1149
|
+
const contentTypeValue = `${contentType}`
|
|
1150
|
+
if (!util.isValidHeaderValue(contentTypeValue)) {
|
|
1151
|
+
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
|
|
1152
|
+
return false
|
|
1153
|
+
}
|
|
1154
|
+
headers.push('content-type', contentTypeValue)
|
|
1155
|
+
}
|
|
1139
1156
|
}
|
|
1140
1157
|
|
|
1141
1158
|
if (body && typeof body.read === 'function') {
|
|
@@ -8,7 +8,9 @@ const {
|
|
|
8
8
|
RequestAbortedError,
|
|
9
9
|
SocketError,
|
|
10
10
|
InformationalError,
|
|
11
|
-
InvalidArgumentError
|
|
11
|
+
InvalidArgumentError,
|
|
12
|
+
HeadersTimeoutError,
|
|
13
|
+
BodyTimeoutError
|
|
12
14
|
} = require('../core/errors.js')
|
|
13
15
|
const {
|
|
14
16
|
kUrl,
|
|
@@ -33,6 +35,7 @@ const {
|
|
|
33
35
|
kHTTPContext,
|
|
34
36
|
kClosed,
|
|
35
37
|
kBodyTimeout,
|
|
38
|
+
kHeadersTimeout,
|
|
36
39
|
kEnableConnectProtocol,
|
|
37
40
|
kRemoteSettings,
|
|
38
41
|
kHTTP2Stream,
|
|
@@ -219,7 +222,11 @@ function resumeH2 (client) {
|
|
|
219
222
|
const socket = client[kSocket]
|
|
220
223
|
|
|
221
224
|
if (socket?.destroyed === false) {
|
|
222
|
-
|
|
225
|
+
// Only let the process exit when there is genuinely nothing outstanding.
|
|
226
|
+
// Unreffing because the peer advertised MAX_CONCURRENT_STREAMS = 0 left
|
|
227
|
+
// queued requests with nothing holding the event loop open, so the process
|
|
228
|
+
// could exit with status 0 while an awaited request never settled.
|
|
229
|
+
if (client[kSize] === 0) {
|
|
223
230
|
socket.unref()
|
|
224
231
|
client[kHTTP2Session].unref()
|
|
225
232
|
} else {
|
|
@@ -314,6 +321,36 @@ function onHttp2SessionEnd () {
|
|
|
314
321
|
* @this {import('http2').ClientHttp2Session}
|
|
315
322
|
* @param {number} errorCode
|
|
316
323
|
*/
|
|
324
|
+
// Backport of #5410 and #5569. HTTP/2 multiplexes, so requests complete out of
|
|
325
|
+
// order; advancing kRunningIdx blindly retired whichever request happened to
|
|
326
|
+
// sit at the head instead of the one that actually finished, which both lost
|
|
327
|
+
// requests and left phantom running slots behind.
|
|
328
|
+
function completeRequest (client, request, resetPendingIdx = false) {
|
|
329
|
+
const queue = client[kQueue]
|
|
330
|
+
const runningIdx = client[kRunningIdx]
|
|
331
|
+
|
|
332
|
+
// In-order completion: clear the request and advance without splicing.
|
|
333
|
+
// The client's resume loop compacts cleared slots once the index grows.
|
|
334
|
+
if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) {
|
|
335
|
+
queue[runningIdx] = null
|
|
336
|
+
client[kRunningIdx] = runningIdx + 1
|
|
337
|
+
return
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const index = queue.indexOf(request, runningIdx)
|
|
341
|
+
|
|
342
|
+
if (index === -1 || index >= client[kPendingIdx]) {
|
|
343
|
+
return
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
queue.splice(index, 1)
|
|
347
|
+
client[kPendingIdx]--
|
|
348
|
+
|
|
349
|
+
if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) {
|
|
350
|
+
client[kPendingIdx] = client[kRunningIdx]
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
317
354
|
function onHttp2SessionGoAway (errorCode) {
|
|
318
355
|
// TODO(mcollina): Verify if GOAWAY implements the spec correctly:
|
|
319
356
|
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
|
|
@@ -335,7 +372,9 @@ function onHttp2SessionGoAway (errorCode) {
|
|
|
335
372
|
if (client[kRunningIdx] < client[kQueue].length) {
|
|
336
373
|
const request = client[kQueue][client[kRunningIdx]]
|
|
337
374
|
client[kQueue][client[kRunningIdx]++] = null
|
|
338
|
-
|
|
375
|
+
if (request != null) {
|
|
376
|
+
util.errorRequest(client, request, err)
|
|
377
|
+
}
|
|
339
378
|
client[kPendingIdx] = client[kRunningIdx]
|
|
340
379
|
}
|
|
341
380
|
|
|
@@ -368,7 +407,9 @@ function onHttp2SessionClose () {
|
|
|
368
407
|
const requests = client[kQueue].splice(client[kRunningIdx])
|
|
369
408
|
for (let i = 0; i < requests.length; i++) {
|
|
370
409
|
const request = requests[i]
|
|
371
|
-
|
|
410
|
+
if (request != null) {
|
|
411
|
+
util.errorRequest(client, request, err)
|
|
412
|
+
}
|
|
372
413
|
}
|
|
373
414
|
}
|
|
374
415
|
}
|
|
@@ -416,7 +457,10 @@ function shouldSendContentLength (method) {
|
|
|
416
457
|
}
|
|
417
458
|
|
|
418
459
|
function writeH2 (client, request) {
|
|
419
|
-
|
|
460
|
+
// Time to the response headers, then time between body chunks. Using
|
|
461
|
+
// bodyTimeout for both made headersTimeout a no-op over HTTP/2.
|
|
462
|
+
const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout]
|
|
463
|
+
const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout]
|
|
420
464
|
const session = client[kHTTP2Session]
|
|
421
465
|
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
|
|
422
466
|
let { body } = request
|
|
@@ -483,6 +527,7 @@ function writeH2 (client, request) {
|
|
|
483
527
|
|
|
484
528
|
// We move the running index to the next request
|
|
485
529
|
client[kOnError](err)
|
|
530
|
+
completeRequest(client, request)
|
|
486
531
|
client[kResume]()
|
|
487
532
|
}
|
|
488
533
|
|
|
@@ -537,7 +582,7 @@ function writeH2 (client, request) {
|
|
|
537
582
|
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
538
583
|
|
|
539
584
|
++session[kOpenStreams]
|
|
540
|
-
client
|
|
585
|
+
completeRequest(client, request)
|
|
541
586
|
})
|
|
542
587
|
|
|
543
588
|
stream.on('error', () => {
|
|
@@ -554,7 +599,7 @@ function writeH2 (client, request) {
|
|
|
554
599
|
if (session[kOpenStreams] === 0) session.unref()
|
|
555
600
|
})
|
|
556
601
|
|
|
557
|
-
stream.setTimeout(
|
|
602
|
+
stream.setTimeout(headersTimeout)
|
|
558
603
|
return true
|
|
559
604
|
}
|
|
560
605
|
|
|
@@ -570,13 +615,14 @@ function writeH2 (client, request) {
|
|
|
570
615
|
|
|
571
616
|
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
|
|
572
617
|
++session[kOpenStreams]
|
|
573
|
-
client
|
|
618
|
+
completeRequest(client, request)
|
|
574
619
|
})
|
|
620
|
+
stream.on('error', abort)
|
|
575
621
|
stream.once('close', () => {
|
|
576
622
|
session[kOpenStreams] -= 1
|
|
577
623
|
if (session[kOpenStreams] === 0) session.unref()
|
|
578
624
|
})
|
|
579
|
-
stream.setTimeout(
|
|
625
|
+
stream.setTimeout(headersTimeout)
|
|
580
626
|
|
|
581
627
|
return true
|
|
582
628
|
}
|
|
@@ -677,7 +723,7 @@ function writeH2 (client, request) {
|
|
|
677
723
|
|
|
678
724
|
// Increment counter as we have new streams open
|
|
679
725
|
++session[kOpenStreams]
|
|
680
|
-
stream.setTimeout(
|
|
726
|
+
stream.setTimeout(headersTimeout)
|
|
681
727
|
|
|
682
728
|
// Track whether we received a response (headers)
|
|
683
729
|
let responseReceived = false
|
|
@@ -686,6 +732,7 @@ function writeH2 (client, request) {
|
|
|
686
732
|
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
|
|
687
733
|
request.onResponseStarted()
|
|
688
734
|
responseReceived = true
|
|
735
|
+
stream.setTimeout(bodyTimeout)
|
|
689
736
|
|
|
690
737
|
// Due to the stream nature, it is possible we face a race condition
|
|
691
738
|
// where the stream has been assigned, but the request has been aborted
|
|
@@ -720,14 +767,13 @@ function writeH2 (client, request) {
|
|
|
720
767
|
request.onComplete({})
|
|
721
768
|
}
|
|
722
769
|
|
|
723
|
-
client
|
|
770
|
+
completeRequest(client, request)
|
|
724
771
|
client[kResume]()
|
|
725
772
|
} else {
|
|
726
773
|
// Stream ended without receiving a response - this is an error
|
|
727
774
|
// (e.g., server destroyed the stream before sending headers)
|
|
728
775
|
abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
|
|
729
|
-
client
|
|
730
|
-
client[kPendingIdx] = client[kRunningIdx]
|
|
776
|
+
completeRequest(client, request, true)
|
|
731
777
|
client[kResume]()
|
|
732
778
|
}
|
|
733
779
|
})
|
|
@@ -738,6 +784,14 @@ function writeH2 (client, request) {
|
|
|
738
784
|
if (session[kOpenStreams] === 0) {
|
|
739
785
|
session.unref()
|
|
740
786
|
}
|
|
787
|
+
|
|
788
|
+
// A stream can close without ever emitting 'end' or 'error': a peer's
|
|
789
|
+
// RST_STREAM(CANCEL) received before the response is reported by Node as a
|
|
790
|
+
// bare 'close', and destroying the stream unenrolls its timeout, so no
|
|
791
|
+
// 'timeout' follows either. Nothing else would ever settle this request.
|
|
792
|
+
if (!request.aborted && !request.completed) {
|
|
793
|
+
abort(new InformationalError('HTTP/2: stream closed before the response was complete'))
|
|
794
|
+
}
|
|
741
795
|
})
|
|
742
796
|
|
|
743
797
|
stream.once('error', function (err) {
|
|
@@ -755,7 +809,9 @@ function writeH2 (client, request) {
|
|
|
755
809
|
})
|
|
756
810
|
|
|
757
811
|
stream.on('timeout', () => {
|
|
758
|
-
const err =
|
|
812
|
+
const err = responseReceived
|
|
813
|
+
? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`)
|
|
814
|
+
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
|
|
759
815
|
stream.removeAllListeners('data')
|
|
760
816
|
session[kOpenStreams] -= 1
|
|
761
817
|
|
package/lib/dispatcher/client.js
CHANGED
|
@@ -374,7 +374,9 @@ class Client extends DispatcherBase {
|
|
|
374
374
|
const requests = this[kQueue].splice(this[kPendingIdx])
|
|
375
375
|
for (let i = 0; i < requests.length; i++) {
|
|
376
376
|
const request = requests[i]
|
|
377
|
-
|
|
377
|
+
if (request != null) {
|
|
378
|
+
util.errorRequest(this, request, err)
|
|
379
|
+
}
|
|
378
380
|
}
|
|
379
381
|
|
|
380
382
|
const callback = () => {
|
|
@@ -413,7 +415,9 @@ function onError (client, err) {
|
|
|
413
415
|
|
|
414
416
|
for (let i = 0; i < requests.length; i++) {
|
|
415
417
|
const request = requests[i]
|
|
416
|
-
|
|
418
|
+
if (request != null) {
|
|
419
|
+
util.errorRequest(client, request, err)
|
|
420
|
+
}
|
|
417
421
|
}
|
|
418
422
|
assert(client[kSize] === 0)
|
|
419
423
|
}
|