diff --git a/dist/index.js b/dist/index.js index 398ad58..f3632b1 100644 --- a/dist/index.js +++ b/dist/index.js @@ -2139,7 +2139,7 @@ exports.getProxyAgentDispatcher = getProxyAgentDispatcher; exports.getProxyFetch = getProxyFetch; exports.getApiBaseUrl = getApiBaseUrl; const httpClient = __importStar(__nccwpck_require__(7708)); -const undici_1 = __nccwpck_require__(8216); +const undici_1 = __nccwpck_require__(6552); function getAuthString(token, options) { if (!token && !options.auth) { throw new Error('Parameter token or opts.auth is required'); @@ -2391,7 +2391,7 @@ const http = __importStar(__nccwpck_require__(8611)); const https = __importStar(__nccwpck_require__(5692)); const pm = __importStar(__nccwpck_require__(8188)); const tunnel = __importStar(__nccwpck_require__(329)); -const undici_1 = __nccwpck_require__(8216); +const undici_1 = __nccwpck_require__(6552); var HttpCodes; (function (HttpCodes) { HttpCodes[HttpCodes["OK"] = 200] = "OK"; @@ -4010,39 +4010,39 @@ exports.debug = debug; // for test /***/ }), -/***/ 8216: +/***/ 6552: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const Client = __nccwpck_require__(125) -const Dispatcher = __nccwpck_require__(1419) -const Pool = __nccwpck_require__(1276) -const BalancedPool = __nccwpck_require__(1277) -const RoundRobinPool = __nccwpck_require__(6520) -const Agent = __nccwpck_require__(821) -const Dispatcher1Wrapper = __nccwpck_require__(8106) -const ProxyAgent = __nccwpck_require__(3256) -const Socks5ProxyAgent = __nccwpck_require__(5567) -const EnvHttpProxyAgent = __nccwpck_require__(3673) -const RetryAgent = __nccwpck_require__(7418) -const H2CClient = __nccwpck_require__(5719) -const errors = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) +const Client = __nccwpck_require__(3869) +const Dispatcher = __nccwpck_require__(7595) +const Pool = __nccwpck_require__(1324) +const BalancedPool = __nccwpck_require__(8429) +const RoundRobinPool = __nccwpck_require__(7512) +const Agent = __nccwpck_require__(5733) +const Dispatcher1Wrapper = __nccwpck_require__(2474) +const ProxyAgent = __nccwpck_require__(5131) +const Socks5ProxyAgent = __nccwpck_require__(1855) +const EnvHttpProxyAgent = __nccwpck_require__(4505) +const RetryAgent = __nccwpck_require__(1674) +const H2CClient = __nccwpck_require__(1895) +const errors = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) const { InvalidArgumentError } = errors -const api = __nccwpck_require__(5247) -const buildConnector = __nccwpck_require__(9512) -const MockClient = __nccwpck_require__(1421) -const { MockCallHistory, MockCallHistoryLog } = __nccwpck_require__(8535) -const MockAgent = __nccwpck_require__(3253) -const MockPool = __nccwpck_require__(252) -const SnapshotAgent = __nccwpck_require__(639) -const mockErrors = __nccwpck_require__(4805) -const RetryHandler = __nccwpck_require__(5184) -const { getGlobalDispatcher, setGlobalDispatcher } = __nccwpck_require__(6013) -const DecoratorHandler = __nccwpck_require__(5683) -const RedirectHandler = __nccwpck_require__(474) +const api = __nccwpck_require__(5423) +const buildConnector = __nccwpck_require__(7432) +const MockClient = __nccwpck_require__(6333) +const { MockCallHistory, MockCallHistoryLog } = __nccwpck_require__(2791) +const MockAgent = __nccwpck_require__(3301) +const MockPool = __nccwpck_require__(380) +const SnapshotAgent = __nccwpck_require__(5903) +const mockErrors = __nccwpck_require__(6069) +const RetryHandler = __nccwpck_require__(1360) +const { getGlobalDispatcher, setGlobalDispatcher } = __nccwpck_require__(7949) +const DecoratorHandler = __nccwpck_require__(5043) +const RedirectHandler = __nccwpck_require__(1274) Object.assign(Dispatcher.prototype, api) @@ -4063,21 +4063,21 @@ module.exports.RetryHandler = RetryHandler module.exports.DecoratorHandler = DecoratorHandler module.exports.RedirectHandler = RedirectHandler module.exports.interceptors = { - redirect: __nccwpck_require__(3074), - responseError: __nccwpck_require__(9870), - retry: __nccwpck_require__(8450), - dump: __nccwpck_require__(1684), - dns: __nccwpck_require__(5955), - cache: __nccwpck_require__(6590), - decompress: __nccwpck_require__(6661), - deduplicate: __nccwpck_require__(1824) + redirect: __nccwpck_require__(4546), + responseError: __nccwpck_require__(9534), + retry: __nccwpck_require__(2194), + dump: __nccwpck_require__(6596), + dns: __nccwpck_require__(6003), + cache: __nccwpck_require__(334), + decompress: __nccwpck_require__(917), + deduplicate: __nccwpck_require__(8064) } module.exports.cacheStores = { - MemoryCacheStore: __nccwpck_require__(1) + MemoryCacheStore: __nccwpck_require__(1915) } -const SqliteCacheStore = __nccwpck_require__(8090) +const SqliteCacheStore = __nccwpck_require__(8890) module.exports.cacheStores.SqliteCacheStore = SqliteCacheStore module.exports.buildConnector = buildConnector @@ -4139,7 +4139,7 @@ function makeDispatcher (fn) { module.exports.setGlobalDispatcher = setGlobalDispatcher module.exports.getGlobalDispatcher = getGlobalDispatcher -const fetchImpl = (__nccwpck_require__(2726).fetch) +const fetchImpl = (__nccwpck_require__(6422).fetch) // Capture __filename at module load time for stack trace augmentation. // This may be undefined when bundled in environments like Node.js internals. @@ -4179,22 +4179,22 @@ module.exports.fetch = function fetch (init, options = undefined) { throw err }) } -module.exports.Headers = __nccwpck_require__(8732).Headers -module.exports.Response = __nccwpck_require__(5459).Response -module.exports.Request = __nccwpck_require__(6423).Request -module.exports.FormData = __nccwpck_require__(1726).FormData +module.exports.Headers = __nccwpck_require__(2476).Headers +module.exports.Response = __nccwpck_require__(3171).Response +module.exports.Request = __nccwpck_require__(167).Request +module.exports.FormData = __nccwpck_require__(9438).FormData -const { setGlobalOrigin, getGlobalOrigin } = __nccwpck_require__(9083) +const { setGlobalOrigin, getGlobalOrigin } = __nccwpck_require__(3995) module.exports.setGlobalOrigin = setGlobalOrigin module.exports.getGlobalOrigin = getGlobalOrigin -const { CacheStorage } = __nccwpck_require__(3221) -const { kConstruct } = __nccwpck_require__(451) +const { CacheStorage } = __nccwpck_require__(7477) +const { kConstruct } = __nccwpck_require__(2019) module.exports.caches = new CacheStorage(kConstruct) -const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = __nccwpck_require__(9853) +const { deleteCookie, getCookies, getSetCookies, setCookie, parseCookie } = __nccwpck_require__(3597) module.exports.deleteCookie = deleteCookie module.exports.getCookies = getCookies @@ -4202,21 +4202,21 @@ module.exports.getSetCookies = getSetCookies module.exports.setCookie = setCookie module.exports.parseCookie = parseCookie -const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(132) +const { parseMIMEType, serializeAMimeType } = __nccwpck_require__(7844) module.exports.parseMIMEType = parseMIMEType module.exports.serializeAMimeType = serializeAMimeType -const { CloseEvent, ErrorEvent, MessageEvent } = __nccwpck_require__(6284) -const { WebSocket, ping } = __nccwpck_require__(2054) +const { CloseEvent, ErrorEvent, MessageEvent } = __nccwpck_require__(7756) +const { WebSocket, ping } = __nccwpck_require__(1942) module.exports.WebSocket = WebSocket module.exports.CloseEvent = CloseEvent module.exports.ErrorEvent = ErrorEvent module.exports.MessageEvent = MessageEvent module.exports.ping = ping -module.exports.WebSocketStream = __nccwpck_require__(4177).WebSocketStream -module.exports.WebSocketError = __nccwpck_require__(4399).WebSocketError +module.exports.WebSocketStream = __nccwpck_require__(4961).WebSocketStream +module.exports.WebSocketError = __nccwpck_require__(4879).WebSocketError module.exports.request = makeDispatcher(api.request) module.exports.stream = makeDispatcher(api.stream) @@ -4232,7 +4232,7 @@ module.exports.MockAgent = MockAgent module.exports.SnapshotAgent = SnapshotAgent module.exports.mockErrors = mockErrors -const { EventSource } = __nccwpck_require__(1294) +const { EventSource } = __nccwpck_require__(2286) module.exports.EventSource = EventSource @@ -4254,14 +4254,14 @@ module.exports.install = install /***/ }), -/***/ 2758: +/***/ 7670: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { addAbortListener } = __nccwpck_require__(648) -const { RequestAbortedError } = __nccwpck_require__(3291) +const { addAbortListener } = __nccwpck_require__(824) +const { RequestAbortedError } = __nccwpck_require__(4715) const kListener = Symbol('kListener') const kSignal = Symbol('kSignal') @@ -4321,7 +4321,7 @@ module.exports = { /***/ }), -/***/ 3116: +/***/ 6812: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -4329,9 +4329,9 @@ module.exports = { const assert = __nccwpck_require__(4589) const { AsyncResource } = __nccwpck_require__(6698) -const { InvalidArgumentError, SocketError } = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) -const { addSignal, removeSignal } = __nccwpck_require__(2758) +const { InvalidArgumentError, SocketError } = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) +const { addSignal, removeSignal } = __nccwpck_require__(7670) class ConnectHandler extends AsyncResource { constructor (opts, callback) { @@ -4441,7 +4441,7 @@ module.exports = connect /***/ }), -/***/ 1942: +/***/ 6854: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -4458,10 +4458,10 @@ const { InvalidArgumentError, InvalidReturnValueError, RequestAbortedError -} = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) -const { kBodyUsed } = __nccwpck_require__(451) -const { addSignal, removeSignal } = __nccwpck_require__(2758) +} = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) +const { kBodyUsed } = __nccwpck_require__(2019) +const { addSignal, removeSignal } = __nccwpck_require__(7670) function noop () {} @@ -4714,7 +4714,7 @@ module.exports = pipeline /***/ }), -/***/ 6195: +/***/ 6243: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -4722,9 +4722,9 @@ module.exports = pipeline const assert = __nccwpck_require__(4589) const { AsyncResource } = __nccwpck_require__(6698) -const { Readable } = __nccwpck_require__(5071) -const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) +const { Readable } = __nccwpck_require__(6639) +const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) function noop () {} @@ -4786,7 +4786,13 @@ class RequestHandler extends AsyncResource { this.removeAbortListener = util.addAbortListener(signal, () => { this.reason = signal.reason ?? new RequestAbortedError() if (this.res) { - util.destroy(this.res.on('error', noop), this.reason) + // Null the reference before destroying, mirroring onResponseError, so + // that chunks flushed after the abort (e.g. an async decompressor + // flush) are dropped by the `!this.res` guard in onResponseData + // instead of being pushed into the torn-down stream. + const res = this.res + this.res = null + util.destroy(res.on('error', noop), this.reason) } else if (this.abort) { this.abort(this.reason) } @@ -4960,7 +4966,7 @@ module.exports.RequestHandler = RequestHandler /***/ }), -/***/ 6848: +/***/ 6976: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -4968,9 +4974,9 @@ module.exports.RequestHandler = RequestHandler const assert = __nccwpck_require__(4589) const { AsyncResource } = __nccwpck_require__(6698) -const { InvalidArgumentError, InvalidReturnValueError } = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) -const { addSignal, removeSignal } = __nccwpck_require__(2758) +const { InvalidArgumentError, InvalidReturnValueError } = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) +const { addSignal, removeSignal } = __nccwpck_require__(7670) function noop () {} @@ -5238,18 +5244,18 @@ module.exports = stream /***/ }), -/***/ 5266: +/***/ 8962: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { InvalidArgumentError, SocketError } = __nccwpck_require__(3291) +const { InvalidArgumentError, SocketError } = __nccwpck_require__(4715) const { AsyncResource } = __nccwpck_require__(6698) const assert = __nccwpck_require__(4589) -const util = __nccwpck_require__(648) -const { kHTTP2Stream } = __nccwpck_require__(451) -const { addSignal, removeSignal } = __nccwpck_require__(2758) +const util = __nccwpck_require__(824) +const { kHTTP2Stream } = __nccwpck_require__(2019) +const { addSignal, removeSignal } = __nccwpck_require__(7670) class UpgradeHandler extends AsyncResource { constructor (opts, callback) { @@ -5367,22 +5373,22 @@ module.exports = upgrade /***/ }), -/***/ 5247: +/***/ 5423: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -module.exports.request = __nccwpck_require__(6195) -module.exports.stream = __nccwpck_require__(6848) -module.exports.pipeline = __nccwpck_require__(1942) -module.exports.upgrade = __nccwpck_require__(5266) -module.exports.connect = __nccwpck_require__(3116) +module.exports.request = __nccwpck_require__(6243) +module.exports.stream = __nccwpck_require__(6976) +module.exports.pipeline = __nccwpck_require__(6854) +module.exports.upgrade = __nccwpck_require__(8962) +module.exports.connect = __nccwpck_require__(6812) /***/ }), -/***/ 5071: +/***/ 6639: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -5391,9 +5397,9 @@ module.exports.connect = __nccwpck_require__(3116) const assert = __nccwpck_require__(4589) const { addAbortListener } = __nccwpck_require__(8474) const { Readable } = __nccwpck_require__(7075) -const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) -const { ReadableStreamFrom } = __nccwpck_require__(648) +const { RequestAbortedError, NotSupportedError, InvalidArgumentError, AbortError } = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) +const { ReadableStreamFrom } = __nccwpck_require__(824) const kConsume = Symbol('kConsume') const kReading = Symbol('kReading') @@ -5713,7 +5719,15 @@ class BodyReadable extends Readable { */ setEncoding (encoding) { if (Buffer.isEncoding(encoding)) { - this._readableState.encoding = encoding + // Delegate to Node.js Readable.setEncoding() which initializes a + // StringDecoder and re-encodes already-buffered chunks. This properly + // handles multi-byte sequences split at chunk boundaries for the + // for-await / on('data') paths. Without this, Node.js uses + // buf.toString(encoding) on each chunk, producing U+FFFD for split chars. + // + // The consume path (body.text(), body.json(), ...) copes with the + // decoded strings this leaves in state.buffer, see consumeStart(). + super.setEncoding(encoding) } return this } @@ -5833,14 +5847,29 @@ function consumeStart (consume) { } } + // If setEncoding() was called, state.buffer holds decoded strings, which + // consumePush() turns back into bytes. The trailing bytes of a multi-byte + // sequence split across a chunk boundary are not part of any of those + // strings, they are held inside the decoder until the rest arrives, so + // take them from there. + const decoder = state.decoder + if (decoder != null && decoder.lastNeed > 0) { + consumePush(consume, Buffer.from(decoder.lastChar.subarray(0, decoder.lastTotal - decoder.lastNeed))) + } + if (state.endEmitted) { - consumeEnd(this[kConsume], this._readableState.encoding) - } else { - consume.stream.on('end', function () { - consumeEnd(this[kConsume], this._readableState.encoding) - }) + // No `this` to read the consume off here: consumeStart is a free function, called from + // the queueMicrotask above. The callback below does have one, because the emitter passes + // the stream as its receiver. Returning matters too - consumeEnd() clears consume.stream, + // which the resume() below would then dereference. + consumeEnd(consume, state.encoding) + return } + consume.stream.on('end', function () { + consumeEnd(this[kConsume], this._readableState.encoding) + }) + consume.stream.resume() while (consume.stream.read() != null) { @@ -5930,10 +5959,22 @@ function consumeEnd (consume, encoding) { /** * @param {Consume} consume - * @param {Buffer} chunk + * @param {Buffer|string} chunk * @returns {void} */ function consumePush (consume, chunk) { + if (consume.body === null) { + return + } + + if (typeof chunk === 'string') { + // Buffered before the consume started, while an encoding was set. + // consume.length has to stay a byte count and chunksDecode()/chunksConcat() + // only work on bytes, so re-encode. A string's own length is in UTF-16 code + // units and Uint8Array.prototype.set() ignores a string argument entirely. + chunk = Buffer.from(chunk, consume.stream._readableState.encoding) + } + consume.length += chunk.length consume.body.push(chunk) } @@ -5971,7 +6012,7 @@ module.exports = { /***/ }), -/***/ 1: +/***/ 1915: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -5979,7 +6020,7 @@ module.exports = { const { Writable } = __nccwpck_require__(7075) const { EventEmitter } = __nccwpck_require__(8474) -const { assertCacheKey, assertCacheValue } = __nccwpck_require__(3283) +const { assertCacheKey, assertCacheValue } = __nccwpck_require__(2627) /** * @typedef {import('../../types/cache-interceptor.d.ts').default.CacheKey} CacheKey @@ -6195,17 +6236,62 @@ class MemoryCacheStore extends EventEmitter { } function findEntry (key, entries, now) { - return entries.find((entry) => ( - entry.deleteAt > now && - entry.method === key.method && - (entry.vary == null || Object.keys(entry.vary).every(headerName => { - if (entry.vary[headerName] === null) { - return key.headers[headerName] === undefined + for (let i = 0; i < entries.length; i++) { + const entry = entries[i] + if ( + entry.deleteAt > now && + entry.method === key.method && + varyMatches(key, entry) + ) { + return entry + } + } +} + +function varyMatches (key, entry) { + if (entry.vary == null) { + return true + } + + for (const headerName in entry.vary) { + if (Object.hasOwn(entry.vary, headerName) && !headerValueEquals(key.headers?.[headerName], entry.vary[headerName])) { + return false + } + } + + return true +} + +/** + * @param {string|string[]|null|undefined} lhs + * @param {string|string[]|null|undefined} rhs + * @returns {boolean} + */ +function headerValueEquals (lhs, rhs) { + if (lhs == null && rhs == null) { + return true + } + + if ((lhs == null && rhs != null) || + (lhs != null && rhs == null)) { + return false + } + + if (Array.isArray(lhs) && Array.isArray(rhs)) { + if (lhs.length !== rhs.length) { + return false + } + + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] !== rhs[i]) { + return false } + } - return entry.vary[headerName] === key.headers[headerName] - })) - )) + return true + } + + return lhs === rhs } module.exports = MemoryCacheStore @@ -6213,14 +6299,14 @@ module.exports = MemoryCacheStore /***/ }), -/***/ 8090: +/***/ 8890: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const { Writable } = __nccwpck_require__(7075) -const { assertCacheKey, assertCacheValue } = __nccwpck_require__(3283) +const { assertCacheKey, assertCacheValue } = __nccwpck_require__(2627) let DatabaseSync @@ -6675,7 +6761,13 @@ function headerValueEquals (lhs, rhs) { return false } - return lhs.every((x, i) => x === rhs[i]) + for (let i = 0; i < lhs.length; i++) { + if (lhs[i] !== rhs[i]) { + return false + } + } + + return true } return lhs === rhs @@ -6684,7 +6776,7 @@ function headerValueEquals (lhs, rhs) { /***/ }), -/***/ 9512: +/***/ 7432: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -6692,8 +6784,8 @@ function headerValueEquals (lhs, rhs) { const net = __nccwpck_require__(7030) const assert = __nccwpck_require__(4589) -const util = __nccwpck_require__(648) -const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(3291) +const util = __nccwpck_require__(824) +const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(4715) let tls // include tls conditionally since it is not always available @@ -6795,13 +6887,27 @@ function buildConnector ({ allowH2, preferH2, useH2c, maxCachedSessions, socketP port = port || 80 - socket = net.connect({ + const connectOptions = { highWaterMark: 64 * 1024, // Same as nodejs fs streams. ...options, localAddress, port, host: hostname - }) + } + + const family = net.isIP(hostname) + if (family !== 0 && servername && servername !== hostname) { + connectOptions.host = servername + connectOptions.lookup = (_hostname, lookupOptions, cb) => { + if (lookupOptions.all) { + cb(null, [{ address: hostname, family }]) + } else { + cb(null, hostname, family) + } + } + } + + socket = net.connect(connectOptions) if (useH2c === true) { socket.alpnProtocol = 'h2' } @@ -6870,7 +6976,7 @@ module.exports = buildConnector /***/ }), -/***/ 8967: +/***/ 5447: /***/ ((module) => { "use strict"; @@ -6998,7 +7104,7 @@ module.exports = { /***/ }), -/***/ 8054: +/***/ 2966: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -7233,7 +7339,7 @@ module.exports = { /***/ }), -/***/ 3291: +/***/ 4715: /***/ ((module) => { "use strict"; @@ -7642,6 +7748,25 @@ class SecureProxyConnectionError extends UndiciError { } } +const kProxyConnectionError = Symbol.for('undici.error.UND_ERR_PRX_CONN') +class ProxyConnectionError extends UndiciError { + constructor (cause, message, options = {}) { + super(message, { cause, ...options }) + this.name = 'ProxyConnectionError' + this.message = message || 'Proxy Connection failed' + this.code = 'UND_ERR_PRX_CONN' + this.cause = cause + } + + static [Symbol.hasInstance] (instance) { + return instance && instance[kProxyConnectionError] === true + } + + get [kProxyConnectionError] () { + return true + } +} + const kMaxOriginsReachedError = Symbol.for('undici.error.UND_ERR_MAX_ORIGINS_REACHED') class MaxOriginsReachedError extends UndiciError { constructor (message) { @@ -7710,6 +7835,7 @@ module.exports = { RequestRetryError, ResponseError, SecureProxyConnectionError, + ProxyConnectionError, MaxOriginsReachedError, Socks5ProxyError, MessageSizeExceededError @@ -7718,7 +7844,7 @@ module.exports = { /***/ }), -/***/ 7895: +/***/ 9463: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -7727,7 +7853,7 @@ module.exports = { const { InvalidArgumentError, NotSupportedError -} = __nccwpck_require__(3291) +} = __nccwpck_require__(4715) const assert = __nccwpck_require__(4589) const { isValidHTTPToken, @@ -7745,9 +7871,9 @@ const { getServerName, normalizedMethodRecords, getProtocolFromUrlString -} = __nccwpck_require__(648) -const { channels } = __nccwpck_require__(8054) -const { headerNameLowerCasedRecord } = __nccwpck_require__(8967) +} = __nccwpck_require__(824) +const { channels } = __nccwpck_require__(2966) +const { headerNameLowerCasedRecord } = __nccwpck_require__(5447) // Verifies that a given path is valid does not contain control chars \x00 to \x20 const invalidPathRegex = /[^\u0021-\u00ff]/ @@ -7897,7 +8023,7 @@ class Request { this.method = method - this.typeOfService = typeOfService ?? 0 + this.typeOfService = typeOfService this.abort = null @@ -7949,7 +8075,7 @@ class Request { this.protocol = getProtocolFromUrlString(origin) this.idempotent = idempotent == null - ? method === 'HEAD' || method === 'GET' + ? method === 'HEAD' || method === 'GET' || method === 'QUERY' : idempotent this.blocking = blocking ?? this.method !== 'HEAD' @@ -8196,7 +8322,13 @@ function processHeader (request, key, val) { } else if (typeof val[i] === 'object') { throw new InvalidArgumentError(`invalid ${key} header`) } else { - arr.push(`${val[i]}`) + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). + const str = `${val[i]}` + if (!isValidHeaderValue(str)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } + arr.push(str) } } val = arr @@ -8207,7 +8339,12 @@ function processHeader (request, key, val) { } else if (val === null) { val = '' } else { + // Coerce primitives (and reject unsafe coercions such as functions + // with a crafted toString/Symbol.toPrimitive). val = `${val}` + if (!isValidHeaderValue(val)) { + throw new InvalidArgumentError(`invalid ${key} header`) + } } if (headerName === 'host') { @@ -8261,7 +8398,7 @@ module.exports = Request /***/ }), -/***/ 6346: +/***/ 4058: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -8269,9 +8406,9 @@ module.exports = Request const { EventEmitter } = __nccwpck_require__(8474) const { Buffer } = __nccwpck_require__(4573) -const { InvalidArgumentError, Socks5ProxyError } = __nccwpck_require__(3291) +const { InvalidArgumentError, Socks5ProxyError } = __nccwpck_require__(4715) const { debuglog } = __nccwpck_require__(7975) -const { parseAddress } = __nccwpck_require__(5916) +const { parseAddress } = __nccwpck_require__(9660) const debug = debuglog('undici:socks5') const EMPTY_BUFFER = Buffer.alloc(0) @@ -8691,7 +8828,7 @@ module.exports = { /***/ }), -/***/ 5916: +/***/ 9660: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -8699,7 +8836,7 @@ module.exports = { const { Buffer } = __nccwpck_require__(4573) const net = __nccwpck_require__(7030) -const { InvalidArgumentError } = __nccwpck_require__(3291) +const { InvalidArgumentError } = __nccwpck_require__(4715) /** * Parse an address and determine its type @@ -8911,7 +9048,7 @@ module.exports = { /***/ }), -/***/ 451: +/***/ 2019: /***/ ((module) => { "use strict"; @@ -8973,6 +9110,7 @@ module.exports = { kCounter: Symbol('socket request counter'), kMaxResponseSize: Symbol('max response size'), kHTTP2Session: Symbol('http2Session'), + kHTTP2Options: Symbol('http2 options'), kHTTP2SessionState: Symbol('http2Session state'), kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), kConstruct: Symbol('constructable'), @@ -8995,7 +9133,7 @@ module.exports = { /***/ }), -/***/ 6048: +/***/ 6224: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -9004,7 +9142,7 @@ module.exports = { const { wellknownHeaderNames, headerNameLowerCasedRecord -} = __nccwpck_require__(8967) +} = __nccwpck_require__(5447) class TstNode { /** @type {any} */ @@ -9163,23 +9301,23 @@ module.exports = { /***/ }), -/***/ 648: +/***/ 824: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const assert = __nccwpck_require__(4589) -const { kDestroyed, kBodyUsed, kListeners, kBody } = __nccwpck_require__(451) +const { kDestroyed, kBodyUsed, kListeners, kBody } = __nccwpck_require__(2019) const { IncomingMessage } = __nccwpck_require__(7067) const stream = __nccwpck_require__(7075) const net = __nccwpck_require__(7030) const { stringify } = __nccwpck_require__(1792) const { EventEmitter: EE, addAbortListener: addAbortListenerNative } = __nccwpck_require__(8474) -const timers = __nccwpck_require__(8275) -const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(3291) -const { headerNameLowerCasedRecord } = __nccwpck_require__(8967) -const { tree } = __nccwpck_require__(6048) +const timers = __nccwpck_require__(6051) +const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(4715) +const { headerNameLowerCasedRecord } = __nccwpck_require__(5447) +const { tree } = __nccwpck_require__(6224) class BodyAsyncIterable { constructor (body) { @@ -9539,7 +9677,12 @@ function destroy (stream, err) { stream.socket = null } - stream.destroy(err) + try { + stream.destroy(err) + } catch { + // stream.destroy may throw on managed sockets (e.g., http2). + // Silently ignore — the socket lifecycle is handled by the subsystem. + } } else if (err) { queueMicrotask(() => { stream.emit('error', err) @@ -10088,11 +10231,32 @@ function onConnectTimeout (socket, opts) { destroy(socket, new ConnectTimeoutError(message)) } +let lastUrlString = null +let lastProtocol = null + /** * @param {string} urlString * @returns {string} */ function getProtocolFromUrlString (urlString) { + // Requests are typically dispatched against the same origin over and over, + // so cache the last (urlString, protocol) pair to skip re-parsing. + if (urlString === lastUrlString) { + return lastProtocol + } + + const protocol = getProtocolFromUrlStringSlow(urlString) + lastUrlString = urlString + lastProtocol = protocol + + return protocol +} + +/** + * @param {string} urlString + * @returns {string} + */ +function getProtocolFromUrlStringSlow (urlString) { if ( urlString[0] === 'h' && urlString[1] === 't' && @@ -10129,7 +10293,9 @@ const normalizedMethodRecordsBase = { post: 'POST', POST: 'POST', put: 'PUT', - PUT: 'PUT' + PUT: 'PUT', + query: 'QUERY', + QUERY: 'QUERY' } const normalizedMethodRecords = { @@ -10192,18 +10358,18 @@ module.exports = { /***/ }), -/***/ 821: +/***/ 5733: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { InvalidArgumentError, MaxOriginsReachedError } = __nccwpck_require__(3291) -const { kBusy, kClients, kConnected, kRunning, kClose, kDestroy, kDispatch, kUrl } = __nccwpck_require__(451) -const DispatcherBase = __nccwpck_require__(1049) -const Pool = __nccwpck_require__(1276) -const Client = __nccwpck_require__(125) -const util = __nccwpck_require__(648) +const { InvalidArgumentError, MaxOriginsReachedError } = __nccwpck_require__(4715) +const { kBusy, kClients, kConnected, kRunning, kClose, kDestroy, kDispatch, kUrl } = __nccwpck_require__(2019) +const DispatcherBase = __nccwpck_require__(713) +const Pool = __nccwpck_require__(1324) +const Client = __nccwpck_require__(3869) +const util = __nccwpck_require__(824) const kOnConnect = Symbol('onConnect') const kOnDisconnect = Symbol('onDisconnect') @@ -10305,15 +10471,15 @@ class Agent extends DispatcherBase { } let hasOrigin = false - for (const client of this[kClients].values()) { - if (client[kUrl].origin === dispatcher[kUrl].origin) { + for (const k of this[kClients].keys()) { + if (k === origin || k === `${origin}#http1-only`) { hasOrigin = true break } } if (!hasOrigin) { - this[kOrigins].delete(dispatcher[kUrl].origin) + this[kOrigins].delete(origin) } } @@ -10372,7 +10538,7 @@ module.exports = Agent /***/ }), -/***/ 1277: +/***/ 8429: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -10381,7 +10547,7 @@ module.exports = Agent const { BalancedPoolMissingUpstreamError, InvalidArgumentError -} = __nccwpck_require__(3291) +} = __nccwpck_require__(4715) const { PoolBase, kClients, @@ -10389,10 +10555,10 @@ const { kAddClient, kRemoveClient, kGetDispatcher -} = __nccwpck_require__(6488) -const Pool = __nccwpck_require__(1276) -const { kUrl } = __nccwpck_require__(451) -const util = __nccwpck_require__(648) +} = __nccwpck_require__(7960) +const Pool = __nccwpck_require__(1324) +const { kUrl } = __nccwpck_require__(2019) +const util = __nccwpck_require__(824) const kFactory = Symbol('factory') const kOptions = Symbol('options') @@ -10594,7 +10760,7 @@ module.exports = BalancedPool /***/ }), -/***/ 6629: +/***/ 8101: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -10603,13 +10769,14 @@ module.exports = BalancedPool /* global WebAssembly */ const assert = __nccwpck_require__(4589) -const util = __nccwpck_require__(648) -const { channels } = __nccwpck_require__(8054) -const timers = __nccwpck_require__(8275) +const util = __nccwpck_require__(824) +const { channels } = __nccwpck_require__(2966) +const timers = __nccwpck_require__(6051) const { RequestContentLengthMismatchError, ResponseContentLengthMismatchError, RequestAbortedError, + InvalidArgumentError, HeadersTimeoutError, HeadersOverflowError, SocketError, @@ -10617,7 +10784,7 @@ const { BodyTimeoutError, HTTPParserError, ResponseExceededMaxSizeError -} = __nccwpck_require__(3291) +} = __nccwpck_require__(4715) const { kUrl, kReset, @@ -10651,20 +10818,21 @@ const { kResume, kHTTPContext, kClosed -} = __nccwpck_require__(451) +} = __nccwpck_require__(2019) -const constants = __nccwpck_require__(6368) +const constants = __nccwpck_require__(1280) const EMPTY_BUF = Buffer.alloc(0) const FastBuffer = Buffer[Symbol.species] const removeAllListeners = util.removeAllListeners const kIdleSocketValidation = Symbol('kIdleSocketValidation') const kIdleSocketValidationTimeout = Symbol('kIdleSocketValidationTimeout') const kSocketUsed = Symbol('kSocketUsed') +const kTypeOfService = Symbol('kTypeOfService') let extractBody function lazyllhttp () { - const llhttpWasmData = process.env.JEST_WORKER_ID ? __nccwpck_require__(678) : undefined + const llhttpWasmData = process.env.JEST_WORKER_ID ? __nccwpck_require__(2038) : undefined let mod @@ -10679,7 +10847,7 @@ function lazyllhttp () { if (useWasmSIMD) { try { - mod = new WebAssembly.Module(__nccwpck_require__(2354)) + mod = new WebAssembly.Module(__nccwpck_require__(2242)) } catch { } } @@ -10689,7 +10857,7 @@ function lazyllhttp () { // being enabled, but the occurring of this other error // * https://github.com/emscripten-core/emscripten/issues/11495 // got me to remove that check to avoid breaking Node 12. - mod = new WebAssembly.Module(llhttpWasmData || __nccwpck_require__(678)) + mod = new WebAssembly.Module(llhttpWasmData || __nccwpck_require__(2038)) } return new WebAssembly.Instance(mod, { @@ -10977,6 +11145,29 @@ class Parser { const { llhttp } = this + // The peer closed the connection. If the body parser was paused by + // backpressure we must finish parsing before signalling EOF, otherwise + // llhttp_finish() would crash (it used to assert !paused) or report a + // half-parsed message. Backpressure is advisory here: onData keeps buffering + // delivered bytes into the response stream, so resume across pauses and + // drain whatever is still buffered on the socket. A Content-Length/chunked + // body reaches on_message_complete during execute(); an EOF-delimited body + // stays paused (its length is unknown) and is completed by llhttp_finish(). + if (this.paused) { + let data + do { + llhttp.llhttp_resume(this.ptr) + this.paused = false + data = this.socket.read() || EMPTY_BUF + this.execute(data) + } while (this.paused && data.length > 0) + + if (this.paused) { + llhttp.llhttp_resume(this.ptr) + this.paused = false + } + } + let ret try { @@ -11711,6 +11902,32 @@ function shouldSendContentLength (method) { return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' } +function setTypeOfService (socket, request) { + if (typeof socket.setTypeOfService !== 'function') { + return + } + + const typeOfService = request.typeOfService + + if (typeOfService === undefined) { + return + } + + const currentTypeOfService = socket[kTypeOfService] + + if (currentTypeOfService === typeOfService) { + return + } + + try { + socket.setTypeOfService(typeOfService) + socket[kTypeOfService] = typeOfService + } catch { + // QoS marking is best-effort. setTypeOfService() can throw synchronously on + // some platforms depending on socket state, but that must not abort the request. + } +} + /** * @param {import('./client.js')} client * @param {import('../core/request.js')} request @@ -11741,7 +11958,7 @@ function writeH1 (client, request) { if (util.isFormDataLike(body)) { if (!extractBody) { - extractBody = (__nccwpck_require__(9284).extractBody) + extractBody = (__nccwpck_require__(9412).extractBody) } const [bodyStream, contentType] = extractBody(body) @@ -11750,8 +11967,16 @@ function writeH1 (client, request) { } body = bodyStream.stream contentLength = bodyStream.length - } else if (util.isBlobLike(body) && request.contentType == null && body.type) { - headers.push('content-type', body.type) + } else if (util.isBlobLike(body) && request.contentType == null) { + const contentType = body.type + if (contentType) { + const contentTypeValue = `${contentType}` + if (!util.isValidHeaderValue(contentTypeValue)) { + util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header')) + return false + } + headers.push('content-type', contentTypeValue) + } } if (body && typeof body.read === 'function') { @@ -11842,9 +12067,7 @@ function writeH1 (client, request) { socket[kBlocking] = true } - if (socket.setTypeOfService) { - socket.setTypeOfService(request.typeOfService) - } + setTypeOfService(socket, request) let header = `${method} ${path} HTTP/1.1\r\n` @@ -12338,7 +12561,7 @@ module.exports = connectH1 /***/ }), -/***/ 6892: +/***/ 8364: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -12346,7 +12569,7 @@ module.exports = connectH1 const assert = __nccwpck_require__(4589) const { pipeline } = __nccwpck_require__(7075) -const util = __nccwpck_require__(648) +const util = __nccwpck_require__(824) const { RequestContentLengthMismatchError, RequestAbortedError, @@ -12355,7 +12578,7 @@ const { InvalidArgumentError, HeadersTimeoutError, BodyTimeoutError -} = __nccwpck_require__(3291) +} = __nccwpck_require__(4715) const { kUrl, kReset, @@ -12370,10 +12593,7 @@ const { kStrictContentLength, kOnError, kMaxConcurrentStreams, - kPingInterval, kHTTP2Session, - kHTTP2InitialWindowSize, - kHTTP2ConnectionWindowSize, kHostAuthority, kResume, kSize, @@ -12385,9 +12605,10 @@ const { kEnableConnectProtocol, kRemoteSettings, kHTTP2Stream, - kHTTP2SessionState -} = __nccwpck_require__(451) -const { channels } = __nccwpck_require__(8054) + kHTTP2SessionState, + kHTTP2Options +} = __nccwpck_require__(2019) +const { channels } = __nccwpck_require__(2966) const kOpenStreams = Symbol('open streams') const kRequestStreamId = Symbol('request stream id') @@ -12395,6 +12616,14 @@ const kRequestStream = Symbol('request stream') const kRequestStreamCleanup = Symbol('request stream cleanup') const kRequestStreamState = Symbol('request stream state') const kReceivedGoAway = Symbol('received goaway') +const kGoAwayReplayAttempts = Symbol('goaway replay attempts') +const kRefusedStreamRetry = Symbol('refused stream retry') + +// RFC 9113 section 8.7: a client SHOULD NOT automatically retry a request more +// than once. Without a budget a peer that keeps refusing turns one request into +// an unbounded connect/refuse/reconnect loop that never settles and starves the +// event loop. +const MAX_GOAWAY_REPLAY_ATTEMPTS = 1 let extractBody @@ -12498,13 +12727,24 @@ function requeueUnsentRequest (client, request) { } function completeRequest (client, request, resetPendingIdx = false) { - const index = client[kQueue].indexOf(request, client[kRunningIdx]) + const queue = client[kQueue] + const runningIdx = client[kRunningIdx] + + // In-order completion: clear the request and advance without splicing. + // The client's resume loop compacts cleared slots once the index grows. + if (runningIdx < client[kPendingIdx] && queue[runningIdx] === request) { + queue[runningIdx] = null + client[kRunningIdx] = runningIdx + 1 + return + } + + const index = queue.indexOf(request, runningIdx) if (index === -1 || index >= client[kPendingIdx]) { return } - client[kQueue].splice(index, 1) + queue.splice(index, 1) client[kPendingIdx]-- if (resetPendingIdx && client[kPendingIdx] < client[kRunningIdx]) { @@ -12512,17 +12752,25 @@ function completeRequest (client, request, resetPendingIdx = false) { } } -function canRetryRequestAfterGoAway (request) { +function canReplayRequest (request) { const { body } = request return body == null || util.isBuffer(body) || util.isBlobLike(body) } -function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) { - const stream = request[kRequestStream] +// Count a GOAWAY refusal against the request's replay budget. A peer that +// refuses every connection must eventually surface an error to the caller +// rather than being retried forever. Kept separate from canReplayRequest so +// that the REFUSED_STREAM retry, which has its own single-attempt limit, does +// not consume this budget just by asking whether the body can be replayed. +function registerGoAwayRefusal (request) { + const attempts = (request[kGoAwayReplayAttempts] ?? 0) + 1 + request[kGoAwayReplayAttempts] = attempts - clearRequestStream(request) + return attempts <= MAX_GOAWAY_REPLAY_ATTEMPTS +} +function closeStream (stream, code = NGHTTP2_REFUSED_STREAM) { if (stream != null && !stream.destroyed && !stream.closed) { try { stream.close(code) @@ -12530,15 +12778,48 @@ function closeRequestStream (request, code = NGHTTP2_REFUSED_STREAM) { } } +function detachRequestStreamForClose (request) { + const stream = request[kRequestStream] + + clearRequestStream(request) + severRequestStream(stream) + + return stream +} + +// Unbind a stream from its request for good. releaseRequestStream() alone +// leaves the 'close' listener attached and kRequestStreamState populated, so a +// stream abandoned here would still run completeRequestStream() later — and +// splice out the request that has since been requeued onto another session. +function severRequestStream (stream) { + if (stream == null || stream[kRequestStreamState] == null) { + return + } + + stream[kRequestStreamState] = null + stream.off('close', completeRequestStream) + // Upgrade streams use their own close cleanup, which would otherwise release + // the session a second time after the stream has been severed for GOAWAY. + stream.off('close', onUpgradeStreamClose) + + if (stream[kHTTP2Session] != null) { + closeStreamSession(stream) + } + + if (!stream.destroyed && !stream.closed) { + stream.once('error', noop) + } +} + function connectH2 (client, socket) { client[kSocket] = socket - const http2InitialWindowSize = client[kHTTP2InitialWindowSize] - const http2ConnectionWindowSize = client[kHTTP2ConnectionWindowSize] + const http2InitialWindowSize = client[kHTTP2Options].sessionOptions?.initialWindowSize + const http2ConnectionWindowSize = client[kHTTP2Options].connectionWindowSize const session = http2.connect(client[kUrl], { createConnection: () => socket, - peerMaxConcurrentStreams: client[kMaxConcurrentStreams], + peerMaxConcurrentStreams: client[kHTTP2Options].maxConcurrentStreams, settings: { // TODO(metcoder95): add support for PUSH enablePush: false, @@ -12552,8 +12833,16 @@ function connectH2 (client, socket) { session[kSocket] = socket session[kHTTP2SessionState] = { idleTimeout: null, + // Armed while the peer advertises MAX_CONCURRENT_STREAMS = 0 and we have + // work that cannot start. See setNoStreamsTimeout. + noStreamsTimeout: null, + // Sockets start out ref'd. Session ref/unref proxies to the socket, so a + // single cached flag lets us skip redundant uv ref/unref calls, provided + // every ref/unref of the session or its socket goes through + // refH2Session/unrefH2Session. + refed: true, ping: { - interval: client[kPingInterval] === 0 ? null : setInterval(onHttp2SendPing, client[kPingInterval], session).unref() + interval: client[kHTTP2Options].pingInterval === 0 ? null : setInterval(onHttp2SendPing, client[kHTTP2Options].pingInterval, session).unref() } } session[kReceivedGoAway] = false @@ -12571,14 +12860,13 @@ function connectH2 (client, socket) { util.addListener(session, 'error', onHttp2SessionError) util.addListener(session, 'frameError', onHttp2FrameError) - util.addListener(session, 'end', onHttp2SessionEnd) util.addListener(session, 'goaway', onHttp2SessionGoAway) util.addListener(session, 'close', onHttp2SessionClose) util.addListener(session, 'remoteSettings', onHttp2RemoteSettings) // TODO (@metcoder95): implement SETTINGS support // util.addListener(session, 'localSettings', onHttp2RemoteSettings) - session.unref() + unrefH2Session(session) client[kHTTP2Session] = session socket[kHTTP2Session] = session @@ -12647,16 +12935,6 @@ function connectH2 (client, socket) { // Don't dispatch an upgrade until all preceding requests have completed. // Possibly, we do not have remote settings confirmed yet. if ((request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false) return true - // Request with stream or iterator body can error while other requests - // are inflight and indirectly error those as well. - // Ensure this doesn't happen by waiting for inflight - // to complete before dispatching. - - // Request with stream or iterator body cannot be retried. - // Ensure that no other requests are inflight and - // could cause failure. - if (util.bodyLength(request.body) !== 0 && - (util.isStream(request.body) || util.isAsyncIterable(request.body) || util.isFormDataLike(request.body))) return true } else { return (request.upgrade === 'websocket' || request.method === 'CONNECT') && session[kRemoteSettings] === false } @@ -12667,17 +12945,36 @@ function connectH2 (client, socket) { } } +// Session ref/unref proxies to the underlying socket, so refH2Session and +// unrefH2Session cover both and can skip the call when the cached ref state +// already matches. +function refH2Session (session) { + const state = session[kHTTP2SessionState] + + if (state.refed === false) { + state.refed = true + session.ref() + } +} + +function unrefH2Session (session) { + const state = session[kHTTP2SessionState] + + if (state.refed === true) { + state.refed = false + session.unref() + } +} + function resumeH2 (client) { const socket = client[kSocket] const session = client[kHTTP2Session] if (socket?.destroyed === false) { if (client[kSize] === 0 || client[kMaxConcurrentStreams] === 0) { - socket.unref() - session.unref() + unrefH2Session(session) } else { - socket.ref() - session.ref() + refH2Session(session) } if (client[kSize] === 0 && session[kOpenStreams] === 0) { @@ -12685,7 +12982,74 @@ function resumeH2 (client) { } else { clearHttp2IdleTimeout(session) } + + if (client[kMaxConcurrentStreams] === 0 && client[kRunning] === 0 && client[kPending] > 0) { + setNoStreamsTimeout(session) + } else { + clearNoStreamsTimeout(session) + } + } +} + +function clearNoStreamsTimeout (session) { + const state = session[kHTTP2SessionState] + + if (state?.noStreamsTimeout != null) { + clearTimeout(state.noStreamsTimeout) + state.noStreamsTimeout = null + } +} + +// A peer is allowed to advertise SETTINGS_MAX_CONCURRENT_STREAMS = 0 to refuse +// new streams (RFC 9113 §6.5.2), and is expected to raise it again later. Until +// it does, busy() reports the client as permanently busy and queued requests +// cannot open a stream — which means no per-stream timeout covers them, and no +// reconnect can happen either, so the SETTINGS frame that would lift the limit +// can never arrive. Give the peer headersTimeout to start honouring requests +// before failing them; a request that cannot even be sent has missed the same +// deadline as one whose headers never arrive. +function setNoStreamsTimeout (session) { + const client = session[kClient] + const state = session[kHTTP2SessionState] + const timeout = client[kHeadersTimeout] + + if (!timeout || state.noStreamsTimeout != null) { + return + } + + state.noStreamsTimeout = setTimeout(onNoStreamsTimeout, timeout, session).unref() +} + +function onNoStreamsTimeout (session) { + const client = session[kClient] + const state = session[kHTTP2SessionState] + + state.noStreamsTimeout = null + + if ( + client[kHTTP2Session] !== session || + client[kMaxConcurrentStreams] !== 0 || + client[kRunning] !== 0 || + client[kPending] === 0 + ) { + return + } + + const err = new HeadersTimeoutError( + `HTTP/2: server did not accept a new stream within ${client[kHeadersTimeout]}` + ) + + const requests = client[kQueue].splice(client[kPendingIdx]) + for (let i = 0; i < requests.length; i++) { + if (requests[i] != null) { + util.errorRequest(client, requests[i], err) + } } + + // Drop the unusable session so the next request gets a fresh connection, + // whose SETTINGS may well allow streams again. + session[kError] = err + resetHttp2Session(session, err) } function clearHttp2IdleTimeout (session) { @@ -12791,23 +13155,26 @@ function onHttp2SessionError (err) { assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') this[kSocket][kError] = err + + if (this[kReceivedGoAway]) { + return + } + this[kClient][kOnError](err) } function onHttp2FrameError (type, code, id) { if (id === 0) { + if (this[kReceivedGoAway]) { + return + } + const err = new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`) this[kSocket][kError] = err this[kClient][kOnError](err) } } -function onHttp2SessionEnd () { - const err = new SocketError('other side closed', util.getSocketInfo(this[kSocket])) - this.destroy(err) - util.destroy(this[kSocket], err) -} - /** * This is the root cause of #3011 * We need to handle GOAWAY frames properly, and trigger the session close @@ -12829,14 +13196,18 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) { const previousPendingIdx = client[kPendingIdx] const pendingIdx = getGoAwayPendingIdx(client, lastStreamID) const retriableRequests = [] + const streamsToClose = [] + // Closing one stream after GOAWAY can synchronously emit frameError on + // sibling streams. Detach all affected requests first so those errors do + // not fail requests that are about to be requeued. for (let i = pendingIdx; i < previousPendingIdx; i++) { const request = client[kQueue][i] if (request != null) { - closeRequestStream(request) + streamsToClose.push(detachRequestStreamForClose(request)) - if (canRetryRequestAfterGoAway(request)) { + if (canReplayRequest(request) && registerGoAwayRefusal(request)) { retriableRequests.push(request) } else { util.errorRequest(client, request, err) @@ -12844,6 +13215,10 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) { } } + for (let i = 0; i < streamsToClose.length; i++) { + closeStream(streamsToClose[i]) + } + if (pendingIdx !== previousPendingIdx) { const remainingPendingRequests = client[kQueue].slice(previousPendingIdx) client[kQueue].length = pendingIdx @@ -12857,6 +13232,7 @@ function onHttp2SessionGoAway (errorCode, lastStreamID) { } clearHttp2IdleTimeout(this) + clearNoStreamsTimeout(this) if (!this.closed && !this.destroyed) { this.close() @@ -12881,6 +13257,7 @@ function onHttp2SessionClose () { } clearHttp2IdleTimeout(this) + clearNoStreamsTimeout(this) if (state.ping.interval != null) { clearInterval(state.ping.interval) @@ -12939,7 +13316,11 @@ function onHttp2SocketError (err) { this[kError] = err - this[kClient][kOnError](err) + if (this[kHTTP2Session]?.[kReceivedGoAway]) { + return + } + + this[kHTTP2Session]?.[kClient]?.[kOnError](err) } function onHttp2SocketEnd () { @@ -12958,7 +13339,7 @@ function closeStreamSession (stream) { stream[kHTTP2Session] = null session[kOpenStreams] -= 1 if (session[kOpenStreams] === 0) { - session.unref() + unrefH2Session(session) setHttp2IdleTimeout(session) } } @@ -12973,22 +13354,34 @@ function onUpgradeStreamClose () { closeStreamSession(this) } -function onRequestStreamClose () { +// Idempotent terminal cleanup, called from both 'end' and 'close': the +// null-state guard no-ops the later call. +function completeRequestStream () { const state = this[kRequestStreamState] - if (state) { - // Release the stream first so request references are cleared, - // then complete the response with trailers if available. - releaseRequestStream(this) + if (state == null) { + return + } - if (state.pendingEnd && !state.request.aborted && !state.request.completed) { - state.request.onResponseEnd(state.trailers || {}) - state.finalizeRequest() - } + // Release the stream first so request references are cleared, + // then complete the response with trailers if available. + releaseRequestStream(this) + + if (state.pendingEnd && !state.request.aborted && !state.request.completed) { + state.request.onResponseEnd(state.trailers || {}) + } else if (!state.request.aborted && !state.request.completed) { + // The stream closed without a complete response and without reporting an + // error. finalizeRequest() below frees the queue slot either way, so + // without this the request would simply vanish and its caller would never + // hear back. + util.errorRequest( + state.client, + state.request, + new InformationalError('HTTP/2: stream closed before the response was complete') + ) } - this.off('data', onData) - this.off('error', noop) + finalizeRequest(state) closeStreamSession(this) this[kRequestStreamState] = null } @@ -13111,7 +13504,7 @@ function onUpgradeResponse (headers, _flags) { removeUpgradeStreamListeners(stream) detachRequestFromStream(request) - state.finalizeRequest() + finalizeRequest(state) } function setupUpgradeStream (stream, state) { @@ -13134,12 +13527,51 @@ function setupUpgradeStream (stream, state) { stream.setTimeout(headersTimeout) } +function finalizeRequest (state, resetPendingIdx = false) { + if (state.requestFinalized) { + return + } + + state.requestFinalized = true + completeRequest(state.client, state.request, resetPendingIdx) + + state.client[kResume]() +} + +function openStream (client, request, session, abort, headers, options) { + try { + return session.request(headers, options) + } catch (err) { + // A GOAWAY'd session rejects new streams, same as an invalid session: + // reset and requeue on a fresh connection rather than the destroy + abort + // below, whose destroy(socket, err) can crash via an unhandled 'error'. + if (err?.code === 'ERR_HTTP2_INVALID_SESSION' || err?.code === 'ERR_HTTP2_GOAWAY_SESSION') { + const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket])) + wrappedErr.cause = err + session[kError] = wrappedErr + resetHttp2Session(session, wrappedErr) + requeueUnsentRequest(client, request) + + return null + } + + const wrappedErr = new InformationalError(err.message, { cause: err }) + session[kError] = wrappedErr + session[kSocket][kError] = wrappedErr + + session.destroy(wrappedErr) + util.destroy(session[kSocket], wrappedErr) + abort(wrappedErr) + + return null + } +} + function writeH2 (client, request) { const headersTimeout = request.headersTimeout ?? client[kHeadersTimeout] const bodyTimeout = request.bodyTimeout ?? client[kBodyTimeout] const session = client[kHTTP2Session] const { method, path, host, upgrade, expectContinue, signal, protocol, headers: reqHeaders } = request - let { body } = request if (upgrade != null && upgrade !== 'websocket') { util.errorRequest(client, request, new InvalidArgumentError(`Custom upgrade "${upgrade}" not supported over HTTP/2`)) @@ -13148,22 +13580,28 @@ function writeH2 (client, request) { const headers = buildRequestHeaders(reqHeaders) - /** @type {import('node:http2').ClientHttp2Stream} */ - let stream = null - headers[HTTP2_HEADER_AUTHORITY] = host || client[kHostAuthority] headers[HTTP2_HEADER_METHOD] = method - let requestFinalized = false - const finalizeRequest = (resetPendingIdx = false) => { - if (requestFinalized) { - return - } - - requestFinalized = true - completeRequest(client, request, resetPendingIdx) - - client[kResume]() + // Single pre-shaped state object shared by all stream event handlers. + // All fields are declared up-front so the object keeps a stable hidden + // class for the whole request lifetime. + const state = { + abort: null, + body: request.body, + client, + contentLength: null, + expectsPayload: false, + request, + headersTimeout, + bodyTimeout, + requestFinalized: false, + responseReceived: false, + bodySent: false, + pendingEnd: false, + trailers: null, + session, + stream: null } const abort = (err, resetPendingIdx = false) => { @@ -13175,47 +13613,39 @@ function writeH2 (client, request) { util.errorRequest(client, request, err) - if (stream != null) { + if (state.stream != null) { clearRequestStream(request) - // On Abort, we close the stream to send RST_STREAM frame + // On Abort, we close the stream to send RST_STREAM frame. + const stream = state.stream stream.close() + // close() alone leaves cleanup waiting on the 'close' event; on a busy, + // long-lived multiplexed session that event can fail to fire, leaving the + // native Http2Stream (and the whole request graph it pins) alive for the + // session's life. Destroy the stream synchronously to release the handle + // deterministically. Deferring the destroy (e.g. via setImmediate) leaks + // the same way when the event loop is stalled and the callback never runs + // under abort churn (#5558); close() has already queued the RST_STREAM + // frame on the native session, so a synchronous destroy still sends it. + if (!stream.destroyed) { + util.destroy(stream) + } + // We move the running index to the next request client[kOnError](err) - finalizeRequest(resetPendingIdx) + finalizeRequest(state, resetPendingIdx) } // We do not destroy the socket as we can continue using the session // the stream gets destroyed and the session remains to create new streams - util.destroy(body, err) + util.destroy(state.body, err) } - const requestStream = (headers, options) => { - try { - return session.request(headers, options) - } catch (err) { - if (err?.code === 'ERR_HTTP2_INVALID_SESSION') { - const wrappedErr = new SocketError(err.message, util.getSocketInfo(session[kSocket])) - wrappedErr.cause = err - session[kError] = wrappedErr - resetHttp2Session(session, wrappedErr) - requeueUnsentRequest(client, request) - - return null - } - - const wrappedErr = new InformationalError(err.message, { cause: err }) - session[kError] = wrappedErr - session[kSocket][kError] = wrappedErr - - session.destroy(wrappedErr) - util.destroy(session[kSocket], wrappedErr) - abort(wrappedErr) + state.abort = abort - return null - } - } + /** @type {import('node:http2').ClientHttp2Stream} */ + let stream = null try { // We are already connected, streams are pending. @@ -13230,24 +13660,13 @@ function writeH2 (client, request) { } if (upgrade || method === 'CONNECT') { - session.ref() - - const upgradeState = { - abort, - finalizeRequest, - request, - headersTimeout, - bodyTimeout, - responseReceived: false, - session, - stream: null - } + refH2Session(session) if (upgrade === 'websocket') { // We cannot upgrade to websocket if extended CONNECT protocol is not supported if (session[kEnableConnectProtocol] === false) { util.errorRequest(client, request, new InformationalError('HTTP/2: Extended CONNECT protocol not supported by server')) - session.unref() + unrefH2Session(session) return false } @@ -13265,12 +13684,12 @@ function writeH2 (client, request) { headers[HTTP2_HEADER_SCHEME] = protocol === 'http:' ? 'http' : 'https' } - stream = requestStream(headers, { endStream: false, signal }) + stream = openStream(client, request, session, abort, headers, { endStream: false, signal }) if (stream == null) { - session.unref() + unrefH2Session(session) return false } - setupUpgradeStream(stream, upgradeState) + setupUpgradeStream(stream, state) return true } @@ -13279,12 +13698,12 @@ function writeH2 (client, request) { // will create a new stream. We trigger a request to create the stream and wait until // `ready` event is triggered // We disabled endStream to allow the user to write to the stream - stream = requestStream(headers, { endStream: false, signal }) + stream = openStream(client, request, session, abort, headers, { endStream: false, signal }) if (stream == null) { - session.unref() + unrefH2Session(session) return false } - setupUpgradeStream(stream, upgradeState) + setupUpgradeStream(stream, state) return true } @@ -13312,6 +13731,8 @@ function writeH2 (client, request) { method === 'PROPPATCH' ) + let body = state.body + if (body && typeof body.read === 'function') { // Try to read EOF in order to get length. body.read(0) @@ -13320,7 +13741,7 @@ function writeH2 (client, request) { let contentLength = util.bodyLength(body) if (util.isFormDataLike(body)) { - extractBody ??= (__nccwpck_require__(9284).extractBody) + extractBody ??= (__nccwpck_require__(9412).extractBody) const [bodyStream, contentType] = extractBody(body) headers['content-type'] = contentType @@ -13358,7 +13779,7 @@ function writeH2 (client, request) { headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}` } - session.ref() + refH2Session(session) if (channels.sendHeaders.hasSubscribers) { let header = '' @@ -13370,26 +13791,16 @@ function writeH2 (client, request) { // TODO(metcoder95): add support for sending trailers const shouldEndStream = body === null || contentLength === 0 - const state = { - abort, - body, - client, - contentLength, - expectsPayload, - finalizeRequest, - request, - headersTimeout, - bodyTimeout, - responseReceived: false, - session, - stream: null - } + + state.body = body + state.contentLength = contentLength + state.expectsPayload = expectsPayload if (expectContinue) { headers[HTTP2_HEADER_EXPECT] = '100-continue' } - stream = requestStream(headers, { endStream: shouldEndStream, signal }) + stream = openStream(client, request, session, abort, headers, { endStream: shouldEndStream, signal }) if (stream == null) { return false } @@ -13400,22 +13811,30 @@ function writeH2 (client, request) { // Increment counter as we have new streams open clearHttp2IdleTimeout(session) ++session[kOpenStreams] - stream.setTimeout(headersTimeout) + + if (headersTimeout) { + stream.setTimeout(headersTimeout) + } stream[kHTTP2Session] = session - stream.once('close', onRequestStreamClose) + stream.on('close', completeRequestStream) bindRequestToStream(request, stream, releaseRequestStream) if (expectContinue) { stream.once('continue', writeBodyH2) } - stream.once('response', onResponse) - stream.once('end', onEnd) - stream.once('error', onError) - stream.once('frameError', onFrameError) + // The handlers below either remove themselves on first invocation or + // become unreachable once the stream closes, so plain `on` avoids the + // per-listener `once` wrapper allocation. + stream.on('response', onResponse) + stream.on('end', onEnd) + stream.on('error', onError) + stream.on('frameError', onFrameError) stream.on('aborted', onAborted) - stream.on('timeout', onTimeout) - stream.once('trailers', onTrailers) + if (headersTimeout || bodyTimeout) { + stream.on('timeout', onTimeout) + } + stream.on('trailers', onTrailers) if (!expectContinue) { writeBodyH2.call(stream) @@ -13453,16 +13872,24 @@ function releaseRequestStream (stream) { detachRequestFromStream(request) } - removeRequestStreamListeners(stream) - + // A closed or destroyed stream cannot emit further events; leaving the + // listeners in place saves the removal scans (they are collected with + // the stream). All handlers bail out when the stream state is gone. if (!stream.destroyed && !stream.closed) { + removeRequestStreamListeners(stream) stream.once('error', noop) } } function onData (chunk) { const stream = this - const { request } = stream[kRequestStreamState] + const state = stream[kRequestStreamState] + + if (state == null) { + return + } + + const { request } = state if (request.aborted || request.completed) { return @@ -13476,22 +13903,40 @@ function onData (chunk) { function onResponse (headers) { const stream = this const state = stream[kRequestStreamState] + + if (state == null) { + return + } + const { request } = state stream.off('response', onResponse) + // Final response received while still awaiting 100 (Continue): the body won't + // be sent, so close our half or the stream stays open and never completes. + if (state.body != null && !state.bodySent && !stream.writableEnded) { + stream.removeListener('continue', writeBodyH2) + stream.end() + } + const statusCode = headers[HTTP2_HEADER_STATUS] delete headers[HTTP2_HEADER_STATUS] request.onResponseStarted() state.responseReceived = true - stream.setTimeout(state.bodyTimeout) + + if (state.headersTimeout || state.bodyTimeout) { + stream.setTimeout(state.bodyTimeout) + } // Due to the stream nature, it is possible we face a race condition // where the stream has been assigned, but the request has been aborted - // the request remains in-flight and headers hasn't been received yet - // for those scenarios, best effort is to destroy the stream immediately - // as there's no value to keep it open. - if (request.aborted) { + // or already completed and headers hasn't been received yet. A late + // 'response' delivered after completion would call request.onResponseStart + // post-completion, tripping its `assert(!this.completed)` (an uncatchable + // throw on the http2 event tick). Guard `completed` here as onEnd/onTrailers + // already do; best effort is to release the stream immediately as there's + // no value to keep it open. + if (request.aborted || request.completed) { releaseRequestStream(stream) return } @@ -13506,17 +13951,25 @@ function onResponse (headers) { function onEnd () { const stream = this const state = stream[kRequestStreamState] + + if (state == null) { + return + } + const { request } = state stream.off('end', onEnd) - // If we received a response, this is a normal completion. - // Defer actual completion to onRequestStreamClose so that - // onTrailers (which may fire after 'end' on Windows) can - // store trailers first. + // onTrailers (which may fire after 'end' on Windows) has already stored + // trailers on the state by now, so completing here still delivers them. if (state.responseReceived) { if (!request.aborted && !request.completed) { state.pendingEnd = true + + // Complete on 'end': a blocked event loop can keep the stream's 'close' + // from firing, stranding its buffers until OOM. Idempotent, so a later + // 'close' no-ops. + completeRequestStream.call(stream) } } else { // Stream ended without receiving a response - this is an error @@ -13525,11 +13978,59 @@ function onEnd () { } } +function retryRefusedStream (stream, state) { + const { client, request } = state + + if ( + state.responseReceived || + request.aborted || + request.completed || + request[kRefusedStreamRetry] || + !canReplayRequest(request) + ) { + return false + } + + // RFC 9113 section 8.7 permits retrying REFUSED_STREAM, but says clients + // SHOULD NOT automatically retry the same request more than once. + request[kRefusedStreamRetry] = true + + // Detach the failed attempt before moving the request back to the pending + // queue. The peer only reset this stream, so the HTTP/2 session remains + // usable for the retry. Severing also drops the 'close' listener, so the + // abandoned stream cannot later complete the retried request. + detachRequestStreamForClose(request) + state.stream = null + state.requestFinalized = true + + completeRequest(client, request) + client[kQueue].splice(client[kPendingIdx], 0, request) + client[kResume]() + + return true +} + function onError (err) { const stream = this const state = stream[kRequestStreamState] + if (state == null) { + return + } + stream.off('error', onError) + + if (typeof stream.rstCode === 'number' && stream.rstCode !== NGHTTP2_NO_ERROR) { + err.http2ErrorCode = stream.rstCode + } + + if ( + stream.rstCode === NGHTTP2_REFUSED_STREAM && + retryRefusedStream(stream, state) + ) { + return + } + state.abort(err) } @@ -13537,6 +14038,10 @@ function onFrameError (type, code) { const stream = this const state = stream[kRequestStreamState] + if (state == null) { + return + } + stream.off('frameError', onFrameError) state.abort(new InformationalError(`HTTP/2: "frameError" received - type ${type}, code ${code}`)) } @@ -13549,6 +14054,10 @@ function onTimeout () { const stream = this const state = stream[kRequestStreamState] + if (state == null) { + return + } + // Remove self so timeout doesn't fire again after we handle it stream.off('timeout', onTimeout) @@ -13561,6 +14070,11 @@ function onTimeout () { function onTrailers (trailers) { const stream = this const state = stream[kRequestStreamState] + + if (state == null) { + return + } + const { request } = state stream.off('trailers', onTrailers) @@ -13570,13 +14084,14 @@ function onTrailers (trailers) { return } - // Store trailers for onRequestStreamClose to use when completing + // Store trailers for completeRequestStream to use when completing state.trailers = trailers } function writeBodyH2 () { const stream = this const state = stream[kRequestStreamState] + state.bodySent = true const { abort, body, client, contentLength, expectsPayload, request } = state if (!body || contentLength === 0) { @@ -13795,7 +14310,7 @@ module.exports = connectH2 /***/ }), -/***/ 125: +/***/ 3869: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -13804,17 +14319,17 @@ module.exports = connectH2 const assert = __nccwpck_require__(4589) const net = __nccwpck_require__(7030) const http = __nccwpck_require__(7067) -const util = __nccwpck_require__(648) -const { ClientStats } = __nccwpck_require__(6686) -const { channels } = __nccwpck_require__(8054) -const Request = __nccwpck_require__(7895) -const DispatcherBase = __nccwpck_require__(1049) +const util = __nccwpck_require__(824) +const { ClientStats } = __nccwpck_require__(6030) +const { channels } = __nccwpck_require__(2966) +const Request = __nccwpck_require__(9463) +const DispatcherBase = __nccwpck_require__(713) const { InvalidArgumentError, InformationalError, ClientDestroyedError -} = __nccwpck_require__(3291) -const buildConnector = __nccwpck_require__(9512) +} = __nccwpck_require__(4715) +const buildConnector = __nccwpck_require__(7432) const { kUrl, kServerName, @@ -13854,13 +14369,11 @@ const { kHTTPContext, kMaxConcurrentStreams, kHostAuthority, - kHTTP2InitialWindowSize, - kHTTP2ConnectionWindowSize, kResume, - kPingInterval -} = __nccwpck_require__(451) -const connectH1 = __nccwpck_require__(6629) -const connectH2 = __nccwpck_require__(6892) + kHTTP2Options +} = __nccwpck_require__(2019) +const connectH1 = __nccwpck_require__(8101) +const connectH2 = __nccwpck_require__(8364) const kClosedResolve = Symbol('kClosedResolve') @@ -13877,6 +14390,16 @@ function getPipelining (client) { return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1 } +let h2NamespaceOptsWarning = false +function emitH2OptionsNamespaceWarning (optName) { + if (h2NamespaceOptsWarning === true) return + + process.emitWarning(`Use h2Options.${optName} instead. ${optName} for H2 will be deprecated in future major.`, { + code: 'UNDICI-H2-OPTIONS' + }) + h2NamespaceOptsWarning = true +} + // Protocol-aware dispatch ceiling. h1 RFC7230 pipelining is unrelated to h2 // stream multiplexing — over h2 the ceiling is the (server-confirmed) // maxConcurrentStreams. Before a context is attached we use the h1 @@ -13929,7 +14452,8 @@ class Client extends DispatcherBase { initialWindowSize, connectionWindowSize, pingInterval, - webSocket + webSocket, + h2Options } = {}) { if (keepAlive !== undefined) { throw new InvalidArgumentError('unsupported keepAlive, use pipelining=0 instead') @@ -14017,24 +14541,55 @@ class Client extends DispatcherBase { throw new InvalidArgumentError('allowH2 must be a valid boolean value') } - if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { - throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0') - } + // We validate only if allowH2 is enabled or null (enabled by default) + if (allowH2 !== false) { + // Prioritise new h2Options object, otherwise fallback to prior configuration options + if (h2Options != null) { + if (h2Options.useH2c != null && typeof h2Options.useH2c !== 'boolean') { + throw new InvalidArgumentError('h2Options.useH2c must be a valid boolean value') + } - if (useH2c != null && typeof useH2c !== 'boolean') { - throw new InvalidArgumentError('useH2c must be a valid boolean value') - } + if (h2Options.settings?.initialWindowSize != null && (!Number.isInteger(h2Options.settings.initialWindowSize) || h2Options.settings.initialWindowSize < 1)) { + throw new InvalidArgumentError('h2Options.settings.initialWindowSize must be a positive integer, greater than 0') + } - if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { - throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0') - } + if (h2Options.maxConcurrentStreams != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.maxConcurrentStreams < 1)) { + throw new InvalidArgumentError('h2Options.maxConcurrentStreams must be a positive integer, greater than 0') + } - if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { - throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0') - } + if (h2Options.connectionWindowSize != null && (!Number.isInteger(h2Options.connectionWindowSize) || h2Options.connectionWindowSize < 1)) { + throw new InvalidArgumentError('h2Options.connectionWindowSize must be a positive integer, greater than 0') + } + + if (h2Options.pingInterval != null && (typeof h2Options.pingInterval !== 'number' || !Number.isInteger(h2Options.pingInterval) || h2Options.pingInterval < 0)) { + throw new InvalidArgumentError('h2Options.pingInterval must be a positive integer, greater or equal to 0') + } + } else { + if (useH2c != null && typeof useH2c !== 'boolean') { + emitH2OptionsNamespaceWarning('useH2c') + throw new InvalidArgumentError('useH2c must be a valid boolean value') + } + + if (maxConcurrentStreams != null && (typeof maxConcurrentStreams !== 'number' || maxConcurrentStreams < 1)) { + emitH2OptionsNamespaceWarning('maxConcurrentStreams') + throw new InvalidArgumentError('maxConcurrentStreams must be a positive integer, greater than 0') + } + + if (initialWindowSize != null && (!Number.isInteger(initialWindowSize) || initialWindowSize < 1)) { + emitH2OptionsNamespaceWarning('initialWindowSize') + throw new InvalidArgumentError('initialWindowSize must be a positive integer, greater than 0') + } + + if (connectionWindowSize != null && (!Number.isInteger(connectionWindowSize) || connectionWindowSize < 1)) { + emitH2OptionsNamespaceWarning('connectionWindowSize') + throw new InvalidArgumentError('connectionWindowSize must be a positive integer, greater than 0') + } - if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) { - throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') + if (pingInterval != null && (typeof pingInterval !== 'number' || !Number.isInteger(pingInterval) || pingInterval < 0)) { + emitH2OptionsNamespaceWarning('pingInterval') + throw new InvalidArgumentError('pingInterval must be a positive integer, greater or equal to 0') + } + } } super({ webSocket }) @@ -14044,8 +14599,8 @@ class Client extends DispatcherBase { ...tls, maxCachedSessions, allowH2, - useH2c, socketPath, + useH2c: h2Options?.useH2c ?? useH2c, timeout: connectTimeout, ...(typeof autoSelectFamily === 'boolean' ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect @@ -14081,16 +14636,20 @@ class Client extends DispatcherBase { this[kMaxResponseSize] = maxResponseSize > -1 ? maxResponseSize : -1 this[kHTTPContext] = null // h2 - this[kMaxConcurrentStreams] = maxConcurrentStreams != null ? maxConcurrentStreams : 100 // Max peerConcurrentStreams for a Node h2 server - // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance: - // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1) - // Allows more data to be sent before requiring acknowledgment, improving throughput - // especially on high-latency networks. This matches common production HTTP/2 servers. - // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set) - // Provides better flow control for the entire connection across multiple streams. - this[kHTTP2InitialWindowSize] = initialWindowSize != null ? initialWindowSize : 262144 - this[kHTTP2ConnectionWindowSize] = connectionWindowSize != null ? connectionWindowSize : 524288 - this[kPingInterval] = pingInterval != null ? pingInterval : 60e3 // Default ping interval for h2 - 1 minute + this[kHTTP2Options] = { + pingInterval: h2Options?.pingInterval ?? pingInterval ?? 60e3, + connectionWindowSize: h2Options?.connectionWindowSize ?? connectionWindowSize ?? 524288, + maxConcurrentStreams: h2Options?.maxConcurrentStreams ?? maxConcurrentStreams ?? 100, // Max peerConcurrentStreams for a Node h2 server + sessionOptions: { + // HTTP/2 window sizes are set to higher defaults than Node.js core for better performance: + // - initialWindowSize: 262144 (256KB) vs Node.js default 65535 (64KB - 1) + // Allows more data to be sent before requiring acknowledgment, improving throughput + // especially on high-latency networks. This matches common production HTTP/2 servers. + // - connectionWindowSize: 524288 (512KB) vs Node.js default (none set) + // Provides better flow control for the entire connection across multiple streams. + initialWindowSize: h2Options?.initialWindowSize ?? initialWindowSize ?? 262144 + } + } // kQueue is built up of 3 sections separated by // the kRunningIdx and kPendingIdx indices. @@ -14473,6 +15032,7 @@ function _resume (client, sync) { } if (!client[kHTTPContext]) { + client[kServerName] = request.servername connect(client) return } @@ -14498,19 +15058,19 @@ module.exports = Client /***/ }), -/***/ 1049: +/***/ 713: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const Dispatcher = __nccwpck_require__(1419) +const Dispatcher = __nccwpck_require__(7595) const { ClientDestroyedError, ClientClosedError, InvalidArgumentError -} = __nccwpck_require__(3291) -const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = __nccwpck_require__(451) +} = __nccwpck_require__(4715) +const { kDestroy, kClose, kClosed, kDestroyed, kDispatch } = __nccwpck_require__(2019) const kOnDestroyed = Symbol('onDestroyed') const kOnClosed = Symbol('onClosed') @@ -14691,7 +15251,7 @@ module.exports = DispatcherBase /***/ }), -/***/ 1419: +/***/ 7595: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -14743,15 +15303,15 @@ module.exports = Dispatcher /***/ }), -/***/ 8106: +/***/ 2474: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const Dispatcher = __nccwpck_require__(1419) -const { InvalidArgumentError } = __nccwpck_require__(3291) -const { toRawHeaders } = __nccwpck_require__(648) +const Dispatcher = __nccwpck_require__(7595) +const { InvalidArgumentError } = __nccwpck_require__(4715) +const { toRawHeaders } = __nccwpck_require__(824) class LegacyHandlerWrapper { #handler @@ -14858,16 +15418,16 @@ module.exports = Dispatcher1Wrapper /***/ }), -/***/ 3673: +/***/ 4505: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const DispatcherBase = __nccwpck_require__(1049) -const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = __nccwpck_require__(451) -const ProxyAgent = __nccwpck_require__(3256) -const Agent = __nccwpck_require__(821) +const DispatcherBase = __nccwpck_require__(713) +const { kClose, kDestroy, kClosed, kDestroyed, kDispatch, kNoProxyAgent, kHttpProxyAgent, kHttpsProxyAgent } = __nccwpck_require__(2019) +const ProxyAgent = __nccwpck_require__(5131) +const Agent = __nccwpck_require__(5733) const DEFAULT_PORTS = { 'http:': 80, @@ -14929,9 +15489,10 @@ class EnvHttpProxyAgent extends DispatcherBase { #getProxyAgentForUrl (url) { let { protocol, host: hostname, port } = url - // Stripping ports in this way instead of using parsedUrl.hostname to make - // sure that the brackets around IPv6 addresses are kept. - hostname = hostname.replace(/:\d*$/, '').toLowerCase() + // Remove the port suffix (e.g. ":8080") and then strip surrounding + // brackets from IPv6 literals (e.g. "[::1]" -> "::1") so that the + // result matches the unbracketed form stored by #parseNoProxy. + hostname = hostname.replace(/:\d*$/, '').replace(/^\[(.+)\]$/, '$1').toLowerCase() port = Number.parseInt(port, 10) || DEFAULT_PORTS[protocol] || 0 if (!this.#shouldProxy(hostname, port)) { return this[kNoProxyAgent] @@ -14983,11 +15544,32 @@ class EnvHttpProxyAgent extends DispatcherBase { if (!entry) { continue } - const parsed = entry.match(/^(.+):(\d+)$/) + + // An IPv6 entry with a port must be bracketed: [::1]:443. + // A bare IPv6 address like ::1 contains colons that must not be + // confused with a host:port separator, so we handle it separately. + let hostname, port + const ipv6WithPort = entry.match(/^\[(.+)\]:(\d+)$/) + if (ipv6WithPort) { + hostname = ipv6WithPort[1] + port = Number.parseInt(ipv6WithPort[2], 10) + } else { + // Bracketed IPv6 without port, or plain hostname[:port], or bare IPv6. + // Strip optional brackets first. + const unbracketed = entry.replace(/^\[(.+)\]$/, '$1') + // A bare IPv6 address contains multiple colons; a hostname:port entry + // has exactly one colon followed by digits. Only attempt host:port + // splitting when that is unambiguously the case. + const colonCount = (unbracketed.match(/:/g) || []).length + const parsed = colonCount === 1 && unbracketed.match(/^(.+):(\d+)$/) + hostname = parsed ? parsed[1] : unbracketed + port = parsed ? Number.parseInt(parsed[2], 10) : 0 + } + noProxyEntries.push({ // strip leading dot or asterisk with dot - hostname: (parsed ? parsed[1] : entry).replace(/^\*?\./, '').toLowerCase(), - port: parsed ? Number.parseInt(parsed[2], 10) : 0 + hostname: hostname.replace(/^\*?\./, '').toLowerCase(), + port }) } @@ -15012,7 +15594,7 @@ module.exports = EnvHttpProxyAgent /***/ }), -/***/ 1804: +/***/ 6060: /***/ ((module) => { "use strict"; @@ -15155,14 +15737,14 @@ module.exports = class FixedQueue { /***/ }), -/***/ 5719: +/***/ 1895: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { InvalidArgumentError } = __nccwpck_require__(3291) -const Client = __nccwpck_require__(125) +const { InvalidArgumentError } = __nccwpck_require__(4715) +const Client = __nccwpck_require__(3869) class H2CClient extends Client { constructor (origin, clientOpts) { @@ -15214,16 +15796,16 @@ module.exports = H2CClient /***/ }), -/***/ 6488: +/***/ 7960: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { PoolStats } = __nccwpck_require__(6686) -const DispatcherBase = __nccwpck_require__(1049) -const FixedQueue = __nccwpck_require__(1804) -const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = __nccwpck_require__(451) +const { PoolStats } = __nccwpck_require__(6030) +const DispatcherBase = __nccwpck_require__(713) +const FixedQueue = __nccwpck_require__(6060) +const { kConnected, kSize, kRunning, kPending, kQueued, kBusy, kFree, kUrl, kClose, kDestroy, kDispatch } = __nccwpck_require__(2019) const kClients = Symbol('clients') const kNeedDrain = Symbol('needDrain') @@ -15454,7 +16036,7 @@ module.exports = { /***/ }), -/***/ 1276: +/***/ 1324: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -15468,14 +16050,14 @@ const { kGetDispatcher, kHasDispatcher, kRemoveClient -} = __nccwpck_require__(6488) -const Client = __nccwpck_require__(125) +} = __nccwpck_require__(7960) +const Client = __nccwpck_require__(3869) const { InvalidArgumentError -} = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) -const { kUrl } = __nccwpck_require__(451) -const buildConnector = __nccwpck_require__(9512) +} = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) +const { kUrl } = __nccwpck_require__(2019) +const buildConnector = __nccwpck_require__(7432) const kOptions = Symbol('options') const kConnections = Symbol('connections') @@ -15605,21 +16187,21 @@ module.exports = Pool /***/ }), -/***/ 3256: +/***/ 5131: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { kProxy, kClose, kDestroy, kDispatch } = __nccwpck_require__(451) -const Agent = __nccwpck_require__(821) -const Pool = __nccwpck_require__(1276) -const DispatcherBase = __nccwpck_require__(1049) -const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError } = __nccwpck_require__(3291) -const buildConnector = __nccwpck_require__(9512) -const Client = __nccwpck_require__(125) -const { channels } = __nccwpck_require__(8054) -const Socks5ProxyAgent = __nccwpck_require__(5567) +const { kProxy, kClose, kDestroy, kDispatch } = __nccwpck_require__(2019) +const Agent = __nccwpck_require__(5733) +const Pool = __nccwpck_require__(1324) +const DispatcherBase = __nccwpck_require__(713) +const { InvalidArgumentError, RequestAbortedError, SecureProxyConnectionError, ProxyConnectionError } = __nccwpck_require__(4715) +const buildConnector = __nccwpck_require__(7432) +const Client = __nccwpck_require__(3869) +const { channels } = __nccwpck_require__(2966) +const Socks5ProxyAgent = __nccwpck_require__(1855) const kAgent = Symbol('proxy agent') const kClient = Symbol('proxy client') @@ -15648,10 +16230,15 @@ function defaultAgentFactory (origin, opts) { return new Pool(origin, opts) } +function shouldProxyTunnel (requestProtocol, proxyTunnel) { + return proxyTunnel === true || requestProtocol !== 'http:' +} + class Http1ProxyWrapper extends DispatcherBase { #client + #proxyServername - constructor (proxyUrl, { headers = {}, connect, factory }) { + constructor (proxyUrl, { headers = {}, connect, factory, proxyServername }) { if (!proxyUrl) { throw new InvalidArgumentError('Proxy URL is mandatory') } @@ -15659,6 +16246,7 @@ class Http1ProxyWrapper extends DispatcherBase { super() this[kProxyHeaders] = headers + this.#proxyServername = proxyServername if (factory) { this.#client = factory(proxyUrl, { connect }) } else { @@ -15693,6 +16281,13 @@ class Http1ProxyWrapper extends DispatcherBase { } opts.headers = { ...this[kProxyHeaders], ...headers } + // Pin the SNI/cert hostname to the proxy. Without this the underlying + // Client would derive it from the (rewritten) Host header, which points + // at the target — wrong for the TLS handshake to the proxy itself. + if (this.#proxyServername != null) { + opts.servername = this.#proxyServername + } + return this.#client[kDispatch](opts, handler) } @@ -15716,7 +16311,7 @@ class ProxyAgent extends DispatcherBase { throw new InvalidArgumentError('Proxy opts.clientFactory must be a function.') } - const { proxyTunnel = true, connectTimeout } = opts + const { proxyTunnel, connectTimeout } = opts super() @@ -15743,6 +16338,7 @@ class ProxyAgent extends DispatcherBase { } const connect = buildConnector({ timeout: connectTimeout, ...opts.proxyTls }) + const connectHTTP1 = buildConnector({ timeout: connectTimeout, ...opts.proxyTls, allowH2: false }) this[kConnectEndpoint] = buildConnector({ timeout: connectTimeout, ...opts.requestTls }) this[kConnectEndpointHTTP1] = buildConnector({ timeout: connectTimeout, ...opts.requestTls, allowH2: false }) @@ -15763,11 +16359,23 @@ class ProxyAgent extends DispatcherBase { }) } - if (!this[kTunnelProxy] && protocol === 'http:' && this[kProxy].protocol === 'http:') { + if (!shouldProxyTunnel(protocol, this[kTunnelProxy])) { + const forwardConnect = this[kProxy].protocol === 'https:' + ? (opts, cb) => connectHTTP1(opts, (err, socket) => { + if (err && err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { + cb(new SecureProxyConnectionError(err)) + } else { + cb(err, socket) + } + }) + : connectHTTP1 return new Http1ProxyWrapper(this[kProxy].uri, { headers: this[kProxyHeaders], - connect, - factory: agentFactory + connect: forwardConnect, + factory: agentFactory, + proxyServername: this[kProxy].protocol === 'https:' + ? (this[kProxyTls]?.servername || proxyHostname) + : undefined }) } return agentFactory(origin, options) @@ -15842,6 +16450,14 @@ class ProxyAgent extends DispatcherBase { if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { // Throw a custom error to avoid loop in client.js#connect callback(new SecureProxyConnectionError(err)) + } else if (err.code === 'UND_ERR_SOCKET') { + // A socket failure while establishing the tunnel means the CONNECT + // never completed, so there is nothing to recover - the proxy just + // tore down the connection. client.js#onError treats UND_ERR_SOCKET + // as a recoverable error on an established connection and leaves the + // request queued, which makes connect() retry forever. Surface it as + // a non-recoverable proxy error so the request fails instead. (#3897) + callback(new ProxyConnectionError(err)) } else { callback(err) } @@ -15957,14 +16573,14 @@ module.exports = ProxyAgent /***/ }), -/***/ 7418: +/***/ 1674: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const Dispatcher = __nccwpck_require__(1419) -const RetryHandler = __nccwpck_require__(5184) +const Dispatcher = __nccwpck_require__(7595) +const RetryHandler = __nccwpck_require__(1360) class RetryAgent extends Dispatcher { #agent = null @@ -16000,7 +16616,7 @@ module.exports = RetryAgent /***/ }), -/***/ 6520: +/***/ 7512: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -16014,14 +16630,14 @@ const { kGetDispatcher, kHasDispatcher, kRemoveClient -} = __nccwpck_require__(6488) -const Client = __nccwpck_require__(125) +} = __nccwpck_require__(7960) +const Client = __nccwpck_require__(3869) const { InvalidArgumentError -} = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) -const { kUrl } = __nccwpck_require__(451) -const buildConnector = __nccwpck_require__(9512) +} = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) +const { kUrl } = __nccwpck_require__(2019) +const buildConnector = __nccwpck_require__(7432) const kOptions = Symbol('options') const kConnections = Symbol('connections') @@ -16167,7 +16783,7 @@ module.exports = RoundRobinPool /***/ }), -/***/ 5567: +/***/ 1855: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -16176,12 +16792,12 @@ module.exports = RoundRobinPool const { URL } = __nccwpck_require__(3136) let tls // include tls conditionally since it is not always available -const DispatcherBase = __nccwpck_require__(1049) -const { InvalidArgumentError } = __nccwpck_require__(3291) -const { Socks5Client, STATES } = __nccwpck_require__(6346) -const { kDispatch, kClose, kDestroy } = __nccwpck_require__(451) -const Pool = __nccwpck_require__(1276) -const buildConnector = __nccwpck_require__(9512) +const DispatcherBase = __nccwpck_require__(713) +const { InvalidArgumentError } = __nccwpck_require__(4715) +const { Socks5Client, STATES } = __nccwpck_require__(4058) +const { kBusy, kConnected, kDispatch, kClose, kDestroy } = __nccwpck_require__(2019) +const Pool = __nccwpck_require__(1324) +const buildConnector = __nccwpck_require__(7432) const { debuglog } = __nccwpck_require__(7975) const debug = debuglog('undici:socks5-proxy') @@ -16399,6 +17015,20 @@ class Socks5ProxyAgent extends DispatcherBase { } }) this[kPools].set(originKey, pool) + + const closePoolIfUnused = () => { + if (this[kPools].get(originKey) !== pool || pool[kConnected] > 0 || pool[kBusy]) { + return + } + + this[kPools].delete(originKey) + if (!pool.destroyed) { + pool.close() + } + } + + pool.on('disconnect', closePoolIfUnused) + pool.on('connectionError', closePoolIfUnused) } // Dispatch the request through the per-origin pool @@ -16441,7 +17071,7 @@ module.exports = Socks5ProxyAgent /***/ }), -/***/ 5244: +/***/ 5372: /***/ ((module) => { "use strict"; @@ -16482,7 +17112,7 @@ module.exports = { /***/ }), -/***/ 6013: +/***/ 7949: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -16492,9 +17122,12 @@ module.exports = { // this version number must be increased to avoid conflicts. const globalDispatcher = Symbol.for('undici.globalDispatcher.2') const legacyGlobalDispatcher = Symbol.for('undici.globalDispatcher.1') -const { InvalidArgumentError } = __nccwpck_require__(3291) -const Agent = __nccwpck_require__(821) -const Dispatcher1Wrapper = __nccwpck_require__(8106) +const { InvalidArgumentError } = __nccwpck_require__(4715) +const Agent = __nccwpck_require__(5733) +const Dispatcher1Wrapper = __nccwpck_require__(2474) + +// Fallback storage for when globalThis is not extensible (e.g. frozen) +let fallbackDispatcher if (getGlobalDispatcher() === undefined) { setGlobalDispatcher(new Agent()) @@ -16505,25 +17138,42 @@ function setGlobalDispatcher (agent) { throw new InvalidArgumentError('Argument agent must implement Agent') } - Object.defineProperty(globalThis, globalDispatcher, { - value: agent, - writable: true, - enumerable: false, - configurable: false - }) + try { + Object.defineProperty(globalThis, globalDispatcher, { + value: agent, + writable: true, + enumerable: false, + configurable: false + }) + } catch (err) { + // globalThis is not extensible (e.g. Object.freeze(globalThis)) + // Use fallback storage instead + if (err instanceof TypeError) { + fallbackDispatcher = agent + return + } + throw err + } - const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent) + try { + const legacyAgent = agent instanceof Dispatcher1Wrapper ? agent : new Dispatcher1Wrapper(agent) - Object.defineProperty(globalThis, legacyGlobalDispatcher, { - value: legacyAgent, - writable: true, - enumerable: false, - configurable: false - }) + Object.defineProperty(globalThis, legacyGlobalDispatcher, { + value: legacyAgent, + writable: true, + enumerable: false, + configurable: false + }) + } catch (err) { + // globalThis is not extensible; fallback storage is already set + if (!(err instanceof TypeError)) { + throw err + } + } } function getGlobalDispatcher () { - return globalThis[globalDispatcher] + return globalThis[globalDispatcher] ?? fallbackDispatcher } // These are the globals that can be installed by undici.install(). @@ -16552,19 +17202,22 @@ module.exports = { /***/ }), -/***/ 8096: +/***/ 4272: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const util = __nccwpck_require__(648) +const util = __nccwpck_require__(824) const { parseCacheControlHeader, + hasInvalidCacheControlDirective, parseVaryHeader, + hasVaryStar, + isInvalidOrWildcardVaryHeader, isEtagUsable -} = __nccwpck_require__(3283) -const { parseHttpDate } = __nccwpck_require__(1109) +} = __nccwpck_require__(2627) +const { parseHttpDate } = __nccwpck_require__(1285) function noop () {} @@ -16584,6 +17237,95 @@ const NOT_UNDERSTOOD_STATUS_CODES = [ const MAX_RESPONSE_AGE = 2147483647000 +// Retention for revalidation-only entries (zero freshness lifetime but a +// validator present); each successful revalidation re-stores the entry. +const REVALIDATION_ONLY_RETENTION = 86400000 // 24 hours + +function trimOWS (value) { + return value.replace(/^[\t ]+|[\t ]+$/g, '') +} + +function arrayIncludes (array, value) { + for (let i = 0; i < array.length; i++) { + if (array[i] === value) { + return true + } + } + + return false +} + +function appendConnectionHeaderTokens (headersToRemove, connectionHeader) { + const values = Array.isArray(connectionHeader) ? connectionHeader : [connectionHeader] + + for (let i = 0; i < values.length; i++) { + const tokens = values[i].split(',') + for (let j = 0; j < tokens.length; j++) { + headersToRemove.push(trimOWS(tokens[j]).toLowerCase()) + } + } +} + +function getSameOriginPath (cacheKey, location) { + if (typeof location !== 'string') { + return undefined + } + + let originUrl + let requestUrl + let locationUrl + try { + originUrl = new URL(cacheKey.origin) + requestUrl = new URL(cacheKey.path, originUrl) + locationUrl = new URL(location, requestUrl) + } catch { + return undefined + } + + if (locationUrl.origin !== originUrl.origin) { + return undefined + } + + return locationUrl.pathname + locationUrl.search +} + +function deleteCachedUri (store, cacheKey, path) { + deleteCachedValue(store, { + ...cacheKey, + path + }) + + for (let i = 0; i < util.safeHTTPMethods.length; i++) { + const method = util.safeHTTPMethods[i] + if (method !== cacheKey.method) { + deleteCachedValue(store, { + ...cacheKey, + method, + path + }) + } + } +} + +function deleteLocationTargets (store, cacheKey, headerValue) { + if (headerValue === undefined) { + return + } + + const values = Array.isArray(headerValue) ? headerValue : [headerValue] + for (let i = 0; i < values.length; i++) { + const path = getSameOriginPath(cacheKey, values[i]) + if (path !== undefined) { + deleteCachedUri(store, cacheKey, path) + } + } +} + +function invalidateUnsafeRequest (store, cacheKey, resHeaders) { + deleteCachedUri(store, cacheKey, cacheKey.path) + deleteLocationTargets(store, cacheKey, resHeaders.location) + deleteLocationTargets(store, cacheKey, resHeaders['content-location']) +} /** * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler * @@ -16665,28 +17407,28 @@ class CacheHandler { const handler = this if ( - !util.safeHTTPMethods.includes(this.#cacheKey.method) && + !arrayIncludes(util.safeHTTPMethods, this.#cacheKey.method) && statusCode >= 200 && statusCode <= 399 ) { // Successful response to an unsafe method, delete it from cache // https://www.rfc-editor.org/rfc/rfc9111.html#name-invalidating-stored-response - try { - this.#store.delete(this.#cacheKey)?.catch?.(noop) - } catch { - // Fail silently - } + invalidateUnsafeRequest(this.#store, this.#cacheKey, resHeaders) return downstreamOnHeaders() } const cacheControlHeader = resHeaders['cache-control'] - const heuristicallyCacheable = resHeaders['last-modified'] && HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) + const heuristicallyCacheable = resHeaders['last-modified'] && arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode) if ( !cacheControlHeader && !resHeaders['expires'] && !heuristicallyCacheable && !this.#cacheByDefault ) { + if (statusCode === 304 && resHeaders.vary && isInvalidOrWildcardVaryHeader(resHeaders.vary)) { + deleteCachedValue(this.#store, this.#cacheKey) + } + // Don't have anything to tell us this response is cachable and we're not // caching by default return downstreamOnHeaders() @@ -16694,31 +17436,54 @@ class CacheHandler { const cacheControlDirectives = cacheControlHeader ? parseCacheControlHeader(cacheControlHeader) : {} if (!canCacheResponse(this.#cacheType, statusCode, resHeaders, cacheControlDirectives, this.#cacheKey.headers)) { + if (statusCode === 304 && (cacheControlHeader || revalidationResponseDisallowsCachedReuse(this.#cacheType, resHeaders, cacheControlDirectives))) { + deleteCachedValue(this.#store, this.#cacheKey) + } + return downstreamOnHeaders() } const now = Date.now() - const resAge = resHeaders.age ? getAge(resHeaders.age) : undefined - if (resAge && resAge >= MAX_RESPONSE_AGE) { + const resAge = Object.hasOwn(resHeaders, 'age') ? getAge(resHeaders.age) : undefined + if (resAge !== undefined && resAge >= MAX_RESPONSE_AGE) { // Response considered stale + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) return downstreamOnHeaders() } - const resDate = typeof resHeaders.date === 'string' - ? parseHttpDate(resHeaders.date) - : undefined + const resDate = Object.hasOwn(resHeaders, 'date') ? getDate(resHeaders.date) : undefined + if (resDate === null) { + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) + return downstreamOnHeaders() + } + + const apparentAge = resDate ? Math.max(0, now - resDate.getTime()) : 0 + const currentAge = Math.max(apparentAge, resAge ?? 0) + + const hasValidator = + (typeof resHeaders.etag === 'string' && isEtagUsable(resHeaders.etag)) || + typeof resHeaders['last-modified'] === 'string' const staleAt = - determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives) ?? + determineStaleAt(this.#cacheType, now, resAge, resHeaders, resDate, cacheControlDirectives, hasValidator) ?? this.#cacheByDefault - if (staleAt === undefined || (resAge && resAge > staleAt)) { + // Zero freshness lifetime but a validator: stale from the start, yet still + // storable since each reuse is preceded by a revalidation request. + // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.4 + const revalidationOnly = staleAt === 0 && hasValidator + if (staleAt === undefined || (currentAge >= staleAt && !revalidationOnly)) { + if (cacheControlHeader || staleAt !== undefined) { + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) + } + return downstreamOnHeaders() } - const baseTime = resDate ? resDate.getTime() : now + const baseTime = now - currentAge const absoluteStaleAt = staleAt + baseTime - if (now >= absoluteStaleAt) { + if (now >= absoluteStaleAt && !revalidationOnly) { // Response is already stale + deleteCachedValueIfNotModified(statusCode, this.#store, this.#cacheKey) return downstreamOnHeaders() } @@ -16731,8 +17496,8 @@ class CacheHandler { } } - const cachedAt = resAge ? now - resAge : now - const deleteAt = determineDeleteAt(baseTime, cachedAt, cacheControlDirectives, absoluteStaleAt) + const cachedAt = baseTime + const deleteAt = determineDeleteAt(baseTime, now, cacheControlDirectives, absoluteStaleAt) const strippedHeaders = stripNecessaryHeaders(resHeaders, cacheControlDirectives) /** @@ -16762,6 +17527,7 @@ class CacheHandler { value.statusCode = cachedValue.statusCode value.statusMessage = cachedValue.statusMessage value.etag = cachedValue.etag + value.vary = varyDirectives ?? cachedValue.vary value.headers = { ...cachedValue.headers, ...strippedHeaders } downstreamOnHeaders() @@ -16892,6 +17658,36 @@ class CacheHandler { } } +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheStore} store + * @param {import('../../types/cache-interceptor.d.ts').default.CacheKey} cacheKey + */ +function deleteCachedValue (store, cacheKey) { + try { + store.delete(cacheKey)?.catch?.(noop) + } catch { + // Fail silently + } +} + +function deleteCachedValueIfNotModified (statusCode, store, cacheKey) { + if (statusCode === 304) { + deleteCachedValue(store, cacheKey) + } +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders + * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * @returns {boolean} + */ +function revalidationResponseDisallowsCachedReuse (cacheType, resHeaders, cacheControlDirectives) { + return cacheControlDirectives['no-store'] === true || + (cacheType === 'shared' && cacheControlDirectives.private === true) || + (resHeaders.vary ? isInvalidOrWildcardVaryHeader(resHeaders.vary) : false) +} + /** * @see https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen * @@ -16903,12 +17699,12 @@ class CacheHandler { */ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirectives, reqHeaders) { // Status code must be final and understood. - if (statusCode < 200 || NOT_UNDERSTOOD_STATUS_CODES.includes(statusCode)) { + if (statusCode < 200 || arrayIncludes(NOT_UNDERSTOOD_STATUS_CODES, statusCode)) { return false } // Responses with neither status codes that are heuristically cacheable, nor "explicit enough" caching // directives, are not cacheable. "Explicit enough": see https://www.rfc-editor.org/rfc/rfc9111.html#section-3 - if (!HEURISTICALLY_CACHEABLE_STATUS_CODES.includes(statusCode) && !resHeaders['expires'] && + if (!arrayIncludes(HEURISTICALLY_CACHEABLE_STATUS_CODES, statusCode) && !resHeaders['expires'] && !cacheControlDirectives.public && cacheControlDirectives['max-age'] === undefined && // RFC 9111: a private response directive, if the cache is not shared @@ -16927,12 +17723,12 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect } // https://www.rfc-editor.org/rfc/rfc9111.html#section-4.1-5 - if (resHeaders.vary?.includes('*')) { + if (resHeaders.vary && hasVaryStar(resHeaders.vary)) { return false } // https://www.rfc-editor.org/rfc/rfc9111.html#name-storing-responses-to-authen - if (reqHeaders?.authorization) { + if (reqHeaders != null && Object.hasOwn(reqHeaders, 'authorization')) { if ( !cacheControlDirectives.public && !cacheControlDirectives['s-maxage'] && @@ -16947,14 +17743,14 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect if ( Array.isArray(cacheControlDirectives['no-cache']) && - cacheControlDirectives['no-cache'].includes('authorization') + arrayIncludes(cacheControlDirectives['no-cache'], 'authorization') ) { return false } if ( Array.isArray(cacheControlDirectives['private']) && - cacheControlDirectives['private'].includes('authorization') + arrayIncludes(cacheControlDirectives['private'], 'authorization') ) { return false } @@ -16963,14 +17759,51 @@ function canCacheResponse (cacheType, statusCode, resHeaders, cacheControlDirect return true } +/** + * @param {string | string[]} dateHeader + * @returns {Date | null | undefined} + */ +function getDate (dateHeader) { + let dateValue = dateHeader + if (Array.isArray(dateValue)) { + if (dateValue.length !== 1) { + return null + } + + dateValue = dateValue[0] + } + + if (typeof dateValue !== 'string') { + return null + } + + return parseHttpDate(dateValue) +} + /** * @param {string | string[]} ageHeader * @returns {number | undefined} */ function getAge (ageHeader) { - const age = parseInt(Array.isArray(ageHeader) ? ageHeader[0] : ageHeader) + let ageValue = ageHeader + if (Array.isArray(ageValue)) { + if (ageValue.length !== 1) { + return MAX_RESPONSE_AGE + } + + ageValue = ageValue[0] + } + + if (typeof ageValue !== 'string' || !/^[\t ]*[0-9]+[\t ]*$/.test(ageValue)) { + return MAX_RESPONSE_AGE + } + + const age = BigInt(ageValue.replace(/^[\t ]+|[\t ]+$/g, '')) + if (age >= BigInt(MAX_RESPONSE_AGE / 1000)) { + return MAX_RESPONSE_AGE + } - return isNaN(age) ? undefined : age * 1000 + return Number(age) * 1000 } /** @@ -16980,51 +17813,80 @@ function getAge (ageHeader) { * @param {import('../../types/header.d.ts').IncomingHttpHeaders} resHeaders * @param {Date | undefined} responseDate * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} cacheControlDirectives + * @param {boolean} hasValidator whether the response has a validator (etag or + * last-modified) that revalidation requests can be made with * * @returns {number | undefined} time that the value is stale at in seconds or undefined if it shouldn't be cached */ -function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives) { +function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheControlDirectives, hasValidator) { if (cacheType === 'shared') { // Prioritize s-maxage since we're a shared cache // s-maxage > max-age > Expire // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10-3 + if (hasInvalidCacheControlDirective(cacheControlDirectives, 's-maxage')) { + return 0 + } + const sMaxAge = cacheControlDirectives['s-maxage'] if (sMaxAge !== undefined) { - return sMaxAge > 0 ? sMaxAge * 1000 : undefined + if (sMaxAge > 0) { + return sMaxAge * 1000 + } + + // Immediately stale, but storable if we can revalidate it before reuse. + return 0 } } + if (hasInvalidCacheControlDirective(cacheControlDirectives, 'max-age')) { + return 0 + } + const maxAge = cacheControlDirectives['max-age'] if (maxAge !== undefined) { - return maxAge > 0 ? maxAge * 1000 : undefined + if (maxAge > 0) { + return maxAge * 1000 + } + + // Immediately stale, but storable if we can revalidate it before reuse. + return 0 } - if (typeof resHeaders.expires === 'string') { + if (Object.hasOwn(resHeaders, 'expires')) { // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.3 + if (typeof resHeaders.expires !== 'string') { + return 0 + } + const expiresDate = parseHttpDate(resHeaders.expires) - if (expiresDate) { - if (now >= expiresDate.getTime()) { - return undefined - } + if (!expiresDate) { + return 0 + } - if (responseDate) { - if (responseDate >= expiresDate) { - return undefined - } + if (now >= expiresDate.getTime()) { + return 0 + } - if (age !== undefined && age > (expiresDate - responseDate)) { - return undefined - } + if (responseDate) { + if (responseDate >= expiresDate) { + return 0 + } + + const freshnessLifetime = expiresDate.getTime() - responseDate.getTime() + if (age !== undefined && age >= freshnessLifetime) { + return 0 } - return expiresDate.getTime() - now + return freshnessLifetime } + + return expiresDate.getTime() - now } if (typeof resHeaders['last-modified'] === 'string') { // https://www.rfc-editor.org/rfc/rfc9111.html#name-calculating-heuristic-fresh - const lastModified = new Date(resHeaders['last-modified']) - if (isValidDate(lastModified)) { + const lastModified = parseHttpDate(resHeaders['last-modified']) + if (lastModified) { if (lastModified.getTime() >= now) { return undefined } @@ -17040,6 +17902,12 @@ function determineStaleAt (cacheType, now, age, resHeaders, responseDate, cacheC return 31536000000 } + if (cacheControlDirectives['no-cache'] === true && hasValidator) { + // No freshness source, but a validator lets us revalidate before reuse. + // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.4 + return 0 + } + return undefined } @@ -17076,6 +17944,11 @@ function determineDeleteAt (baseTime, cachedAt, cacheControlDirectives, staleAt) // revalidated. if (staleWhileRevalidate === -Infinity && staleIfError === -Infinity && immutable === -Infinity) { const freshnessLifetime = staleAt - baseTime + if (freshnessLifetime <= 0) { + // Revalidation-only entry: no freshness lifetime to size the buffer on, + // so retain it for a bounded window instead. + return cachedAt + REVALIDATION_ONLY_RETENTION + } const datePrecisionPadding = Math.min(Math.max(cachedAt - baseTime, 0), 1000) return staleAt + freshnessLifetime + datePrecisionPadding } @@ -17104,14 +17977,7 @@ function stripNecessaryHeaders (resHeaders, cacheControlDirectives) { ] if (resHeaders['connection']) { - if (Array.isArray(resHeaders['connection'])) { - // connection: a - // connection: b - headersToRemove.push(...resHeaders['connection'].map(header => header.trim())) - } else { - // connection: a, b - headersToRemove.push(...resHeaders['connection'].split(',').map(header => header.trim())) - } + appendConnectionHeaderTokens(headersToRemove, resHeaders['connection']) } if (Array.isArray(cacheControlDirectives['no-cache'])) { @@ -17124,7 +17990,7 @@ function stripNecessaryHeaders (resHeaders, cacheControlDirectives) { let strippedHeaders for (const headerName of headersToRemove) { - if (resHeaders[headerName]) { + if (Object.hasOwn(resHeaders, headerName)) { strippedHeaders ??= { ...resHeaders } delete strippedHeaders[headerName] } @@ -17133,20 +17999,12 @@ function stripNecessaryHeaders (resHeaders, cacheControlDirectives) { return strippedHeaders ?? resHeaders } -/** - * @param {Date} date - * @returns {boolean} - */ -function isValidDate (date) { - return date instanceof Date && Number.isFinite(date.valueOf()) -} - module.exports = CacheHandler /***/ }), -/***/ 6549: +/***/ 3125: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -17171,7 +18029,7 @@ class CacheRevalidationHandler { #successful = false /** - * @type {((boolean, any) => void) | null} + * @type {((success: boolean, context?: any, statusCode?: number, headers?: import('../../types/header.d.ts').IncomingHttpHeaders) => void) | null} */ #callback @@ -17188,7 +18046,7 @@ class CacheRevalidationHandler { #allowErrorStatusCodes /** - * @param {(boolean) => void} callback Function to call if the cached value is valid + * @param {(success: boolean, context?: any, statusCode?: number, headers?: import('../../types/header.d.ts').IncomingHttpHeaders) => void} callback Function to call if the cached value is valid * @param {import('../../types/dispatcher.d.ts').default.DispatchHandlers} handler * @param {boolean} allowErrorStatusCodes */ @@ -17223,7 +18081,7 @@ class CacheRevalidationHandler { // https://datatracker.ietf.org/doc/html/rfc5861#section-4 this.#successful = statusCode === 304 || (this.#allowErrorStatusCodes && statusCode >= 500 && statusCode <= 504) - this.#callback(this.#successful, this.#context) + this.#callback(this.#successful, this.#context, statusCode, headers) this.#callback = null if (this.#successful) { @@ -17261,6 +18119,16 @@ class CacheRevalidationHandler { } if (this.#callback) { + // Serve the stale cached response on a connection error, per stale-if-error: + // RFC 5861 counts an unreachable origin (a would-be 5xx) as an error. + // https://datatracker.ietf.org/doc/html/rfc5861#section-4 + if (this.#allowErrorStatusCodes) { + this.#successful = true + this.#callback(true, this.#context) + this.#callback = null + return + } + this.#callback(false) this.#callback = null } @@ -17278,7 +18146,7 @@ module.exports = CacheRevalidationHandler /***/ }), -/***/ 5683: +/***/ 5043: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -17352,13 +18220,13 @@ module.exports = class DecoratorHandler { /***/ }), -/***/ 2295: +/***/ 8583: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { RequestAbortedError } = __nccwpck_require__(3291) +const { RequestAbortedError } = __nccwpck_require__(4715) /** * @typedef {import('../../types/dispatcher.d.ts').default.DispatchHandler} DispatchHandler @@ -17820,15 +18688,15 @@ module.exports = DeduplicationHandler /***/ }), -/***/ 474: +/***/ 1274: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const util = __nccwpck_require__(648) +const util = __nccwpck_require__(824) const assert = __nccwpck_require__(4589) -const { InvalidArgumentError } = __nccwpck_require__(3291) +const { InvalidArgumentError } = __nccwpck_require__(4715) const redirectableStatusCodes = [300, 301, 302, 303, 307, 308] @@ -17878,15 +18746,19 @@ class RedirectHandler { throw new Error('max redirects') } + let removeContentHeaders = statusCode === 303 + // https://tools.ietf.org/html/rfc7231#section-6.4.2 // https://fetch.spec.whatwg.org/#http-redirect-fetch // In case of HTTP 301 or 302 with POST, change the method to GET + // QUERY is safe (RFC 10008) and should not change method like GET. if ((statusCode === 301 || statusCode === 302) && this.opts.method === 'POST') { this.opts.method = 'GET' if (util.isStream(this.opts.body)) { util.destroy(this.opts.body.on('error', noop)) } this.opts.body = null + removeContentHeaders = true } // https://tools.ietf.org/html/rfc7231#section-6.4.4 @@ -17926,9 +18798,9 @@ class RedirectHandler { } // Remove headers referring to the original URL. - // By default it is Host only, unless it's a 303 (see below), which removes also all Content-* headers. + // By default it is Host only. A 303 or a 301/302 POST-to-GET redirect also removes all Content-* headers. // https://tools.ietf.org/html/rfc7231#section-6.4 - this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin, this.stripHeadersOnRedirect, this.stripHeadersOnCrossOriginRedirect) + this.opts.headers = cleanRequestHeaders(this.opts.headers, removeContentHeaders, this.opts.origin !== origin, this.stripHeadersOnRedirect, this.stripHeadersOnCrossOriginRedirect) this.opts.path = path this.opts.origin = origin this.opts.query = null @@ -18045,24 +18917,66 @@ module.exports = RedirectHandler /***/ }), -/***/ 5184: +/***/ 1360: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const assert = __nccwpck_require__(4589) -const { kRetryHandlerDefaultRetry } = __nccwpck_require__(451) -const { RequestRetryError } = __nccwpck_require__(3291) +const { kRetryHandlerDefaultRetry } = __nccwpck_require__(2019) +const { RequestRetryError } = __nccwpck_require__(4715) const { isDisturbed, parseRangeHeader, wrapRequestBody -} = __nccwpck_require__(648) +} = __nccwpck_require__(824) function calculateRetryAfterHeader (retryAfter) { const retryTime = new Date(retryAfter).getTime() - return isNaN(retryTime) ? 0 : retryTime - Date.now() + return isNaN(retryTime) ? null : retryTime - Date.now() +} + +function validatePartialResponseContentLength (headers, range, statusCode, retryCount) { + const contentLength = headers['content-length'] + if (contentLength == null) { + return + } + + if (!Number.isFinite(range.start) || !Number.isFinite(range.end)) { + return + } + + const length = Number(contentLength) + const expectedLength = range.end - range.start + 1 + if (!Number.isFinite(length) || length !== expectedLength) { + throw new RequestRetryError('Content-Length mismatch', statusCode, { + headers, + data: { count: retryCount } + }) + } +} + +// A stable controller handed to the downstream handler for the lifetime of the +// request. Each transparent retry/resume is a *separate* dispatch with its +// *own* connection controller. Without a stable proxy the downstream body keeps +// flow-controlling the original (now-dead) controller while data flows on the +// new one: backpressure pauses the new connection's controller, but the +// consumer's resume() targets the old one, so the resumed body stalls forever. +// The proxy always forwards to the controller of the currently active connection. +class RetryController { + constructor () { + this.target = null + } + + pause () { this.target?.pause() } + resume () { this.target?.resume() } + abort (reason) { this.target?.abort(reason) } + get paused () { return this.target?.paused ?? false } + get aborted () { return this.target?.aborted ?? false } + get reason () { return this.target?.reason ?? null } + get rawHeaders () { return this.target?.rawHeaders ?? null } + get rawTrailers () { return this.target?.rawTrailers ?? null } } class RetryHandler { @@ -18096,7 +19010,7 @@ class RetryHandler { timeoutFactor: timeoutFactor ?? 2, maxRetries: maxRetries ?? 5, // What errors we should retry - methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE', 'QUERY'], // Indicates which errors to retry statusCodes: statusCodes ?? [500, 502, 503, 504, 429], // List of errors to retry @@ -18121,6 +19035,7 @@ class RetryHandler { this.etag = null this.statusCode = null this.headers = null + this.controllerProxy = new RetryController() } onResponseStartWithRetry (controller, statusCode, headers, statusMessage, err) { @@ -18128,7 +19043,7 @@ class RetryHandler { // Preserve old behavior for status codes that are not eligible for retry if (this.retryOpts.statusCodes.includes(statusCode) === false) { this.headersSent = true - this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage) + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) } else { this.error = err } @@ -18138,14 +19053,14 @@ class RetryHandler { if (isDisturbed(this.opts.body)) { this.headersSent = true - this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage) + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) return } function shouldRetry (passedErr) { if (passedErr) { this.headersSent = true - this.handler.onResponseStart?.(controller, statusCode, headers, statusMessage) + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) controller.resume() return } @@ -18154,6 +19069,13 @@ class RetryHandler { controller.resume() } + // The pause()/resume() pair (here and in shouldRetry) acts on THIS + // connection's controller -- never the downstream proxy. We hold this exact + // connection while the retry policy decides (possibly after a timeout) and + // must resume the same one. Routing this through controllerProxy would risk + // resuming a different connection if a later dispatch re-points the proxy in + // between, leaving this one paused forever -- the very stall the proxy exists + // to prevent. controller.pause() this.retryOpts.retry( err, @@ -18166,13 +19088,19 @@ class RetryHandler { } onRequestStart (controller, context) { + // request.js creates a fresh RequestController per dispatch and passes that + // same instance to every later callback of the dispatch. onRequestStart is + // the first callback (it is where the controller is created), so re-pointing + // the proxy here is enough to keep it on the active connection across every + // transparent retry/resume. + this.controllerProxy.target = controller if (!this.headersSent) { - this.handler.onRequestStart?.(controller, context) + this.handler.onRequestStart?.(this.controllerProxy, context) } } - onRequestUpgrade (controller, statusCode, headers, socket) { - this.handler.onRequestUpgrade?.(controller, statusCode, headers, socket) + onRequestUpgrade (_controller, statusCode, headers, socket) { + this.handler.onRequestUpgrade?.(this.controllerProxy, statusCode, headers, socket) } static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { @@ -18226,14 +19154,21 @@ class RetryHandler { } const retryTimeout = - retryAfterHeader > 0 - ? Math.min(retryAfterHeader, maxTimeout) - : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout) + retryAfterHeader === 0 + ? 0 + : retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(minTimeout * timeoutFactor ** (counter - 1), maxTimeout) setTimeout(() => cb(null), retryTimeout) } onResponseStart (controller, statusCode, headers, statusMessage) { + if (statusCode < 200) { + this.handler.onResponseStart?.(this.controllerProxy, statusCode, headers, statusMessage) + return + } + this.error = null this.retryCount += 1 this.statusCode = statusCode @@ -18283,6 +19218,8 @@ class RetryHandler { }) } + validatePartialResponseContentLength(headers, contentRange, statusCode, this.retryCount) + const { start, size, end = size ? size - 1 : null } = contentRange assert(this.start === start, 'content-range mismatch') @@ -18296,10 +19233,10 @@ class RetryHandler { // First time we receive 206 const range = parseRangeHeader(headers['content-range']) - if (range == null) { + if (range == null || range.end == null) { this.headersSent = true this.handler.onResponseStart?.( - controller, + this.controllerProxy, statusCode, headers, statusMessage @@ -18307,6 +19244,8 @@ class RetryHandler { return } + validatePartialResponseContentLength(headers, range, statusCode, this.retryCount) + const { start, size, end = size ? size - 1 : null } = range assert( start != null && Number.isFinite(start), @@ -18319,7 +19258,7 @@ class RetryHandler { } // We make our best to checkpoint the body for further range headers - if (this.end == null) { + if (this.end == null && this.opts.method !== 'HEAD') { const contentLength = headers['content-length'] this.end = contentLength != null ? Number(contentLength) - 1 : null } @@ -18346,7 +19285,7 @@ class RetryHandler { this.headersSent = true this.handler.onResponseStart?.( - controller, + this.controllerProxy, statusCode, headers, statusMessage @@ -18359,17 +19298,17 @@ class RetryHandler { } } - onResponseData (controller, chunk) { + onResponseData (_controller, chunk) { if (this.error) { return } this.start += chunk.length - this.handler.onResponseData?.(controller, chunk) + this.handler.onResponseData?.(this.controllerProxy, chunk) } - onResponseEnd (controller, trailers) { + onResponseEnd (_controller, trailers) { if (this.error && this.retryOpts.throwOnError) { throw this.error } @@ -18386,13 +19325,13 @@ class RetryHandler { } } this.retryCount = 0 - return this.handler.onResponseEnd?.(controller, trailers) + return this.handler.onResponseEnd?.(this.controllerProxy, trailers) } - this.retry(controller) + this.retry() } - retry (controller) { + retry () { if (this.start !== 0) { const headers = { range: `bytes=${this.start}-${this.end ?? ''}` } @@ -18414,23 +19353,25 @@ class RetryHandler { this.retryCountCheckpoint = this.retryCount this.dispatch(this.opts, this) } catch (err) { - this.handler.onResponseError?.(controller, err) + this.handler.onResponseError?.(this.controllerProxy, err) } } onResponseError (controller, err) { + // controller is THIS failed connection (not the proxy): we inspect whether + // the consumer aborted it to decide retry-vs-propagate. if (controller?.aborted || isDisturbed(this.opts.body)) { - this.handler.onResponseError?.(controller, err) + this.handler.onResponseError?.(this.controllerProxy, err) return } function shouldRetry (returnedErr) { if (!returnedErr) { - this.retry(controller) + this.retry() return } - this.handler?.onResponseError?.(controller, returnedErr) + this.handler?.onResponseError?.(this.controllerProxy, returnedErr) } // We reconcile in case of a mix between network errors @@ -18460,7 +19401,7 @@ module.exports = RetryHandler /***/ }), -/***/ 6590: +/***/ 334: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -18468,12 +19409,13 @@ module.exports = RetryHandler const assert = __nccwpck_require__(4589) const { Readable } = __nccwpck_require__(7075) -const util = __nccwpck_require__(648) -const CacheHandler = __nccwpck_require__(8096) -const MemoryCacheStore = __nccwpck_require__(1) -const CacheRevalidationHandler = __nccwpck_require__(6549) -const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader } = __nccwpck_require__(3283) -const { AbortError } = __nccwpck_require__(3291) +const util = __nccwpck_require__(824) +const CacheHandler = __nccwpck_require__(4272) +const MemoryCacheStore = __nccwpck_require__(1915) +const CacheRevalidationHandler = __nccwpck_require__(3125) +const { assertCacheStore, assertCacheMethods, makeCacheKey, normalizeHeaders, parseCacheControlHeader, isInvalidOrWildcardVaryHeader } = __nccwpck_require__(2627) +const { AbortError } = __nccwpck_require__(4715) +const { parseHttpDate } = __nccwpck_require__(1285) /** * @param {(string | RegExp)[] | undefined} origins @@ -18494,6 +19436,44 @@ function assertCacheOrigins (origins, name) { const nop = () => {} +function trimOWS (value) { + return value.replace(/^[\t ]+|[\t ]+$/g, '') +} + +function arrayIncludes (array, value) { + for (let i = 0; i < array.length; i++) { + if (array[i] === value) { + return true + } + } + + return false +} + +function hasPragmaNoCache (headers) { + const pragma = headers?.pragma + if (!pragma) { + return false + } + + const values = Array.isArray(pragma) ? pragma : [pragma] + for (let i = 0; i < values.length; i++) { + const value = values[i] + if (typeof value !== 'string') { + continue + } + + const directives = value.split(',') + for (let j = 0; j < directives.length; j++) { + if (trimOWS(directives[j]).toLowerCase() === 'no-cache') { + return true + } + } + } + + return false +} + /** * @typedef {(options: import('../../types/dispatcher.d.ts').default.DispatchOptions, handler: import('../../types/dispatcher.d.ts').default.DispatchHandler) => void} DispatchFn */ @@ -18523,16 +19503,92 @@ function needsRevalidation (result, cacheControlDirectives, { headers = {} }) { return false } +/** + * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @returns {boolean} + */ +function staleResponseRequiresRevalidation (result, cacheType) { + return result.cacheControlDirectives?.['must-revalidate'] === true || + (cacheType === 'shared' && ( + result.cacheControlDirectives?.['proxy-revalidate'] === true || + // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.2.10 + // s-maxage implies proxy-revalidate for shared caches. + result.cacheControlDirectives?.['s-maxage'] !== undefined + )) +} + +/** + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType + * @param {import('../../types/header.d.ts').IncomingHttpHeaders} headers + * @returns {boolean} + */ +function revalidationResponseDisallowsCachedReuse (cacheType, headers) { + if (headers.vary && isInvalidOrWildcardVaryHeader(headers.vary)) { + return true + } + + const cacheControl = headers['cache-control'] + if (!cacheControl) { + return false + } + + const cacheControlDirectives = parseCacheControlHeader(cacheControl) + return cacheControlDirectives['no-store'] === true || + (cacheType === 'shared' && cacheControlDirectives.private === true) +} + +function revalidationResponseUpdatesCacheControl (headers) { + return headers['cache-control'] !== undefined +} + +function deleteCachedValue (store, cacheKey) { + try { + store.delete(cacheKey)?.catch?.(nop) + } catch { + // Fail silently + } +} + +function getUsableLastModified (headers) { + const lastModified = headers?.['last-modified'] + if (typeof lastModified === 'string' && parseHttpDate(lastModified)) { + return lastModified + } +} + +function makeRevalidationHeaders (opts, result) { + const headers = { + ...opts.headers, + 'if-modified-since': getUsableLastModified(result.headers) ?? new Date(result.cachedAt).toUTCString() + } + + if (result.etag) { + headers['if-none-match'] = result.etag + } + + if (result.vary) { + for (const key in result.vary) { + if (result.vary[key] != null) { + headers[key] = result.vary[key] + } + } + } + + return headers +} + /** * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result * @param {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives | undefined} cacheControlDirectives + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType * @returns {boolean} */ -function isStale (result, cacheControlDirectives) { +function isStale (result, cacheControlDirectives, cacheType) { const now = Date.now() if (now > result.staleAt) { // Response is stale - if (cacheControlDirectives?.['max-stale']) { + if (!staleResponseRequiresRevalidation(result, cacheType) && cacheControlDirectives?.['max-stale']) { // There's a threshold where we can serve stale responses, let's see if // we're in it // https://www.rfc-editor.org/rfc/rfc9111.html#name-max-stale @@ -18559,11 +19615,12 @@ function isStale (result, cacheControlDirectives) { /** * Check if we're within the stale-while-revalidate window for a stale response * @param {import('../../types/cache-interceptor.d.ts').default.GetResult} result + * @param {import('../../types/cache-interceptor.d.ts').default.CacheOptions['type']} cacheType * @returns {boolean} */ -function withinStaleWhileRevalidateWindow (result) { +function withinStaleWhileRevalidateWindow (result, cacheType) { const staleWhileRevalidate = result.cacheControlDirectives?.['stale-while-revalidate'] - if (!staleWhileRevalidate) { + if (!staleWhileRevalidate || staleResponseRequiresRevalidation(result, cacheType)) { return false } @@ -18746,14 +19803,10 @@ function handleResult ( } const age = Math.round((now - result.cachedAt) / 1000) - if (reqCacheControl?.['max-age'] && age >= reqCacheControl['max-age']) { - // Response is considered expired for this specific request - // https://www.rfc-editor.org/rfc/rfc9111.html#section-5.2.1.1 - return dispatch(opts, handler) - } + const requestMaxAgeExpired = reqCacheControl?.['max-age'] !== undefined && age >= reqCacheControl['max-age'] - const stale = isStale(result, reqCacheControl) - const revalidate = needsRevalidation(result, reqCacheControl, opts) + const stale = requestMaxAgeExpired || isStale(result, reqCacheControl, globalOpts.type) + const revalidate = requestMaxAgeExpired || needsRevalidation(result, reqCacheControl, opts) // Check if the response is stale if (stale || revalidate) { @@ -18765,28 +19818,13 @@ function handleResult ( // RFC 5861: If we're within stale-while-revalidate window, serve stale immediately // and revalidate in background, unless immediate revalidation is necessary - if (!revalidate && withinStaleWhileRevalidateWindow(result)) { + if (!revalidate && withinStaleWhileRevalidateWindow(result, globalOpts.type)) { // Serve stale response immediately sendCachedValue(handler, opts, result, age, null, true) // Start background revalidation (fire-and-forget) queueMicrotask(() => { - const headers = { - ...opts.headers, - 'if-modified-since': new Date(result.cachedAt).toUTCString() - } - - if (result.etag) { - headers['if-none-match'] = result.etag - } - - if (result.vary) { - for (const key in result.vary) { - if (result.vary[key] != null) { - headers[key] = result.vary[key] - } - } - } + const headers = makeRevalidationHeaders(opts, result) // Background revalidation - update cache if we get new data dispatch( @@ -18810,28 +19848,15 @@ function handleResult ( } let withinStaleIfErrorThreshold = false - const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error'] - if (staleIfErrorExpiry) { - withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000)) - } - - const headers = { - ...opts.headers, - 'if-modified-since': new Date(result.cachedAt).toUTCString() - } - - if (result.etag) { - headers['if-none-match'] = result.etag - } - - if (result.vary) { - for (const key in result.vary) { - if (result.vary[key] != null) { - headers[key] = result.vary[key] - } + if (!staleResponseRequiresRevalidation(result, globalOpts.type)) { + const staleIfErrorExpiry = result.cacheControlDirectives['stale-if-error'] ?? reqCacheControl?.['stale-if-error'] + if (staleIfErrorExpiry) { + withinStaleIfErrorThreshold = now < (result.staleAt + (staleIfErrorExpiry * 1000)) } } + const headers = makeRevalidationHeaders(opts, result) + // We need to revalidate the response return dispatch( { @@ -18839,8 +19864,23 @@ function handleResult ( headers }, new CacheRevalidationHandler( - (success, context) => { + (success, context, statusCode, headers) => { if (success) { + if (statusCode === 304) { + if (revalidationResponseDisallowsCachedReuse(globalOpts.type, headers)) { + if (util.isStream(result.body)) { + result.body.on('error', nop).destroy() + } + + deleteCachedValue(globalOpts.store, cacheKey) + return dispatch(opts, new CacheHandler(globalOpts, cacheKey, handler)) + } + + if (revalidationResponseUpdatesCacheControl(headers)) { + deleteCachedValue(globalOpts.store, cacheKey) + } + } + // TODO: successful revalidation should be considered fresh (not give stale warning). sendCachedValue(handler, opts, result, age, context, stale) } else if (util.isStream(result.body)) { @@ -18897,17 +19937,26 @@ module.exports = (opts = {}) => { type } - const safeMethodsToNotCache = util.safeHTTPMethods.filter(method => methods.includes(method) === false) + const safeMethodsToNotCache = [] + for (let i = 0; i < util.safeHTTPMethods.length; i++) { + const method = util.safeHTTPMethods[i] + if (!arrayIncludes(methods, method)) { + safeMethodsToNotCache.push(method) + } + } return dispatch => { return (opts, handler) => { - if (!opts.origin || safeMethodsToNotCache.includes(opts.method)) { - // Not a method we want to cache or we don't have the origin, skip + if (arrayIncludes(safeMethodsToNotCache, opts.method)) { + // Not a method we want to cache, skip return dispatch(opts, handler) } // Check if origin is in whitelist if (origins !== undefined) { + if (!opts.origin) { + return dispatch(opts, handler) + } const requestOrigin = opts.origin.toString().toLowerCase() let isAllowed = false @@ -18936,7 +19985,9 @@ module.exports = (opts = {}) => { const reqCacheControl = opts.headers?.['cache-control'] ? parseCacheControlHeader(opts.headers['cache-control']) - : undefined + : hasPragmaNoCache(opts.headers) + ? { 'no-cache': true } + : undefined if (reqCacheControl?.['no-store']) { return dispatch(opts, handler) @@ -18976,7 +20027,7 @@ module.exports = (opts = {}) => { /***/ }), -/***/ 6661: +/***/ 917: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -18984,7 +20035,7 @@ module.exports = (opts = {}) => { const { createInflate, createGunzip, createBrotliDecompress, createZstdDecompress } = __nccwpck_require__(8522) const { pipeline } = __nccwpck_require__(7075) -const DecoratorHandler = __nccwpck_require__(5683) +const DecoratorHandler = __nccwpck_require__(5043) /** @typedef {import('node:stream').Transform} Transform */ /** @typedef {import('node:stream').Transform} Controller */ @@ -19014,6 +20065,8 @@ let warningEmitted = /** @type {boolean} */ (false) class DecompressHandler extends DecoratorHandler { /** @type {Transform[]} */ #decompressors = [] + /** @type {Record | undefined} */ + #trailers /** @type {Readonly} */ #skipStatusCodes /** @type {boolean} */ @@ -19105,7 +20158,7 @@ class DecompressHandler extends DecoratorHandler { this.#setupDecompressorEvents(decompressor, controller) decompressor.on('end', () => { - super.onResponseEnd(controller, {}) + super.onResponseEnd(controller, this.#trailers) }) } @@ -19123,7 +20176,7 @@ class DecompressHandler extends DecoratorHandler { super.onResponseError(controller, err) return } - super.onResponseEnd(controller, {}) + super.onResponseEnd(controller, this.#trailers) }) } @@ -19218,6 +20271,7 @@ class DecompressHandler extends DecoratorHandler { */ onResponseEnd (controller, trailers) { if (this.#decompressors.length > 0) { + this.#trailers = trailers this.#decompressors[0].end() this.#cleanupDecompressors() return @@ -19258,6 +20312,10 @@ function createDecompressInterceptor (options = {}) { return (dispatch) => { return (opts, handler) => { + if (opts.method === 'HEAD') { + return dispatch(opts, handler) + } + const decompressHandler = new DecompressHandler(handler, options) return dispatch(opts, decompressHandler) } @@ -19269,16 +20327,16 @@ module.exports = createDecompressInterceptor /***/ }), -/***/ 1824: +/***/ 8064: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const diagnosticsChannel = __nccwpck_require__(3053) -const util = __nccwpck_require__(648) -const DeduplicationHandler = __nccwpck_require__(2295) -const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = __nccwpck_require__(3283) +const util = __nccwpck_require__(824) +const DeduplicationHandler = __nccwpck_require__(8583) +const { normalizeHeaders, makeCacheKey, makeDeduplicationKey } = __nccwpck_require__(2627) const pendingRequestsChannel = diagnosticsChannel.channel('undici:request:pending-requests') @@ -19334,7 +20392,7 @@ module.exports = (opts = {}) => { return dispatch => { return (opts, handler) => { - if (!opts.origin || methods.includes(opts.method) === false) { + if (opts.upgrade || methods.includes(opts.method) === false) { return dispatch(opts, handler) } @@ -19394,15 +20452,15 @@ module.exports = (opts = {}) => { /***/ }), -/***/ 5955: +/***/ 6003: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const { isIP } = __nccwpck_require__(7030) const { lookup } = __nccwpck_require__(610) -const DecoratorHandler = __nccwpck_require__(5683) -const { InvalidArgumentError, InformationalError } = __nccwpck_require__(3291) +const DecoratorHandler = __nccwpck_require__(5043) +const { InvalidArgumentError, InformationalError } = __nccwpck_require__(4715) const maxInt = Math.pow(2, 31) - 1 function hasSafeIterator (headers) { @@ -19977,14 +21035,14 @@ module.exports = interceptorOpts => { /***/ }), -/***/ 1684: +/***/ 6596: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(3291) -const DecoratorHandler = __nccwpck_require__(5683) +const { InvalidArgumentError, RequestAbortedError } = __nccwpck_require__(4715) +const DecoratorHandler = __nccwpck_require__(5043) class DumpHandler extends DecoratorHandler { #maxSize = 1024 * 1024 @@ -20097,13 +21155,13 @@ module.exports = createDumpInterceptor /***/ }), -/***/ 3074: +/***/ 4546: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const RedirectHandler = __nccwpck_require__(474) +const RedirectHandler = __nccwpck_require__(1274) function createRedirectInterceptor ({ maxRedirections: defaultMaxRedirections, throwOnMaxRedirect: defaultThrowOnMaxRedirect, stripHeadersOnRedirect: defaultStripHeadersOnRedirect, stripHeadersOnCrossOriginRedirect: defaultStripHeadersOnCrossOriginRedirect } = {}) { return (dispatch) => { @@ -20126,15 +21184,15 @@ module.exports = createRedirectInterceptor /***/ }), -/***/ 9870: +/***/ 9534: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; // const { parseHeaders } = require('../core/util') -const DecoratorHandler = __nccwpck_require__(5683) -const { ResponseError } = __nccwpck_require__(3291) +const DecoratorHandler = __nccwpck_require__(5043) +const { ResponseError } = __nccwpck_require__(4715) class ResponseErrorHandler extends DecoratorHandler { #statusCode @@ -20229,12 +21287,12 @@ module.exports = () => { /***/ }), -/***/ 8450: +/***/ 2194: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const RetryHandler = __nccwpck_require__(5184) +const RetryHandler = __nccwpck_require__(1360) module.exports = globalOpts => { return dispatch => { @@ -20256,14 +21314,14 @@ module.exports = globalOpts => { /***/ }), -/***/ 6368: +/***/ 1280: /***/ ((__unused_webpack_module, exports, __nccwpck_require__) => { "use strict"; Object.defineProperty(exports, "__esModule", ({ value: true })); exports.SPECIAL_HEADERS = exports.MINOR = exports.MAJOR = exports.HTAB_SP_VCHAR_OBS_TEXT = exports.QUOTED_STRING = exports.CONNECTION_TOKEN_CHARS = exports.HEADER_CHARS = exports.TOKEN = exports.HEX = exports.URL_CHAR = exports.USERINFO_CHARS = exports.MARK = exports.ALPHANUM = exports.NUM = exports.HEX_MAP = exports.NUM_MAP = exports.ALPHA = exports.STATUSES_HTTP = exports.H_METHOD_MAP = exports.METHOD_MAP = exports.METHODS_RTSP = exports.METHODS_ICE = exports.METHODS_HTTP = exports.HEADER_STATE = exports.FINISH = exports.STATUSES = exports.METHODS = exports.LENIENT_FLAGS = exports.FLAGS = exports.TYPE = exports.ERROR = void 0; -const utils_1 = __nccwpck_require__(1780); +const utils_1 = __nccwpck_require__(3348); // Emums exports.ERROR = { OK: 0, @@ -20795,7 +21853,7 @@ exports["default"] = { /***/ }), -/***/ 678: +/***/ 2038: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -20819,7 +21877,7 @@ Object.defineProperty(module, 'exports', { /***/ }), -/***/ 2354: +/***/ 2242: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -20843,7 +21901,7 @@ Object.defineProperty(module, 'exports', { /***/ }), -/***/ 1780: +/***/ 3348: /***/ ((__unused_webpack_module, exports) => { "use strict"; @@ -20863,14 +21921,14 @@ function enumToMap(obj, filter = [], exceptions = []) { /***/ }), -/***/ 3253: +/***/ 3301: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { kClients } = __nccwpck_require__(451) -const Agent = __nccwpck_require__(821) +const { kClients } = __nccwpck_require__(2019) +const Agent = __nccwpck_require__(5733) const { kAgent, kMockAgentSet, @@ -20888,14 +21946,14 @@ const { kMockAgentAcceptsNonStandardSearchParameters, kMockCallHistoryAddLog, kIgnoreTrailingSlash -} = __nccwpck_require__(5301) -const MockClient = __nccwpck_require__(1421) -const MockPool = __nccwpck_require__(252) -const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = __nccwpck_require__(5277) -const { InvalidArgumentError, UndiciError } = __nccwpck_require__(3291) -const Dispatcher = __nccwpck_require__(1419) -const PendingInterceptorsFormatter = __nccwpck_require__(4486) -const { MockCallHistory } = __nccwpck_require__(8535) +} = __nccwpck_require__(9045) +const MockClient = __nccwpck_require__(6333) +const MockPool = __nccwpck_require__(380) +const { matchValue, normalizeSearchParams, buildAndValidateMockOptions, normalizeOrigin } = __nccwpck_require__(5325) +const { InvalidArgumentError, UndiciError } = __nccwpck_require__(4715) +const Dispatcher = __nccwpck_require__(7595) +const PendingInterceptorsFormatter = __nccwpck_require__(4966) +const { MockCallHistory } = __nccwpck_require__(2791) class MockAgent extends Dispatcher { constructor (opts = {}) { @@ -21103,14 +22161,14 @@ module.exports = MockAgent /***/ }), -/***/ 8535: +/***/ 2791: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { kMockCallHistoryAddLog } = __nccwpck_require__(5301) -const { InvalidArgumentError } = __nccwpck_require__(3291) +const { kMockCallHistoryAddLog } = __nccwpck_require__(9045) +const { InvalidArgumentError } = __nccwpck_require__(4715) function handleFilterCallsWithOptions (criteria, options, handler, store, allLogs) { switch (options.operator) { @@ -21359,15 +22417,15 @@ module.exports.MockCallHistoryLog = MockCallHistoryLog /***/ }), -/***/ 1421: +/***/ 6333: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const { promisify } = __nccwpck_require__(7975) -const Client = __nccwpck_require__(125) -const { buildMockDispatch } = __nccwpck_require__(5277) +const Client = __nccwpck_require__(3869) +const { buildMockDispatch } = __nccwpck_require__(5325) const { kDispatches, kMockAgent, @@ -21377,10 +22435,10 @@ const { kOriginalDispatch, kConnected, kIgnoreTrailingSlash -} = __nccwpck_require__(5301) -const { MockInterceptor } = __nccwpck_require__(4287) -const Symbols = __nccwpck_require__(451) -const { InvalidArgumentError } = __nccwpck_require__(3291) +} = __nccwpck_require__(9045) +const { MockInterceptor } = __nccwpck_require__(463) +const Symbols = __nccwpck_require__(2019) +const { InvalidArgumentError } = __nccwpck_require__(4715) /** * MockClient provides an API that extends the Client to influence the mockDispatches. @@ -21435,13 +22493,13 @@ module.exports = MockClient /***/ }), -/***/ 4805: +/***/ 6069: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { UndiciError } = __nccwpck_require__(3291) +const { UndiciError } = __nccwpck_require__(4715) const kMockNotMatchedError = Symbol.for('undici.error.UND_MOCK_ERR_MOCK_NOT_MATCHED') @@ -21472,13 +22530,13 @@ module.exports = { /***/ }), -/***/ 4287: +/***/ 463: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { getResponseData, buildKey, addMockDispatch } = __nccwpck_require__(5277) +const { getResponseData, buildKey, addMockDispatch } = __nccwpck_require__(5325) const { kDispatches, kDispatchKey, @@ -21487,9 +22545,14 @@ const { kContentLength, kMockDispatch, kIgnoreTrailingSlash -} = __nccwpck_require__(5301) -const { InvalidArgumentError } = __nccwpck_require__(3291) -const { serializePathWithQuery } = __nccwpck_require__(648) +} = __nccwpck_require__(9045) +const { InvalidArgumentError } = __nccwpck_require__(4715) +const { serializePathWithQuery } = __nccwpck_require__(824) +const { + types: { + isPromise + } +} = __nccwpck_require__(7975) /** * Defines the scope API for an interceptor reply @@ -21595,13 +22658,9 @@ class MockInterceptor { // Values of reply aren't available right now as they // can only be available when the reply callback is invoked. if (typeof replyOptionsCallbackOrStatusCode === 'function') { - // We'll first wrap the provided callback in another function, - // this function will properly resolve the data from the callback - // when invoked. - const wrappedDefaultsCallback = (opts) => { - // Our reply options callback contains the parameter for statusCode, data and options. - const resolvedData = replyOptionsCallbackOrStatusCode(opts) - + // Resolves the data returned by a reply options callback into + // dispatch data, validating its format along the way. + const resolveReplyCallbackData = (resolvedData) => { // Check if it is in the right format if (typeof resolvedData !== 'object' || resolvedData === null) { throw new InvalidArgumentError('reply options callback must return an object') @@ -21616,6 +22675,23 @@ class MockInterceptor { } } + // We'll first wrap the provided callback in another function, + // this function will properly resolve the data from the callback + // when invoked. + const wrappedDefaultsCallback = (opts) => { + // Our reply options callback contains the parameter for statusCode, data and options. + const resolvedData = replyOptionsCallbackOrStatusCode(opts) + + // An asynchronous reply options callback resolves to the reply + // parameters, so the dispatch data can only be resolved once the + // returned promise settles. + if (isPromise(resolvedData)) { + return resolvedData.then(resolveReplyCallbackData) + } + + return resolveReplyCallbackData(resolvedData) + } + // Add usual dispatch data, but this time set the data parameter to function that will eventually provide data. const newMockDispatch = addMockDispatch(this[kDispatches], this[kDispatchKey], wrappedDefaultsCallback, { ignoreTrailingSlash: this[kIgnoreTrailingSlash] }) return new MockScope(newMockDispatch) @@ -21689,15 +22765,15 @@ module.exports.MockScope = MockScope /***/ }), -/***/ 252: +/***/ 380: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const { promisify } = __nccwpck_require__(7975) -const Pool = __nccwpck_require__(1276) -const { buildMockDispatch } = __nccwpck_require__(5277) +const Pool = __nccwpck_require__(1324) +const { buildMockDispatch } = __nccwpck_require__(5325) const { kDispatches, kMockAgent, @@ -21707,10 +22783,10 @@ const { kOriginalDispatch, kConnected, kIgnoreTrailingSlash -} = __nccwpck_require__(5301) -const { MockInterceptor } = __nccwpck_require__(4287) -const Symbols = __nccwpck_require__(451) -const { InvalidArgumentError } = __nccwpck_require__(3291) +} = __nccwpck_require__(9045) +const { MockInterceptor } = __nccwpck_require__(463) +const Symbols = __nccwpck_require__(2019) +const { InvalidArgumentError } = __nccwpck_require__(4715) /** * MockPool provides an API that extends the Pool to influence the mockDispatches. @@ -21765,7 +22841,7 @@ module.exports = MockPool /***/ }), -/***/ 5301: +/***/ 9045: /***/ ((module) => { "use strict"; @@ -21805,13 +22881,13 @@ module.exports = { /***/ }), -/***/ 5277: +/***/ 5325: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { MockNotMatchedError } = __nccwpck_require__(4805) +const { MockNotMatchedError } = __nccwpck_require__(6069) const { kDispatches, kMockAgent, @@ -21819,15 +22895,16 @@ const { kOrigin, kGetNetConnect, kTotalDispatchCount -} = __nccwpck_require__(5301) -const { serializePathWithQuery, parseHeaders } = __nccwpck_require__(648) +} = __nccwpck_require__(9045) +const { serializePathWithQuery, parseHeaders } = __nccwpck_require__(824) const { STATUS_CODES } = __nccwpck_require__(7067) const { types: { isPromise } } = __nccwpck_require__(7975) -const { InvalidArgumentError } = __nccwpck_require__(3291) +const { InvalidArgumentError } = __nccwpck_require__(4715) +const requestAborted = Symbol('request aborted') function matchValue (match, value) { if (typeof match === 'string') { @@ -21964,6 +23041,11 @@ function getResponseData (data) { return data } else if (data instanceof ArrayBuffer) { return data + } else if (ArrayBuffer.isView(data)) { + // A DataView, or any non-Uint8Array typed array, is a byte container + // rather than a plain object. Buffer.from() cannot read one directly, so + // expose the bytes it covers instead of letting it reach JSON.stringify. + return new Uint8Array(data.buffer, data.byteOffset, data.byteLength) } else if (typeof data === 'object') { return JSON.stringify(data) } else if (data) { @@ -22036,9 +23118,15 @@ function deleteMockDispatch (mockDispatches, key) { } /** - * @param {string} path Path to remove trailing slash from + * @param {string|RegExp|Function} path Path, or path matcher, to remove trailing slash from */ function removeTrailingSlash (path) { + // Registered path matchers may be a RegExp or a function, which have no + // trailing slash to strip; hand those back for matchValue to apply. + if (typeof path !== 'string') { + return path + } + while (path.endsWith('/')) { path = path.slice(0, -1) } @@ -22103,26 +23191,66 @@ function mockDispatch (opts, handler) { // Get mock dispatch from built key const key = buildKey(opts) const mockDispatch = getMockDispatch(this[kDispatches], key) + const mockDispatches = this[kDispatches] mockDispatch.timesInvoked++ - // Here's where we resolve a callback if a callback is present for the dispatch data. - if (mockDispatch.data.callback) { - mockDispatch.data = { ...mockDispatch.data, ...mockDispatch.data.callback(opts) } - } - - // Parse mockDispatch data - const { data: { statusCode, data, headers, trailers, error }, delay, persist } = mockDispatch const { timesInvoked, times } = mockDispatch // If it's used up and not persistent, mark as consumed - mockDispatch.consumed = !persist && timesInvoked >= times + mockDispatch.consumed = !mockDispatch.persist && timesInvoked >= times mockDispatch.pending = timesInvoked < times + const hasBodyHooks = typeof handler.onBodySent === 'function' || + typeof handler.onRequestSent === 'function' + + // Here's where we resolve a callback if a callback is present for the dispatch data. + if (mockDispatch.data.callback && (!hasBodyHooks || opts.body == null)) { + const { callback, ...responseDefaults } = mockDispatch.data + const callbackResult = callback(opts) + + // An asynchronous reply options callback resolves to the reply data, so + // the dispatch can only continue once the returned promise settles. + // A rejection cannot be thrown synchronously from the dispatch at that + // point, so it is surfaced as a response error instead. + if (isPromise(callbackResult)) { + callbackResult.then( + (resolvedData) => { + if (resolvedData == null || typeof resolvedData !== 'object') { + handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object')) + return + } + mockDispatch.data = { ...responseDefaults, ...resolvedData } + dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler) + }, + (error) => { + handler.onResponseError(null, error) + } + ) + return true + } + + if (callbackResult == null || typeof callbackResult !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + mockDispatch.data = { ...responseDefaults, ...callbackResult } + } + + return dispatchMockReply(mockDispatches, mockDispatch, key, opts, handler) +} + +/** + * Replies to a request once the mock dispatch data is fully resolved + */ +function dispatchMockReply (mockDispatches, mockDispatch, key, opts, handler) { + // Parse mockDispatch data + const { data: response, delay } = mockDispatch + // If specified, trigger dispatch error - if (error !== null) { - deleteMockDispatch(this[kDispatches], key) - handler.onResponseError(null, error) + if (response.error !== null) { + deleteMockDispatch(mockDispatches, key) + handler.onResponseError(null, response.error) return true } @@ -22157,32 +23285,107 @@ function mockDispatch (opts, handler) { } } + let replyOpts = opts + const dispatches = mockDispatches + // Call onRequestStart to allow the handler to receive the controller handler.onRequestStart?.(controller, null) - // Handle the request with a delay if necessary - if (typeof delay === 'number' && delay > 0) { - timer = setTimeout(() => { - timer = null - handleReply(this[kDispatches]) - }, delay) - } else { - handleReply(this[kDispatches]) + if (aborted) { + return true } - function handleReply (mockDispatches, _data = data) { + const requestBody = dispatchRequestBody(opts.body, handler, controller, () => aborted) + + if (isPromise(requestBody)) { + requestBody.then((body) => { + if (body === requestAborted) { + return + } + + if (body !== opts.body) { + replyOpts = { ...opts, body } + } + + sendReply() + }, (error) => controller.abort(error)) + return true + } + + if (requestBody === requestAborted) { + return true + } + + if (requestBody !== opts.body) { + replyOpts = { ...opts, body: requestBody } + } + + sendReply() + + function sendReply () { + if (response.callback) { + const { callback, ...responseDefaults } = response + let callbackResult + try { + callbackResult = callback(replyOpts) + } catch (err) { + deleteMockDispatch(mockDispatches, key) + handler.onResponseError(null, err) + return + } + + if (isPromise(callbackResult)) { + callbackResult.then( + (resolvedData) => { + if (resolvedData == null || typeof resolvedData !== 'object') { + handler.onResponseError(null, new InvalidArgumentError('reply options callback must return an object')) + return + } + mockDispatch.data = { ...responseDefaults, ...resolvedData } + handleReply(dispatches, mockDispatch.data) + }, + (err) => { + handler.onResponseError(null, err) + } + ) + return + } + + if (callbackResult == null || typeof callbackResult !== 'object') { + throw new InvalidArgumentError('reply options callback must return an object') + } + + mockDispatch.data = { ...responseDefaults, ...callbackResult } + handleReply(dispatches, mockDispatch.data) + return + } + + // Handle the request with a delay if necessary + if (typeof delay === 'number' && delay > 0) { + timer = setTimeout(() => { + timer = null + handleReply(dispatches) + }, delay) + } else { + handleReply(dispatches) + } + } + + function handleReply (mockDispatches, _response = response) { // Don't send response if the request was aborted if (aborted) { return } + const { statusCode, data, headers, trailers } = _response + // fetch's HeadersList is a 1D string array const optsHeaders = Array.isArray(opts.headers) ? buildHeadersFromArray(opts.headers) : opts.headers - const body = typeof _data === 'function' - ? _data({ ...opts, headers: optsHeaders }) - : _data + const body = typeof data === 'function' + ? data({ ...replyOpts, headers: optsHeaders }) + : data // util.types.isPromise is likely needed for jest. if (isPromise(body)) { @@ -22191,7 +23394,7 @@ function mockDispatch (opts, handler) { // synchronously throw the error, which breaks some tests. // Rather, we wait for the callback to resolve if it is a // promise, and then re-run handleReply with the new body. - return body.then((newData) => handleReply(mockDispatches, newData)) + return body.then((newData) => handleReply(mockDispatches, { ..._response, data: newData })) } // Check again if aborted after async body resolution @@ -22200,8 +23403,8 @@ function mockDispatch (opts, handler) { } const responseData = getResponseData(body) - const responseHeaders = generateKeyValues(headers) - const responseTrailers = generateKeyValues(trailers) + const responseHeaders = generateKeyValues(headers ?? {}) + const responseTrailers = generateKeyValues(trailers ?? {}) // Update the controller with response data controller.rawHeaders = responseHeaders @@ -22216,6 +23419,97 @@ function mockDispatch (opts, handler) { return true } +function dispatchRequestBody (body, handler, controller, isAborted) { + if (typeof handler.onBodySent !== 'function' && typeof handler.onRequestSent !== 'function') { + return body + } + + if (body == null) { + return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted + } + + if (body && typeof body[Symbol.asyncIterator] === 'function') { + return dispatchAsyncIterableBody(body, handler, controller, isAborted) + } + + if (isIterableBody(body)) { + const chunks = [] + + for (const chunk of body) { + if (isAborted()) { + return requestAborted + } + chunks.push(chunk) + if (!callOnBodySent(handler, controller, chunk) || isAborted()) { + return requestAborted + } + } + + return callOnRequestSent(handler, controller, isAborted) ? chunks : requestAborted + } + + if (isAborted()) { + return requestAborted + } + + if (!callOnBodySent(handler, controller, body)) { + return requestAborted + } + + return callOnRequestSent(handler, controller, isAborted) ? body : requestAborted +} + +async function dispatchAsyncIterableBody (body, handler, controller, isAborted) { + const chunks = [] + + for await (const chunk of body) { + if (isAborted()) { + return requestAborted + } + chunks.push(chunk) + if (!callOnBodySent(handler, controller, chunk) || isAborted()) { + return requestAborted + } + } + + if (!callOnRequestSent(handler, controller, isAborted)) { + return requestAborted + } + + return { + async * [Symbol.asyncIterator] () { + yield * chunks + } + } +} + +function callOnBodySent (handler, controller, chunk) { + try { + handler.onBodySent?.(chunk) + return true + } catch (error) { + controller.abort(error) + return false + } +} + +function callOnRequestSent (handler, controller, isAborted) { + try { + handler.onRequestSent?.() + return !isAborted() + } catch (error) { + controller.abort(error) + return false + } +} + +function isIterableBody (body) { + return typeof body !== 'string' && + !Buffer.isBuffer(body) && + !ArrayBuffer.isView(body) && + typeof body[Symbol.iterator] === 'function' +} + function buildMockDispatch () { const agent = this[kMockAgent] const origin = this[kOrigin] @@ -22314,7 +23608,7 @@ module.exports = { /***/ }), -/***/ 4486: +/***/ 4966: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -22365,18 +23659,18 @@ module.exports = class PendingInterceptorsFormatter { /***/ }), -/***/ 639: +/***/ 5903: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const Agent = __nccwpck_require__(821) -const MockAgent = __nccwpck_require__(3253) -const { SnapshotRecorder } = __nccwpck_require__(4398) -const { InvalidArgumentError, UndiciError } = __nccwpck_require__(3291) -const util = __nccwpck_require__(648) -const { validateSnapshotMode } = __nccwpck_require__(1499) +const Agent = __nccwpck_require__(5733) +const MockAgent = __nccwpck_require__(3301) +const { SnapshotRecorder } = __nccwpck_require__(8654) +const { InvalidArgumentError, UndiciError } = __nccwpck_require__(4715) +const util = __nccwpck_require__(824) +const { validateSnapshotMode } = __nccwpck_require__(411) const kSnapshotRecorder = Symbol('kSnapshotRecorder') const kSnapshotMode = Symbol('kSnapshotMode') @@ -22744,7 +24038,7 @@ module.exports = SnapshotAgent /***/ }), -/***/ 4398: +/***/ 8654: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -22753,8 +24047,8 @@ module.exports = SnapshotAgent const { writeFile, readFile, mkdir } = __nccwpck_require__(1455) const { dirname, resolve } = __nccwpck_require__(6760) const { setTimeout, clearTimeout } = __nccwpck_require__(7997) -const { InvalidArgumentError, UndiciError } = __nccwpck_require__(3291) -const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = __nccwpck_require__(1499) +const { InvalidArgumentError, UndiciError } = __nccwpck_require__(4715) +const { hashId, isUrlExcludedFactory, normalizeHeaders, createHeaderFilters } = __nccwpck_require__(411) /** * @typedef {Object} SnapshotRequestOptions @@ -23375,14 +24669,14 @@ module.exports = { SnapshotRecorder, formatRequestKey, createRequestHash, filter /***/ }), -/***/ 1499: +/***/ 411: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { InvalidArgumentError } = __nccwpck_require__(3291) -const { runtimeFeatures } = __nccwpck_require__(7217) +const { InvalidArgumentError } = __nccwpck_require__(4715) +const { runtimeFeatures } = __nccwpck_require__(3393) /** * @typedef {Object} HeaderFilters @@ -23541,7 +24835,7 @@ module.exports = { /***/ }), -/***/ 3283: +/***/ 2627: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -23550,18 +24844,152 @@ module.exports = { const { safeHTTPMethods, pathHasQueryOrFragment, - hasSafeIterator -} = __nccwpck_require__(648) + hasSafeIterator, + isValidHTTPToken +} = __nccwpck_require__(824) -const { serializePathWithQuery } = __nccwpck_require__(648) +const { serializePathWithQuery } = __nccwpck_require__(824) + +const MAX_DELTA_SECONDS = 2147483647 +const RESTRICTIVE_DIRECTIVE_NAMES = ['no-store', 'private', 'no-cache'] +const kInvalidCacheControlDirectives = Symbol('invalid cache-control directives') + +function trimOWS (value) { + return value.replace(/^[\t ]+|[\t ]+$/g, '') +} + +function arrayIncludes (array, value) { + for (let i = 0; i < array.length; i++) { + if (array[i] === value) { + return true + } + } + + return false +} + +function trimOWSStart (value) { + return value.replace(/^[\t ]+/, '') +} + +function trimOWSEnd (value) { + return value.replace(/[\t ]+$/, '') +} + +function findUnescapedQuote (value, start) { + let escaped = false + for (let i = start; i < value.length; i++) { + if (escaped) { + escaped = false + } else if (value[i] === '\\') { + escaped = true + } else if (value[i] === '"') { + return i + } + } + + return -1 +} + +function splitCacheControlHeaderValue (value) { + const directives = [] + let start = 0 + let quoteStart = -1 + let inQuote = false + let escaped = false + + for (let i = 0; i < value.length; i++) { + if (inQuote) { + if (escaped) { + escaped = false + } else if (value[i] === '\\') { + escaped = true + } else if (value[i] === '"') { + inQuote = false + quoteStart = -1 + } + } else if (value[i] === '"') { + inQuote = true + quoteStart = i + } else if (value[i] === ',') { + directives.push({ value: value.substring(start, i), fromMalformedQuote: false }) + start = i + 1 + } + } + + if (!inQuote) { + directives.push({ value: value.substring(start), fromMalformedQuote: false }) + return directives + } + + const tail = value.substring(start) + const quoteOffset = quoteStart - start + let tailStart = 0 + for (let i = 0; i < tail.length; i++) { + if (tail[i] === ',') { + directives.push({ + value: tail.substring(tailStart, i), + fromMalformedQuote: tailStart > quoteOffset + }) + tailStart = i + 1 + } + } + + directives.push({ + value: tail.substring(tailStart), + fromMalformedQuote: tailStart > quoteOffset + }) + return directives +} + +function markInvalidCacheControlDirective (directives, key) { + let invalidDirectives = directives[kInvalidCacheControlDirectives] + + if (invalidDirectives === undefined) { + invalidDirectives = new Set() + Object.defineProperty(directives, kInvalidCacheControlDirectives, { + value: invalidDirectives + }) + } + + invalidDirectives.add(key) +} + +function hasInvalidCacheControlDirective (directives, key) { + return directives[kInvalidCacheControlDirectives]?.has(key) === true +} + +function getMalformedRestrictiveDirectiveName (key) { + for (const directiveName of RESTRICTIVE_DIRECTIVE_NAMES) { + if ( + key.startsWith(directiveName) && + key.length > directiveName.length && + !isValidHTTPToken(key[directiveName.length]) + ) { + return directiveName + } + } + + let tokenOnlyKey = '' + let hasInvalidTokenChar = false + for (let i = 0; i < key.length; i++) { + if (isValidHTTPToken(key[i])) { + tokenOnlyKey += key[i] + } else { + hasInvalidTokenChar = true + } + } + + if (hasInvalidTokenChar && arrayIncludes(RESTRICTIVE_DIRECTIVE_NAMES, tokenOnlyKey)) { + return tokenOnlyKey + } +} /** * @param {import('../../types/dispatcher.d.ts').default.DispatchOptions} opts */ function makeCacheKey (opts) { - if (!opts.origin) { - throw new Error('opts.origin is undefined') - } + const origin = opts.origin ? opts.origin.toString() : '' let fullPath = opts.path || '/' @@ -23570,13 +24998,27 @@ function makeCacheKey (opts) { } return { - origin: opts.origin.toString(), + origin, method: opts.method, path: fullPath, headers: opts.headers } } +function appendHeader (headers, key, val) { + const headerName = key.toLowerCase() + const current = headers[headerName] + const values = Array.isArray(val) ? val : [val] + + if (current === undefined) { + headers[headerName] = Array.isArray(val) ? val.slice() : val + } else if (Array.isArray(current)) { + current.push(...values) + } else { + headers[headerName] = [current, ...values] + } +} + /** * @param {Record} * @returns {Record} @@ -23589,19 +25031,61 @@ function normalizeHeaders (opts) { headers = {} if (hasSafeIterator(opts.headers)) { - for (const x of opts.headers) { - if (!Array.isArray(x)) { - throw new Error('opts.headers is not a valid header map') + if (Array.isArray(opts.headers)) { + // Array format: could be flat alternating [k, v, k, v, ...] + // or array-of-pairs [[k, v], ...] + const first = opts.headers[0] + if (Array.isArray(first)) { + for (const x of opts.headers) { + if (!Array.isArray(x)) { + throw new Error('opts.headers is not a valid header map') + } + const [key, val] = x + if (typeof key !== 'string' || typeof val !== 'string') { + throw new Error('opts.headers is not a valid header map') + } + appendHeader(headers, key, val) + } + } else { + // Flat alternating array [k, v, k, v, ...] + const len = opts.headers.length + if (len % 2 !== 0) { + throw new Error('opts.headers is not a valid header map') + } + for (let i = 0; i < len; i += 2) { + const key = opts.headers[i] + const val = opts.headers[i + 1] + if (typeof key !== 'string' || (typeof val !== 'string' && !Array.isArray(val))) { + throw new Error('opts.headers is not a valid header map') + } + if (typeof val === 'string') { + appendHeader(headers, key, val) + } else { + const mapped = [] + for (let j = 0; j < val.length; j++) { + const v = val[j] + mapped.push(typeof v === 'string' ? v : v.toString('latin1')) + } + appendHeader(headers, key, mapped) + } + } } - const [key, val] = x - if (typeof key !== 'string' || typeof val !== 'string') { - throw new Error('opts.headers is not a valid header map') + } else { + // Non-array iterable (e.g. Map) — use original iteration logic + for (const x of opts.headers) { + if (!Array.isArray(x)) { + throw new Error('opts.headers is not a valid header map') + } + const [key, val] = x + if (typeof key !== 'string' || typeof val !== 'string') { + throw new Error('opts.headers is not a valid header map') + } + appendHeader(headers, key, val) } - headers[key.toLowerCase()] = val } } else { for (const key of Object.keys(opts.headers)) { - headers[key.toLowerCase()] = opts.headers[key] + appendHeader(headers, key, opts.headers[key]) } } } else { @@ -23673,29 +25157,37 @@ function parseCacheControlHeader (header) { * @type {import('../../types/cache-interceptor.d.ts').default.CacheControlDirectives} */ const output = {} + const invalidNumericDirectives = new Set() + const invalidNoArgumentDirectives = new Set() - let directives - if (Array.isArray(header)) { - directives = [] - - for (const directive of header) { - directives.push(...directive.split(',')) - } - } else { - directives = header.split(',') - } + const directives = splitCacheControlHeaderValue(Array.isArray(header) ? header.join(',') : header) for (let i = 0; i < directives.length; i++) { - const directive = directives[i].toLowerCase() + const directiveRecord = directives[i] + const directive = directiveRecord.value.toLowerCase() + const fromMalformedQuote = directiveRecord.fromMalformedQuote const keyValueDelimiter = directive.indexOf('=') let key let value + let keyHasTrailingWhitespace = false + let valueHasLeadingWhitespace = false if (keyValueDelimiter !== -1) { - key = directive.substring(0, keyValueDelimiter).trimStart() - value = directive.substring(keyValueDelimiter + 1) + const rawKey = directive.substring(0, keyValueDelimiter) + const rawValue = directive.substring(keyValueDelimiter + 1) + + keyHasTrailingWhitespace = trimOWSEnd(rawKey) !== rawKey + valueHasLeadingWhitespace = trimOWSStart(rawValue) !== rawValue + key = trimOWS(rawKey) + value = trimOWSStart(rawValue) } else { - key = directive.trim() + key = trimOWS(directive) + } + + const malformedRestrictiveDirectiveName = getMalformedRestrictiveDirectiveName(key) + if (malformedRestrictiveDirectiveName !== undefined) { + output[malformedRestrictiveDirectiveName] = true + continue } switch (key) { @@ -23705,7 +25197,14 @@ function parseCacheControlHeader (header) { case 's-maxage': case 'stale-while-revalidate': case 'stale-if-error': { - if (value === undefined || value[0] === ' ') { + if (fromMalformedQuote || invalidNumericDirectives.has(key)) { + continue + } + + if (value === undefined || keyHasTrailingWhitespace || valueHasLeadingWhitespace) { + delete output[key] + invalidNumericDirectives.add(key) + markInvalidCacheControlDirective(output, key) continue } @@ -23717,22 +25216,37 @@ function parseCacheControlHeader (header) { value = value.substring(1, value.length - 1) } - const parsedValue = parseInt(value, 10) - // eslint-disable-next-line no-self-compare - if (parsedValue !== parsedValue) { + if (!/^[0-9]+$/.test(value)) { + delete output[key] + invalidNumericDirectives.add(key) + markInvalidCacheControlDirective(output, key) continue } - if (key === 'max-age' && key in output && output[key] >= parsedValue) { - continue - } + const parsedValue = Math.min(parseInt(value, 10), MAX_DELTA_SECONDS) - output[key] = parsedValue + if (key === 'min-fresh') { + if (!(key in output) || output[key] < parsedValue) { + output[key] = parsedValue + } + } else if (!(key in output) || output[key] > parsedValue) { + output[key] = parsedValue + } break } case 'private': case 'no-cache': { + if (fromMalformedQuote) { + output[key] = true + break + } + + if (value !== undefined && value.length === 0) { + output[key] = true + break + } + if (value) { // The private and no-cache directives can be unqualified (aka just // `private` or `no-cache`) or qualified (w/ a value). When they're @@ -23740,45 +25254,64 @@ function parseCacheControlHeader (header) { // `no-cache="header1"`, or `no-cache="header1, header2"` // If we're given multiple headers, the comma messes us up since // we split the full header by commas. So, let's loop through the - // remaining parts in front of us until we find one that ends in a - // quote. We can then just splice all of the parts in between the - // starting quote and the ending quote out of the directives array - // and continue parsing like normal. + // remaining parts in front of us until we find one that contains a + // closing quote. We can then skip the consumed quoted-list fragments and + // continue parsing like normal. // https://www.rfc-editor.org/rfc/rfc9111.html#name-no-cache-2 if (value[0] === '"') { // Something like `no-cache="some-header"` OR `no-cache="some-header, another-header"`. + value = trimOWSEnd(value) - // Add the first header on and cut off the leading quote - const headers = [value.substring(1)] + let fieldList = '' + let lastQuotedPart = i + let foundEndingQuote = false + const closingQuote = findUnescapedQuote(value, 1) - let foundEndingQuote = value[value.length - 1] === '"' - if (!foundEndingQuote) { + if (closingQuote !== -1) { + fieldList = value.substring(1, closingQuote) + foundEndingQuote = true + } else { // Something like `no-cache="some-header, another-header"` // This can still be something invalid, e.g. `no-cache="some-header, ...` + const fieldListParts = [value.substring(1)] + for (let j = i + 1; j < directives.length; j++) { - const nextPart = directives[j] - const nextPartLength = nextPart.length + const nextPart = trimOWS(directives[j].value) + const closingQuote = findUnescapedQuote(nextPart, 0) - headers.push(nextPart.trim()) + lastQuotedPart = j - if (nextPartLength !== 0 && nextPart[nextPartLength - 1] === '"') { + if (closingQuote !== -1) { + fieldListParts.push(nextPart.substring(0, closingQuote)) foundEndingQuote = true break } + + fieldListParts.push(nextPart) } + + fieldList = fieldListParts.join(',') } - if (foundEndingQuote) { - let lastHeader = headers[headers.length - 1] - if (lastHeader[lastHeader.length - 1] === '"') { - lastHeader = lastHeader.substring(0, lastHeader.length - 1) - headers[headers.length - 1] = lastHeader - } + if (!foundEndingQuote) { + output[key] = true + break + } + + i = lastQuotedPart - for (let j = 0; j < headers.length; j++) { - headers[j] = headers[j].trim() + const headers = fieldList.split(',') + let validFieldNames = true + for (let j = 0; j < headers.length; j++) { + headers[j] = trimOWS(headers[j]) + if (!isValidHTTPToken(headers[j])) { + validFieldNames = false } + } + if (!validFieldNames) { + output[key] = true + } else if (output[key] !== true) { if (key in output) { output[key] = output[key].concat(headers) } else { @@ -23786,13 +25319,17 @@ function parseCacheControlHeader (header) { } } } else { - // Something like `no-cache="some-header"` - const fieldName = value.trim() + // Something like `no-cache=some-header` + const fieldName = trimOWS(value) - if (key in output) { - output[key] = output[key].concat(fieldName) - } else { - output[key] = [fieldName] + if (!isValidHTTPToken(fieldName)) { + output[key] = true + } else if (output[key] !== true) { + if (key in output) { + output[key] = output[key].concat(fieldName) + } else { + output[key] = [fieldName] + } } } @@ -23801,19 +25338,27 @@ function parseCacheControlHeader (header) { } // eslint-disable-next-line no-fallthrough case 'public': - case 'no-store': case 'must-revalidate': case 'proxy-revalidate': case 'immutable': case 'no-transform': case 'must-understand': case 'only-if-cached': - if (value) { + if (fromMalformedQuote || invalidNoArgumentDirectives.has(key)) { + continue + } + + if (value !== undefined) { // These are qualified (something like `public=...`) when they aren't - // allowed to be, skip + // allowed to be, skip all instances of the malformed directive. + delete output[key] + invalidNoArgumentDirectives.add(key) continue } + output[key] = true + break + case 'no-store': output[key] = true break default: @@ -23825,31 +25370,79 @@ function parseCacheControlHeader (header) { return output } +/** + * @param {string | string[]} varyHeader Vary header from the server + * @returns {string[]} + */ +function splitVaryHeader (varyHeader) { + const values = Array.isArray(varyHeader) ? varyHeader : [varyHeader] + const output = [] + + for (let i = 0; i < values.length; i++) { + const parts = values[i].split(',') + for (let j = 0; j < parts.length; j++) { + output.push(parts[j]) + } + } + + return output +} + +/** + * @param {string | string[]} varyHeader Vary header from the server + * @returns {boolean} + */ +function hasVaryStar (varyHeader) { + const values = splitVaryHeader(varyHeader) + for (let i = 0; i < values.length; i++) { + if (trimOWS(values[i]).indexOf('*') !== -1) { + return true + } + } + + return false +} + /** * @param {string | string[]} varyHeader Vary header from the server * @param {Record} headers Request headers - * @returns {Record} + * @returns {Record | undefined} */ function parseVaryHeader (varyHeader, headers) { - if (typeof varyHeader === 'string' && varyHeader.includes('*')) { + if (hasVaryStar(varyHeader)) { return headers } const output = /** @type {Record} */ ({}) - const varyingHeaders = typeof varyHeader === 'string' - ? varyHeader.split(',') - : varyHeader + const varyingHeaders = splitVaryHeader(varyHeader) for (const header of varyingHeaders) { - const trimmedHeader = header.trim().toLowerCase() + const trimmedHeader = trimOWS(header).toLowerCase() - output[trimmedHeader] = headers[trimmedHeader] ?? null + if (trimmedHeader.length === 0) { + continue + } + + if (!isValidHTTPToken(trimmedHeader)) { + return undefined + } + + const headerValue = headers[trimmedHeader] + output[trimmedHeader] = Array.isArray(headerValue) ? headerValue.slice() : headerValue ?? null } return output } +/** + * @param {string | string[]} varyHeader Vary header from the server + * @returns {boolean} + */ +function isInvalidOrWildcardVaryHeader (varyHeader) { + return hasVaryStar(varyHeader) || parseVaryHeader(varyHeader, {}) === undefined +} + /** * Note: this deviates from the spec a little. Empty etags ("", W/"") are valid, * however, including them in cached resposnes serves little to no purpose. @@ -23913,7 +25506,7 @@ function assertCacheMethods (methods, name = 'CacheMethods') { } for (const method of methods) { - if (!safeHTTPMethods.includes(method)) { + if (!arrayIncludes(safeHTTPMethods, method)) { throw new TypeError(`element of ${name}-array needs to be one of following values: ${safeHTTPMethods.join(', ')}, got ${method}`) } } @@ -23953,7 +25546,10 @@ module.exports = { assertCacheKey, assertCacheValue, parseCacheControlHeader, + hasInvalidCacheControlDirective, parseVaryHeader, + hasVaryStar, + isInvalidOrWildcardVaryHeader, isEtagUsable, assertCacheMethods, assertCacheStore, @@ -23963,7 +25559,7 @@ module.exports = { /***/ }), -/***/ 1109: +/***/ 1285: /***/ ((module) => { "use strict"; @@ -23987,6 +25583,26 @@ function parseHttpDate (date) { } } +function makeDate (year, monthIdx, day, hour, minute, second, weekday) { + const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) + + // Date.UTC treats years 0-99 as 1900-1999. Reset the full year so component + // checks below validate the HTTP date as written. + if (year >= 0 && year <= 99) { + result.setUTCFullYear(year) + } + + return result.getUTCFullYear() === year && + result.getUTCMonth() === monthIdx && + result.getUTCDate() === day && + result.getUTCHours() === hour && + result.getUTCMinutes() === minute && + result.getUTCSeconds() === second && + result.getUTCDay() === weekday + ? result + : undefined +} + /** * @see https://httpwg.org/specs/rfc9110.html#preferred.date.format * @@ -24193,8 +25809,7 @@ function parseImfDate (date) { second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) - return result.getUTCDay() === weekday ? result : undefined + return makeDate(year, monthIdx, day, hour, minute, second, weekday) } /** @@ -24398,8 +26013,7 @@ function parseAscTimeDate (date) { } const year = (yearDigit1 - 48) * 1000 + (yearDigit2 - 48) * 100 + (yearDigit3 - 48) * 10 + (yearDigit4 - 48) - const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) - return result.getUTCDay() === weekday ? result : undefined + return makeDate(year, monthIdx, day, hour, minute, second, weekday) } /** @@ -24613,8 +26227,7 @@ function parseRfc850Date (date) { second = (code1 - 48) * 10 + (code2 - 48) // Convert ASCII codes to number } - const result = new Date(Date.UTC(year, monthIdx, day, hour, minute, second)) - return result.getUTCDay() === weekday ? result : undefined + return makeDate(year, monthIdx, day, hour, minute, second, weekday) } module.exports = { @@ -24624,7 +26237,7 @@ module.exports = { /***/ }), -/***/ 7217: +/***/ 3393: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -24725,7 +26338,7 @@ module.exports["default"] = instance /***/ }), -/***/ 6686: +/***/ 6030: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -24738,7 +26351,7 @@ const { kSize, kFree, kQueued -} = __nccwpck_require__(451) +} = __nccwpck_require__(2019) class ClientStats { constructor (client) { @@ -24765,7 +26378,7 @@ module.exports = { ClientStats, PoolStats } /***/ }), -/***/ 8275: +/***/ 6051: /***/ ((module) => { "use strict"; @@ -25198,7 +26811,7 @@ module.exports = { /***/ }), -/***/ 3706: +/***/ 7402: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; @@ -25206,14 +26819,14 @@ module.exports = { const assert = __nccwpck_require__(4589) -const { kConstruct } = __nccwpck_require__(451) -const { urlEquals, getFieldValues } = __nccwpck_require__(726) -const { kEnumerableProperty, isDisturbed } = __nccwpck_require__(648) -const { webidl } = __nccwpck_require__(4271) -const { cloneResponse, fromInnerResponse, getResponseState } = __nccwpck_require__(5459) -const { Request, fromInnerRequest, getRequestState } = __nccwpck_require__(6423) -const { fetching } = __nccwpck_require__(2726) -const { urlIsHttpHttpsScheme, readAllBytes } = __nccwpck_require__(4216) +const { kConstruct } = __nccwpck_require__(2019) +const { urlEquals, getFieldValues } = __nccwpck_require__(854) +const { kEnumerableProperty, isDisturbed } = __nccwpck_require__(824) +const { webidl } = __nccwpck_require__(9183) +const { cloneResponse, fromInnerResponse, getResponseState } = __nccwpck_require__(3171) +const { Request, fromInnerRequest, getRequestState } = __nccwpck_require__(167) +const { fetching } = __nccwpck_require__(6422) +const { urlIsHttpHttpsScheme, readAllBytes } = __nccwpck_require__(696) /** * @see https://w3c.github.io/ServiceWorker/#dfn-cache-batch-operation * @typedef {Object} CacheBatchOperation @@ -26068,16 +27681,16 @@ module.exports = { /***/ }), -/***/ 3221: +/***/ 7477: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { Cache } = __nccwpck_require__(3706) -const { webidl } = __nccwpck_require__(4271) -const { kEnumerableProperty } = __nccwpck_require__(648) -const { kConstruct } = __nccwpck_require__(451) +const { Cache } = __nccwpck_require__(7402) +const { webidl } = __nccwpck_require__(9183) +const { kEnumerableProperty } = __nccwpck_require__(824) +const { kConstruct } = __nccwpck_require__(2019) class CacheStorage { /** @@ -26228,15 +27841,15 @@ module.exports = { /***/ }), -/***/ 726: +/***/ 854: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; const assert = __nccwpck_require__(4589) -const { URLSerializer } = __nccwpck_require__(132) -const { isValidHeaderName } = __nccwpck_require__(4216) +const { URLSerializer } = __nccwpck_require__(7844) +const { isValidHeaderName } = __nccwpck_require__(696) /** * @see https://url.spec.whatwg.org/#concept-url-equals @@ -26281,7 +27894,7 @@ module.exports = { /***/ }), -/***/ 9572: +/***/ 5748: /***/ ((module) => { "use strict"; @@ -26301,16 +27914,16 @@ module.exports = { /***/ }), -/***/ 9853: +/***/ 3597: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { parseSetCookie } = __nccwpck_require__(6050) -const { stringify } = __nccwpck_require__(7917) -const { webidl } = __nccwpck_require__(4271) -const { Headers } = __nccwpck_require__(8732) +const { parseSetCookie } = __nccwpck_require__(3442) +const { stringify } = __nccwpck_require__(2829) +const { webidl } = __nccwpck_require__(9183) +const { Headers } = __nccwpck_require__(2476) const brandChecks = webidl.brandCheckMultiple([Headers, globalThis.Headers].filter(Boolean)) @@ -26508,15 +28121,15 @@ module.exports = { /***/ }), -/***/ 6050: +/***/ 3442: /***/ ((module, __unused_webpack_exports, __nccwpck_require__) => { "use strict"; -const { collectASequenceOfCodePointsFast } = __nccwpck_require__(5420) -const { maxNameValuePairSize, maxAttributeValueSize } = __nccwpck_require__(9572) -const { isCTLExcludingHtab } = __nccwpck_require__(7917) +const { collectASequenceOfCodePointsFast } = __nccwpck_require__(9116) +const { maxNameValuePairSize, maxAttributeValueSize } = __nccwpck_require__(5748) +const { isCTLExcludingHtab } = __nccwpck_require__(2829) const assert = __nccwpck_require__(4589) /** @@ -26692,8 +28305,9 @@ function parseUnparsedAttributes (unparsedAttributes, cookieAttributeList = {}) // 2. If the attribute-value failed to parse as a cookie date, ignore // the cookie-av. - - cookieAttributeList.expires = expiryTime + if (!Number.isNaN(expiryTime.getTime())) { + cookieAttributeList.expires = expiryTime + } } else if (attributeNameLowercase === 'max-age') { // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-rfc6265bis#section-5.4.2 // If the attribute-name case-insensitively matches the string "Max- @@ -26830,7 +28444,7 @@ module.exports = { /***/ }), -/***/ 7917: +/***/ 2829: /***/ ((module) => { "use strict"; @@ -26941,7 +28555,7 @@ function validateCookiePath (path) { if ( code < 0x20 || // exclude CTLs (0-31) - code === 0x7F || // DEL + code > 0x7E || // exclude non-ascii and DEL code === 0x3B // ; ) { throw new Error('Invalid cookie path') @@ -26950,16 +28564,80 @@ function validateCookiePath (path) { } /** - * I have no idea why these values aren't allowed to be honest, - * but Deno tests these. - Khafra + * ::= | + * + * ::= any one of the 52 alphabetic characters A through Z in + * upper case and a through z in lower case + * + * ::= any one of the ten digits 0 through 9r + * + * @see https://www.rfc-editor.org/rfc/rfc1034#section-3.5 + * @param {number} code + */ +function isLetterOrDigit (code) { + return ( + (code >= 0x30 && code <= 0x39) || // 0-9 + (code >= 0x41 && code <= 0x5A) || // A-Z + (code >= 0x61 && code <= 0x7A) // a-z + ) +} + +/** + * Validates a cookie domain against the "preferred name syntax". + * + * ::= | " " + * ::=