Skip to content

Commit 6917d07

Browse files
nodejs-github-botaduh95
authored andcommitted
deps: update undici to 7.29.1
PR-URL: #65789 Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Filip Skokan <panva.ip@gmail.com> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent 6da9337 commit 6917d07

19 files changed

Lines changed: 2013 additions & 1098 deletions

deps/undici/src/lib/dispatcher/balanced-pool.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ function defaultFactory (origin, opts) {
4949
}
5050

5151
class BalancedPool extends PoolBase {
52-
constructor (upstreams = [], { factory = defaultFactory, ...opts } = {}) {
52+
constructor (upstreams = [], { factory = defaultFactory, connect, tls, ...opts } = {}) {
5353
if (typeof factory !== 'function') {
5454
throw new InvalidArgumentError('factory must be a function.')
5555
}
5656

5757
super(opts)
5858

59-
this[kOptions] = { ...util.deepClone(opts) }
59+
if (connect && typeof connect !== 'function') connect = { ...connect }
60+
if (tls && typeof tls !== 'function') tls = { ...tls }
61+
this[kOptions] = { ...util.deepClone(opts), connect, tls }
6062
this[kOptions].interceptors = opts.interceptors
6163
? { ...opts.interceptors }
6264
: undefined

deps/undici/src/lib/dispatcher/client-h1.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1012,7 +1012,7 @@ function onSocketClose () {
10121012

10131013
function clearIdleSocketValidation (socket) {
10141014
if (socket[kIdleSocketValidationTimeout]) {
1015-
clearTimeout(socket[kIdleSocketValidationTimeout])
1015+
clearImmediate(socket[kIdleSocketValidationTimeout])
10161016
socket[kIdleSocketValidationTimeout] = null
10171017
}
10181018

@@ -1021,15 +1021,23 @@ function clearIdleSocketValidation (socket) {
10211021

10221022
function scheduleIdleSocketValidation (client, socket) {
10231023
socket[kIdleSocketValidation] = 1
1024-
socket[kIdleSocketValidationTimeout] = setTimeout(() => {
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(() => {
10251034
socket[kIdleSocketValidationTimeout] = null
10261035
socket[kIdleSocketValidation] = 2
10271036

10281037
if (client[kSocket] === socket && !socket.destroyed) {
10291038
client[kResume]()
10301039
}
1031-
}, 0)
1032-
socket[kIdleSocketValidationTimeout].unref?.()
1040+
})
10331041
}
10341042

10351043
/**

deps/undici/src/lib/dispatcher/client-h2.js

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ const {
88
RequestAbortedError,
99
SocketError,
1010
InformationalError,
11-
InvalidArgumentError
11+
InvalidArgumentError,
12+
HeadersTimeoutError,
13+
BodyTimeoutError
1214
} = require('../core/errors.js')
1315
const {
1416
kUrl,
@@ -33,6 +35,7 @@ const {
3335
kHTTPContext,
3436
kClosed,
3537
kBodyTimeout,
38+
kHeadersTimeout,
3639
kEnableConnectProtocol,
3740
kRemoteSettings,
3841
kHTTP2Stream,
@@ -219,7 +222,11 @@ function resumeH2 (client) {
219222
const socket = client[kSocket]
220223

221224
if (socket?.destroyed === false) {
222-
if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) {
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) {
223230
socket.unref()
224231
client[kHTTP2Session].unref()
225232
} else {
@@ -314,6 +321,36 @@ function onHttp2SessionEnd () {
314321
* @this {import('http2').ClientHttp2Session}
315322
* @param {number} errorCode
316323
*/
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+
317354
function onHttp2SessionGoAway (errorCode) {
318355
// TODO(mcollina): Verify if GOAWAY implements the spec correctly:
319356
// https://datatracker.ietf.org/doc/html/rfc7540#section-6.8
@@ -335,7 +372,9 @@ function onHttp2SessionGoAway (errorCode) {
335372
if (client[kRunningIdx] < client[kQueue].length) {
336373
const request = client[kQueue][client[kRunningIdx]]
337374
client[kQueue][client[kRunningIdx]++] = null
338-
util.errorRequest(client, request, err)
375+
if (request != null) {
376+
util.errorRequest(client, request, err)
377+
}
339378
client[kPendingIdx] = client[kRunningIdx]
340379
}
341380

@@ -368,7 +407,9 @@ function onHttp2SessionClose () {
368407
const requests = client[kQueue].splice(client[kRunningIdx])
369408
for (let i = 0; i < requests.length; i++) {
370409
const request = requests[i]
371-
util.errorRequest(client, request, err)
410+
if (request != null) {
411+
util.errorRequest(client, request, err)
412+
}
372413
}
373414
}
374415
}
@@ -416,7 +457,10 @@ function shouldSendContentLength (method) {
416457
}
417458

418459
function writeH2 (client, request) {
419-
const requestTimeout = request.bodyTimeout ?? client[kBodyTimeout]
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]
420464
const session = client[kHTTP2Session]
421465
const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request
422466
let { body } = request
@@ -483,6 +527,7 @@ function writeH2 (client, request) {
483527

484528
// We move the running index to the next request
485529
client[kOnError](err)
530+
completeRequest(client, request)
486531
client[kResume]()
487532
}
488533

@@ -537,7 +582,7 @@ function writeH2 (client, request) {
537582
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
538583

539584
++session[kOpenStreams]
540-
client[kQueue][client[kRunningIdx]++] = null
585+
completeRequest(client, request)
541586
})
542587

543588
stream.on('error', () => {
@@ -554,7 +599,7 @@ function writeH2 (client, request) {
554599
if (session[kOpenStreams] === 0) session.unref()
555600
})
556601

557-
stream.setTimeout(requestTimeout)
602+
stream.setTimeout(headersTimeout)
558603
return true
559604
}
560605

@@ -570,13 +615,14 @@ function writeH2 (client, request) {
570615

571616
request.onUpgrade(statusCode, parseH2Headers(realHeaders), stream)
572617
++session[kOpenStreams]
573-
client[kQueue][client[kRunningIdx]++] = null
618+
completeRequest(client, request)
574619
})
620+
stream.on('error', abort)
575621
stream.once('close', () => {
576622
session[kOpenStreams] -= 1
577623
if (session[kOpenStreams] === 0) session.unref()
578624
})
579-
stream.setTimeout(requestTimeout)
625+
stream.setTimeout(headersTimeout)
580626

581627
return true
582628
}
@@ -677,7 +723,7 @@ function writeH2 (client, request) {
677723

678724
// Increment counter as we have new streams open
679725
++session[kOpenStreams]
680-
stream.setTimeout(requestTimeout)
726+
stream.setTimeout(headersTimeout)
681727

682728
// Track whether we received a response (headers)
683729
let responseReceived = false
@@ -686,6 +732,7 @@ function writeH2 (client, request) {
686732
const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers
687733
request.onResponseStarted()
688734
responseReceived = true
735+
stream.setTimeout(bodyTimeout)
689736

690737
// Due to the stream nature, it is possible we face a race condition
691738
// where the stream has been assigned, but the request has been aborted
@@ -720,14 +767,13 @@ function writeH2 (client, request) {
720767
request.onComplete({})
721768
}
722769

723-
client[kQueue][client[kRunningIdx]++] = null
770+
completeRequest(client, request)
724771
client[kResume]()
725772
} else {
726773
// Stream ended without receiving a response - this is an error
727774
// (e.g., server destroyed the stream before sending headers)
728775
abort(new InformationalError('HTTP/2: stream half-closed (remote)'))
729-
client[kQueue][client[kRunningIdx]++] = null
730-
client[kPendingIdx] = client[kRunningIdx]
776+
completeRequest(client, request, true)
731777
client[kResume]()
732778
}
733779
})
@@ -738,6 +784,14 @@ function writeH2 (client, request) {
738784
if (session[kOpenStreams] === 0) {
739785
session.unref()
740786
}
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+
}
741795
})
742796

743797
stream.once('error', function (err) {
@@ -755,7 +809,9 @@ function writeH2 (client, request) {
755809
})
756810

757811
stream.on('timeout', () => {
758-
const err = new InformationalError(`HTTP/2: "stream timeout after ${requestTimeout}"`)
812+
const err = responseReceived
813+
? new BodyTimeoutError(`HTTP/2: "body timeout after ${bodyTimeout}"`)
814+
: new HeadersTimeoutError(`HTTP/2: "headers timeout after ${headersTimeout}"`)
759815
stream.removeAllListeners('data')
760816
session[kOpenStreams] -= 1
761817

deps/undici/src/lib/dispatcher/client.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,9 @@ class Client extends DispatcherBase {
374374
const requests = this[kQueue].splice(this[kPendingIdx])
375375
for (let i = 0; i < requests.length; i++) {
376376
const request = requests[i]
377-
util.errorRequest(this, request, err)
377+
if (request != null) {
378+
util.errorRequest(this, request, err)
379+
}
378380
}
379381

380382
const callback = () => {
@@ -413,7 +415,9 @@ function onError (client, err) {
413415

414416
for (let i = 0; i < requests.length; i++) {
415417
const request = requests[i]
416-
util.errorRequest(client, request, err)
418+
if (request != null) {
419+
util.errorRequest(client, request, err)
420+
}
417421
}
418422
assert(client[kSize] === 0)
419423
}

deps/undici/src/lib/handler/cache-handler.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,13 @@ class CacheHandler {
207207
}
208208

209209
const cacheControlHeader = resHeaders['cache-control']
210+
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
211+
212+
if (revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives)) {
213+
deleteCachedValue(this.#store, this.#cacheKey)
214+
return downstreamOnHeaders()
215+
}
216+
210217
const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode)
211218
if (
212219
!cacheControlHeader &&
@@ -223,8 +230,7 @@ class CacheHandler {
223230
return downstreamOnHeaders()
224231
}
225232

226-
const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {}
227-
if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
233+
if (!canCacheResponse(this.#cacheType, this.#cacheKey.method, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) {
228234
if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) {
229235
deleteCachedValue(this.#store, this.#cacheKey)
230236
}
@@ -465,20 +471,27 @@ function deleteCachedValueIfNotModified (statusCode, store, cacheKey) {
465471
*/
466472
function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) {
467473
return cacheControlDirectives['no-store'] === true ||
468-
(cacheType === 'shared' && cacheControlDirectives.private === true) ||
474+
(cacheType === 'shared' && (
475+
cacheControlDirectives.private === true ||
476+
Object.hasOwn(resHeaders, 'set-cookie')
477+
)) ||
469478
(resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false)
470479
}
471480

472481
/**
473482
* @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen
474483
*
475484
* @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType
485+
* @param {string} method
476486
* @param {number} statusCode
477487
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders
478488
* @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives
479489
* @param {import('../../types/header.d.ts').IncomingHttpHeaders} [reqHeaders]
480490
*/
481-
function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
491+
function canCacheResponse (cacheType, method, statusCode, resHeaders, cacheControlDirectives, reqHeaders) {
492+
if (!arrayIncludes(util.safeHTTPMethods, method)) {
493+
return false
494+
}
482495
// Status code must be final and understood.
483496
if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) {
484497
return false
@@ -499,7 +512,10 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect
499512
return false
500513
}
501514

502-
if (cacheType === 'shared' && cacheControlDirectives.private === true) {
515+
if (cacheType === 'shared' && (
516+
cacheControlDirectives.private === true ||
517+
Object.hasOwn(resHeaders, 'set-cookie')
518+
)) {
503519
return false
504520
}
505521

0 commit comments

Comments
 (0)