From dcbbee9996c12c5cc18fb50a25ec2d6a5ad2f112 Mon Sep 17 00:00:00 2001 From: actions-bot <58130806+actions-bot@users.noreply.github.com> Date: Tue, 15 Oct 2024 13:46:26 +0100 Subject: [PATCH] build: update distribution (#3451) --- dist/index.js | 920 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 724 insertions(+), 196 deletions(-) diff --git a/dist/index.js b/dist/index.js index 6320cd4..750bf24 100644 --- a/dist/index.js +++ b/dist/index.js @@ -30190,9 +30190,9 @@ class UpgradeHandler extends AsyncResource { } onUpgrade (statusCode, rawHeaders, socket) { - const { callback, opaque, context } = this + assert(statusCode === 101) - assert.strictEqual(statusCode, 101) + const { callback, opaque, context } = this removeSignal(this) @@ -30392,6 +30392,11 @@ class BodyReadable extends Readable { return consume(this, 'blob') } + // https://fetch.spec.whatwg.org/#dom-body-bytes + async bytes () { + return consume(this, 'bytes') + } + // https://fetch.spec.whatwg.org/#dom-body-arraybuffer async arrayBuffer () { return consume(this, 'arrayBuffer') @@ -30577,6 +30582,31 @@ function chunksDecode (chunks, length) { return buffer.utf8Slice(start, bufferLength) } +/** + * @param {Buffer[]} chunks + * @param {number} length + * @returns {Uint8Array} + */ +function chunksConcat (chunks, length) { + if (chunks.length === 0 || length === 0) { + return new Uint8Array(0) + } + if (chunks.length === 1) { + // fast-path + return new Uint8Array(chunks[0]) + } + const buffer = new Uint8Array(Buffer.allocUnsafeSlow(length).buffer) + + let offset = 0 + for (let i = 0; i < chunks.length; ++i) { + const chunk = chunks[i] + buffer.set(chunk, offset) + offset += chunk.length + } + + return buffer +} + function consumeEnd (consume) { const { type, body, resolve, stream, length } = consume @@ -30586,17 +30616,11 @@ function consumeEnd (consume) { } else if (type === 'json') { resolve(JSON.parse(chunksDecode(body, length))) } else if (type === 'arrayBuffer') { - const dst = new Uint8Array(length) - - let pos = 0 - for (const buf of body) { - dst.set(buf, pos) - pos += buf.byteLength - } - - resolve(dst.buffer) + resolve(chunksConcat(body, length).buffer) } else if (type === 'blob') { resolve(new Blob(body, { type: stream[kContentType] })) + } else if (type === 'bytes') { + resolve(chunksConcat(body, length)) } consumeFinish(consume) @@ -30744,6 +30768,9 @@ const net = __nccwpck_require__(7030) const assert = __nccwpck_require__(4589) const util = __nccwpck_require__(3440) const { InvalidArgumentError, ConnectTimeoutError } = __nccwpck_require__(8707) +const timers = __nccwpck_require__(6603) + +function noop () {} let tls // include tls conditionally since it is not always available @@ -30831,9 +30858,11 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess servername = servername || options.servername || util.getServerName(host) || null const sessionKey = servername || hostname + assert(sessionKey) + const session = customSession || sessionCache.get(sessionKey) || null - assert(sessionKey) + port = port || 443 socket = tls.connect({ highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... @@ -30844,7 +30873,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess // TODO(HTTP/2): Add support for h2c ALPNProtocols: allowH2 ? ['http/1.1', 'h2'] : ['http/1.1'], socket: httpSocket, // upgrade socket connection - port: port || 443, + port, host: hostname }) @@ -30855,11 +30884,14 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess }) } else { assert(!httpSocket, 'httpSocket can only be sent on TLS update') + + port = port || 80 + socket = net.connect({ highWaterMark: 64 * 1024, // Same as nodejs fs streams. ...options, localAddress, - port: port || 80, + port, host: hostname }) } @@ -30870,12 +30902,12 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess socket.setKeepAlive(true, keepAliveInitialDelay) } - const cancelTimeout = setupTimeout(() => onConnectTimeout(socket), timeout) + const clearConnectTimeout = setupConnectTimeout(new WeakRef(socket), { timeout, hostname, port }) socket .setNoDelay(true) .once(protocol === 'https:' ? 'secureConnect' : 'connect', function () { - cancelTimeout() + queueMicrotask(clearConnectTimeout) if (callback) { const cb = callback @@ -30884,7 +30916,7 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess } }) .on('error', function (err) { - cancelTimeout() + queueMicrotask(clearConnectTimeout) if (callback) { const cb = callback @@ -30897,36 +30929,70 @@ function buildConnector ({ allowH2, maxCachedSessions, socketPath, timeout, sess } } -function setupTimeout (onConnectTimeout, timeout) { - if (!timeout) { - return () => {} - } - - let s1 = null - let s2 = null - const timeoutId = setTimeout(() => { - // setImmediate is added to make sure that we prioritize socket error events over timeouts - s1 = setImmediate(() => { - if (process.platform === 'win32') { - // Windows needs an extra setImmediate probably due to implementation differences in the socket logic - s2 = setImmediate(() => onConnectTimeout()) - } else { - onConnectTimeout() +/** + * @param {WeakRef} socketWeakRef + * @param {object} opts + * @param {number} opts.timeout + * @param {string} opts.hostname + * @param {number} opts.port + * @returns {() => void} + */ +const setupConnectTimeout = process.platform === 'win32' + ? (socketWeakRef, opts) => { + if (!opts.timeout) { + return noop } - }) - }, timeout) - return () => { - clearTimeout(timeoutId) - clearImmediate(s1) - clearImmediate(s2) - } -} -function onConnectTimeout (socket) { + let s1 = null + let s2 = null + const fastTimer = timers.setFastTimeout(() => { + // setImmediate is added to make sure that we prioritize socket error events over timeouts + s1 = setImmediate(() => { + // Windows needs an extra setImmediate probably due to implementation differences in the socket logic + s2 = setImmediate(() => onConnectTimeout(socketWeakRef.deref(), opts)) + }) + }, opts.timeout) + return () => { + timers.clearFastTimeout(fastTimer) + clearImmediate(s1) + clearImmediate(s2) + } + } + : (socketWeakRef, opts) => { + if (!opts.timeout) { + return noop + } + + let s1 = null + const fastTimer = timers.setFastTimeout(() => { + // setImmediate is added to make sure that we prioritize socket error events over timeouts + s1 = setImmediate(() => { + onConnectTimeout(socketWeakRef.deref(), opts) + }) + }, opts.timeout) + return () => { + timers.clearFastTimeout(fastTimer) + clearImmediate(s1) + } + } + +/** + * @param {net.Socket} socket + * @param {object} opts + * @param {number} opts.timeout + * @param {string} opts.hostname + * @param {number} opts.port + */ +function onConnectTimeout (socket, opts) { let message = 'Connect Timeout Error' if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) { - message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')})` + message += ` (attempted addresses: ${socket.autoSelectFamilyAttemptedAddresses.join(', ')},` + } else { + message += ` (attempted address: ${opts.hostname}:${opts.port},` } + + message += ` timeout: ${opts.timeout}ms)` + util.destroy(socket, new ConnectTimeoutError(message)) } @@ -31472,6 +31538,18 @@ class RequestRetryError extends UndiciError { } } +class ResponseError extends UndiciError { + constructor (message, code, { headers, data }) { + super(message) + this.name = 'ResponseError' + this.message = message || 'Response error' + this.code = 'UND_ERR_RESPONSE' + this.statusCode = code + this.data = data + this.headers = headers + } +} + class SecureProxyConnectionError extends UndiciError { constructor (cause, message, options) { super(message, { cause, ...(options ?? {}) }) @@ -31504,6 +31582,7 @@ module.exports = { BalancedPoolMissingUpstreamError, ResponseExceededMaxSizeError, RequestRetryError, + ResponseError, SecureProxyConnectionError } @@ -32386,7 +32465,7 @@ function getServerName (host) { return null } - assert.strictEqual(typeof host, 'string') + assert(typeof host === 'string') const servername = getHostname(host) if (net.isIP(servername)) { @@ -33319,35 +33398,35 @@ async function lazyllhttp () { return 0 }, wasm_on_status: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) + assert(currentParser.ptr === p) const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onStatus(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_message_begin: (p) => { - assert.strictEqual(currentParser.ptr, p) + assert(currentParser.ptr === p) return currentParser.onMessageBegin() || 0 }, wasm_on_header_field: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) + assert(currentParser.ptr === p) const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onHeaderField(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_header_value: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) + assert(currentParser.ptr === p) const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onHeaderValue(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_headers_complete: (p, statusCode, upgrade, shouldKeepAlive) => { - assert.strictEqual(currentParser.ptr, p) + assert(currentParser.ptr === p) return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 }, wasm_on_body: (p, at, len) => { - assert.strictEqual(currentParser.ptr, p) + assert(currentParser.ptr === p) const start = at - currentBufferPtr + currentBufferRef.byteOffset return currentParser.onBody(new FastBuffer(currentBufferRef.buffer, start, len)) || 0 }, wasm_on_message_complete: (p) => { - assert.strictEqual(currentParser.ptr, p) + assert(currentParser.ptr === p) return currentParser.onMessageComplete() || 0 } @@ -33365,9 +33444,17 @@ let currentBufferRef = null let currentBufferSize = 0 let currentBufferPtr = null -const TIMEOUT_HEADERS = 1 -const TIMEOUT_BODY = 2 -const TIMEOUT_IDLE = 3 +const USE_NATIVE_TIMER = 0 +const USE_FAST_TIMER = 1 + +// Use fast timers for headers and body to take eventual event loop +// latency into account. +const TIMEOUT_HEADERS = 2 | USE_FAST_TIMER +const TIMEOUT_BODY = 4 | USE_FAST_TIMER + +// Use native timers to ignore event loop latency for keep-alive +// handling. +const TIMEOUT_KEEP_ALIVE = 8 | USE_NATIVE_TIMER class Parser { constructor (client, socket, { exports }) { @@ -33398,26 +33485,39 @@ class Parser { this.maxResponseSize = client[kMaxResponseSize] } - setTimeout (value, type) { - this.timeoutType = type - if (value !== this.timeoutValue) { - timers.clearTimeout(this.timeout) - if (value) { - this.timeout = timers.setTimeout(onParserTimeout, value, this) - // istanbul ignore else: only for jest - if (this.timeout.unref) { - this.timeout.unref() - } - } else { + setTimeout (delay, type) { + // If the existing timer and the new timer are of different timer type + // (fast or native) or have different delay, we need to clear the existing + // timer and set a new one. + if ( + delay !== this.timeoutValue || + (type & USE_FAST_TIMER) ^ (this.timeoutType & USE_FAST_TIMER) + ) { + // If a timeout is already set, clear it with clearTimeout of the fast + // timer implementation, as it can clear fast and native timers. + if (this.timeout) { + timers.clearTimeout(this.timeout) this.timeout = null } - this.timeoutValue = value + + if (delay) { + if (type & USE_FAST_TIMER) { + this.timeout = timers.setFastTimeout(onParserTimeout, delay, new WeakRef(this)) + } else { + this.timeout = setTimeout(onParserTimeout, delay, new WeakRef(this)) + this.timeout.unref() + } + } + + this.timeoutValue = delay } else if (this.timeout) { // istanbul ignore else: only for jest if (this.timeout.refresh) { this.timeout.refresh() } } + + this.timeoutType = type } resume () { @@ -33522,7 +33622,7 @@ class Parser { this.llhttp.llhttp_free(this.ptr) this.ptr = null - timers.clearTimeout(this.timeout) + this.timeout && timers.clearTimeout(this.timeout) this.timeout = null this.timeoutValue = null this.timeoutType = null @@ -33597,20 +33697,19 @@ class Parser { const { upgrade, client, socket, headers, statusCode } = this assert(upgrade) + assert(client[kSocket] === socket) + assert(!socket.destroyed) + assert(!this.paused) + assert((headers.length & 1) === 0) const request = client[kQueue][client[kRunningIdx]] assert(request) - - assert(!socket.destroyed) - assert(socket === client[kSocket]) - assert(!this.paused) assert(request.upgrade || request.method === 'CONNECT') this.statusCode = null this.statusText = '' this.shouldKeepAlive = null - assert(this.headers.length % 2 === 0) this.headers = [] this.headersSize = 0 @@ -33667,7 +33766,7 @@ class Parser { return -1 } - assert.strictEqual(this.timeoutType, TIMEOUT_HEADERS) + assert(this.timeoutType === TIMEOUT_HEADERS) this.statusCode = statusCode this.shouldKeepAlive = ( @@ -33700,7 +33799,7 @@ class Parser { return 2 } - assert(this.headers.length % 2 === 0) + assert((this.headers.length & 1) === 0) this.headers = [] this.headersSize = 0 @@ -33757,7 +33856,7 @@ class Parser { const request = client[kQueue][client[kRunningIdx]] assert(request) - assert.strictEqual(this.timeoutType, TIMEOUT_BODY) + assert(this.timeoutType === TIMEOUT_BODY) if (this.timeout) { // istanbul ignore else: only for jest if (this.timeout.refresh) { @@ -33790,11 +33889,12 @@ class Parser { return } + assert(statusCode >= 100) + assert((this.headers.length & 1) === 0) + const request = client[kQueue][client[kRunningIdx]] assert(request) - assert(statusCode >= 100) - this.statusCode = null this.statusText = '' this.bytesRead = 0 @@ -33802,7 +33902,6 @@ class Parser { this.keepAlive = '' this.connection = '' - assert(this.headers.length % 2 === 0) this.headers = [] this.headersSize = 0 @@ -33821,7 +33920,7 @@ class Parser { client[kQueue][client[kRunningIdx]++] = null if (socket[kWriting]) { - assert.strictEqual(client[kRunning], 0) + assert(client[kRunning] === 0) // Response completed before request. util.destroy(socket, new InformationalError('reset')) return constants.ERROR.PAUSED @@ -33847,19 +33946,19 @@ class Parser { } function onParserTimeout (parser) { - const { socket, timeoutType, client } = parser + const { socket, timeoutType, client, paused } = parser.deref() /* istanbul ignore else */ if (timeoutType === TIMEOUT_HEADERS) { if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning] > 1) { - assert(!parser.paused, 'cannot be paused while waiting for headers') + assert(!paused, 'cannot be paused while waiting for headers') util.destroy(socket, new HeadersTimeoutError()) } } else if (timeoutType === TIMEOUT_BODY) { - if (!parser.paused) { + if (!paused) { util.destroy(socket, new BodyTimeoutError()) } - } else if (timeoutType === TIMEOUT_IDLE) { + } else if (timeoutType === TIMEOUT_KEEP_ALIVE) { assert(client[kRunning] === 0 && client[kKeepAliveTimeoutValue]) util.destroy(socket, new InformationalError('socket idle timeout')) } @@ -33880,10 +33979,10 @@ async function connectH1 (client, socket) { socket[kParser] = new Parser(client, socket, llhttpInstance) addListener(socket, 'error', function (err) { - const parser = this[kParser] - assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') + const parser = this[kParser] + // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded // to the user. if (err.code === 'ECONNRESET' && parser.statusCode && !parser.shouldKeepAlive) { @@ -34037,8 +34136,8 @@ function resumeH1 (client) { } if (client[kSize] === 0) { - if (socket[kParser].timeoutType !== TIMEOUT_IDLE) { - socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_IDLE) + if (socket[kParser].timeoutType !== TIMEOUT_KEEP_ALIVE) { + socket[kParser].setTimeout(client[kKeepAliveTimeoutValue], TIMEOUT_KEEP_ALIVE) } } else if (client[kRunning] > 0 && socket[kParser].statusCode < 200) { if (socket[kParser].timeoutType !== TIMEOUT_HEADERS) { @@ -34074,7 +34173,10 @@ function writeH1 (client, request) { const expectsPayload = ( method === 'PUT' || method === 'POST' || - method === 'PATCH' + method === 'PATCH' || + method === 'QUERY' || + method === 'PROPFIND' || + method === 'PROPPATCH' ) if (util.isFormDataLike(body)) { @@ -34353,7 +34455,7 @@ function writeBuffer (abort, body, client, request, socket, contentLength, heade socket.uncork() request.onBodySent(body) - if (!expectsPayload) { + if (!expectsPayload && request.reset !== false) { socket[kReset] = true } } @@ -34383,7 +34485,7 @@ async function writeBlob (abort, body, client, request, socket, contentLength, h request.onBodySent(buffer) request.onRequestSent() - if (!expectsPayload) { + if (!expectsPayload && request.reset !== false) { socket[kReset] = true } @@ -34484,7 +34586,7 @@ class AsyncWriter { socket.cork() if (bytesWritten === 0) { - if (!expectsPayload) { + if (!expectsPayload && request.reset !== false) { socket[kReset] = true } @@ -34613,7 +34715,9 @@ const { kOnError, kMaxConcurrentStreams, kHTTP2Session, - kResume + kResume, + kSize, + kHTTPContext } = __nccwpck_require__(6443) const kOpenStreams = Symbol('open streams') @@ -34749,11 +34853,10 @@ async function connectH2 (client, socket) { version: 'h2', defaultPipelining: Infinity, write (...args) { - // TODO (fix): return - writeH2(client, ...args) + return writeH2(client, ...args) }, resume () { - + resumeH2(client) }, destroy (err, callback) { if (closed) { @@ -34772,6 +34875,20 @@ async function connectH2 (client, socket) { } } +function resumeH2 (client) { + const socket = client[kSocket] + + if (socket?.destroyed === false) { + if (client[kSize] === 0 && client[kMaxConcurrentStreams] === 0) { + socket.unref() + client[kHTTP2Session].unref() + } else { + socket.ref() + client[kHTTP2Session].ref() + } + } +} + function onHttp2SessionError (err) { assert(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID') @@ -34799,17 +34916,32 @@ function onHttp2SessionEnd () { * along with the socket right away */ function onHTTP2GoAway (code) { - const err = new RequestAbortedError(`HTTP/2: "GOAWAY" frame received with code ${code}`) + // We cannot recover, so best to close the session and the socket + const err = this[kError] || new SocketError(`HTTP/2: "GOAWAY" frame received with code ${code}`, util.getSocketInfo(this)) + const client = this[kClient] - // We need to trigger the close cycle right away - // We need to destroy the session and the socket - // Requests should be failed with the error after the current one is handled - this[kSocket][kError] = err - this[kClient][kOnError](err) + client[kSocket] = null + client[kHTTPContext] = null - this.unref() + if (this[kHTTP2Session] != null) { + this[kHTTP2Session].destroy(err) + this[kHTTP2Session] = null + } util.destroy(this[kSocket], err) + + // Fail head of pipeline. + const request = client[kQueue][client[kRunningIdx]] + client[kQueue][client[kRunningIdx]++] = null + util.errorRequest(client, request, err) + + client[kPendingIdx] = client[kRunningIdx] + + assert(client[kRunning] === 0) + + client.emit('disconnect', client[kUrl], [client], err) + + client[kResume]() } // https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 @@ -34826,10 +34958,6 @@ function writeH2 (client, request) { return false } - if (request.aborted) { - return false - } - const headers = {} for (let n = 0; n < reqHeaders.length; n += 2) { const key = reqHeaders[n + 0] @@ -34872,6 +35000,8 @@ function writeH2 (client, request) { // We do not destroy the socket as we can continue using the session // the stream get's destroyed and the session remains to create new streams util.destroy(body, err) + client[kQueue][client[kRunningIdx]++] = null + client[kResume]() } try { @@ -34882,6 +35012,10 @@ function writeH2 (client, request) { util.errorRequest(client, request, err) } + if (request.aborted) { + return false + } + if (method === 'CONNECT') { session.ref() // We are already connected, streams are pending, first request @@ -34893,10 +35027,12 @@ function writeH2 (client, request) { if (stream.id && !stream.pending) { request.onUpgrade(null, null, stream) ++session[kOpenStreams] + client[kQueue][client[kRunningIdx]++] = null } else { stream.once('ready', () => { request.onUpgrade(null, null, stream) ++session[kOpenStreams] + client[kQueue][client[kRunningIdx]++] = null }) } @@ -35017,17 +35153,20 @@ function writeH2 (client, request) { // Present specially when using pipeline or stream if (stream.state?.state == null || stream.state.state < 6) { request.onComplete([]) - return } - // Stream is closed or half-closed-remote (6), decrement counter and cleanup - // It does not have sense to continue working with the stream as we do not - // have yet RST_STREAM support on client-side if (session[kOpenStreams] === 0) { + // Stream is closed or half-closed-remote (6), decrement counter and cleanup + // It does not have sense to continue working with the stream as we do not + // have yet RST_STREAM support on client-side + session.unref() } abort(new InformationalError('HTTP/2: stream half-closed (remote)')) + client[kQueue][client[kRunningIdx]++] = null + client[kPendingIdx] = client[kRunningIdx] + client[kResume]() }) stream.once('close', () => { @@ -35353,6 +35492,8 @@ let deprecatedInterceptorWarned = false const kClosedResolve = Symbol('kClosedResolve') +const noop = () => {} + function getPipelining (client) { return client[kPipelining] ?? client[kHTTPContext]?.defaultPipelining ?? 1 } @@ -35675,6 +35816,10 @@ function onError (client, err) { } } +/** + * @param {Client} client + * @returns + */ async function connect (client) { assert(!client[kConnecting]) assert(!client[kHTTPContext]) @@ -35728,7 +35873,7 @@ async function connect (client) { }) if (client.destroyed) { - util.destroy(socket.on('error', () => {}), new ClientDestroyedError()) + util.destroy(socket.on('error', noop), new ClientDestroyedError()) return } @@ -35739,7 +35884,7 @@ async function connect (client) { ? await connectH2(client, socket) : await connectH1(client, socket) } catch (err) { - socket.destroy().on('error', () => {}) + socket.destroy().on('error', noop) throw err } @@ -36591,9 +36736,9 @@ class PoolBase extends DispatcherBase { async [kClose] () { if (this[kQueue].isEmpty()) { - return Promise.all(this[kClients].map(c => c.close())) + await Promise.all(this[kClients].map(c => c.close())) } else { - return new Promise((resolve) => { + await new Promise((resolve) => { this[kClosedResolve] = resolve }) } @@ -36608,7 +36753,7 @@ class PoolBase extends DispatcherBase { item.handler.onError(err) } - return Promise.all(this[kClients].map(c => c.destroy(err))) + await Promise.all(this[kClients].map(c => c.destroy(err))) } [kDispatch] (opts, handler) { @@ -36845,6 +36990,8 @@ function defaultFactory (origin, opts) { return new Pool(origin, opts) } +const noop = () => {} + class ProxyAgent extends DispatcherBase { constructor (opts) { super() @@ -36903,7 +37050,7 @@ class ProxyAgent extends DispatcherBase { servername: this[kProxyTls]?.servername || proxyHostname }) if (statusCode !== 200) { - socket.on('error', () => {}).destroy() + socket.on('error', noop).destroy() callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)) } if (opts.protocol !== 'https:') { @@ -37587,8 +37734,18 @@ class RetryHandler { if (this.resume != null) { this.resume = null - if (statusCode !== 206) { - return true + // Only Partial Content 206 supposed to provide Content-Range, + // any other status code that partially consumed the payload + // should not be retry because it would result in downstream + // wrongly concatanete multiple responses. + if (statusCode !== 206 && (this.start > 0 || statusCode !== 200)) { + this.abort( + new RequestRetryError('server does not support the range header and the payload was partially consumed', statusCode, { + headers, + data: { count: this.retryCount } + }) + ) + return false } const contentRange = parseRangeHeader(headers['content-range']) @@ -39010,6 +39167,10 @@ function matchKey (mockDispatch, { path, method, body, headers }) { function getResponseData (data) { if (Buffer.isBuffer(data)) { return data + } else if (data instanceof Uint8Array) { + return data + } else if (data instanceof ArrayBuffer) { + return data } else if (typeof data === 'object') { return JSON.stringify(data) } else { @@ -39351,102 +39512,426 @@ module.exports = class Pluralizer { "use strict"; -const TICK_MS = 499 +/** + * This module offers an optimized timer implementation designed for scenarios + * where high precision is not critical. + * + * The timer achieves faster performance by using a low-resolution approach, + * with an accuracy target of within 500ms. This makes it particularly useful + * for timers with delays of 1 second or more, where exact timing is less + * crucial. + * + * It's important to note that Node.js timers are inherently imprecise, as + * delays can occur due to the event loop being blocked by other operations. + * Consequently, timers may trigger later than their scheduled time. + */ -let fastNow = Date.now() +/** + * The fastNow variable contains the internal fast timer clock value. + * + * @type {number} + */ +let fastNow = 0 + +/** + * RESOLUTION_MS represents the target resolution time in milliseconds. + * + * @type {number} + * @default 1000 + */ +const RESOLUTION_MS = 1e3 + +/** + * TICK_MS defines the desired interval in milliseconds between each tick. + * The target value is set to half the resolution time, minus 1 ms, to account + * for potential event loop overhead. + * + * @type {number} + * @default 499 + */ +const TICK_MS = (RESOLUTION_MS >> 1) - 1 + +/** + * fastNowTimeout is a Node.js timer used to manage and process + * the FastTimers stored in the `fastTimers` array. + * + * @type {NodeJS.Timeout} + */ let fastNowTimeout +/** + * The kFastTimer symbol is used to identify FastTimer instances. + * + * @type {Symbol} + */ +const kFastTimer = Symbol('kFastTimer') + +/** + * The fastTimers array contains all active FastTimers. + * + * @type {FastTimer[]} + */ const fastTimers = [] -function onTimeout () { - fastNow = Date.now() +/** + * These constants represent the various states of a FastTimer. + */ - let len = fastTimers.length +/** + * The `NOT_IN_LIST` constant indicates that the FastTimer is not included + * in the `fastTimers` array. Timers with this status will not be processed + * during the next tick by the `onTick` function. + * + * A FastTimer can be re-added to the `fastTimers` array by invoking the + * `refresh` method on the FastTimer instance. + * + * @type {-2} + */ +const NOT_IN_LIST = -2 + +/** + * The `TO_BE_CLEARED` constant indicates that the FastTimer is scheduled + * for removal from the `fastTimers` array. A FastTimer in this state will + * be removed in the next tick by the `onTick` function and will no longer + * be processed. + * + * This status is also set when the `clear` method is called on the FastTimer instance. + * + * @type {-1} + */ +const TO_BE_CLEARED = -1 + +/** + * The `PENDING` constant signifies that the FastTimer is awaiting processing + * in the next tick by the `onTick` function. Timers with this status will have + * their `_idleStart` value set and their status updated to `ACTIVE` in the next tick. + * + * @type {0} + */ +const PENDING = 0 + +/** + * The `ACTIVE` constant indicates that the FastTimer is active and waiting + * for its timer to expire. During the next tick, the `onTick` function will + * check if the timer has expired, and if so, it will execute the associated callback. + * + * @type {1} + */ +const ACTIVE = 1 + +/** + * The onTick function processes the fastTimers array. + * + * @returns {void} + */ +function onTick () { + /** + * Increment the fastNow value by the TICK_MS value, despite the actual time + * that has passed since the last tick. This approach ensures independence + * from the system clock and delays caused by a blocked event loop. + * + * @type {number} + */ + fastNow += TICK_MS + + /** + * The `idx` variable is used to iterate over the `fastTimers` array. + * Expired timers are removed by replacing them with the last element in the array. + * Consequently, `idx` is only incremented when the current element is not removed. + * + * @type {number} + */ let idx = 0 + + /** + * The len variable will contain the length of the fastTimers array + * and will be decremented when a FastTimer should be removed from the + * fastTimers array. + * + * @type {number} + */ + let len = fastTimers.length + while (idx < len) { + /** + * @type {FastTimer} + */ const timer = fastTimers[idx] - if (timer.state === 0) { - timer.state = fastNow + timer.delay - TICK_MS - } else if (timer.state > 0 && fastNow >= timer.state) { - timer.state = -1 - timer.callback(timer.opaque) + // If the timer is in the ACTIVE state and the timer has expired, it will + // be processed in the next tick. + if (timer._state === PENDING) { + // Set the _idleStart value to the fastNow value minus the TICK_MS value + // to account for the time the timer was in the PENDING state. + timer._idleStart = fastNow - TICK_MS + timer._state = ACTIVE + } else if ( + timer._state === ACTIVE && + fastNow >= timer._idleStart + timer._idleTimeout + ) { + timer._state = TO_BE_CLEARED + timer._idleStart = -1 + timer._onTimeout(timer._timerArg) } - if (timer.state === -1) { - timer.state = -2 - if (idx !== len - 1) { - fastTimers[idx] = fastTimers.pop() - } else { - fastTimers.pop() + if (timer._state === TO_BE_CLEARED) { + timer._state = NOT_IN_LIST + + // Move the last element to the current index and decrement len if it is + // not the only element in the array. + if (--len !== 0) { + fastTimers[idx] = fastTimers[len] } - len -= 1 } else { - idx += 1 + ++idx } } - if (fastTimers.length > 0) { + // Set the length of the fastTimers array to the new length and thus + // removing the excess FastTimers elements from the array. + fastTimers.length = len + + // If there are still active FastTimers in the array, refresh the Timer. + // If there are no active FastTimers, the timer will be refreshed again + // when a new FastTimer is instantiated. + if (fastTimers.length !== 0) { refreshTimeout() } } function refreshTimeout () { - if (fastNowTimeout?.refresh) { + // If the fastNowTimeout is already set, refresh it. + if (fastNowTimeout) { fastNowTimeout.refresh() + // fastNowTimeout is not instantiated yet, create a new Timer. } else { clearTimeout(fastNowTimeout) - fastNowTimeout = setTimeout(onTimeout, TICK_MS) + fastNowTimeout = setTimeout(onTick, TICK_MS) + + // If the Timer has an unref method, call it to allow the process to exit if + // there are no other active handles. if (fastNowTimeout.unref) { fastNowTimeout.unref() } } } -class Timeout { - constructor (callback, delay, opaque) { - this.callback = callback - this.delay = delay - this.opaque = opaque +/** + * The `FastTimer` class is a data structure designed to store and manage + * timer information. + */ +class FastTimer { + [kFastTimer] = true - // -2 not in timer list - // -1 in timer list but inactive - // 0 in timer list waiting for time - // > 0 in timer list waiting for time to expire - this.state = -2 + /** + * The state of the timer, which can be one of the following: + * - NOT_IN_LIST (-2) + * - TO_BE_CLEARED (-1) + * - PENDING (0) + * - ACTIVE (1) + * + * @type {-2|-1|0|1} + * @private + */ + _state = NOT_IN_LIST + + /** + * The number of milliseconds to wait before calling the callback. + * + * @type {number} + * @private + */ + _idleTimeout = -1 + + /** + * The time in milliseconds when the timer was started. This value is used to + * calculate when the timer should expire. + * + * @type {number} + * @default -1 + * @private + */ + _idleStart = -1 + + /** + * The function to be executed when the timer expires. + * @type {Function} + * @private + */ + _onTimeout + + /** + * The argument to be passed to the callback when the timer expires. + * + * @type {*} + * @private + */ + _timerArg + + /** + * @constructor + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should wait + * before the specified function or code is executed. + * @param {*} arg + */ + constructor (callback, delay, arg) { + this._onTimeout = callback + this._idleTimeout = delay + this._timerArg = arg this.refresh() } + /** + * Sets the timer's start time to the current time, and reschedules the timer + * to call its callback at the previously specified duration adjusted to the + * current time. + * Using this on a timer that has already called its callback will reactivate + * the timer. + * + * @returns {void} + */ refresh () { - if (this.state === -2) { + // In the special case that the timer is not in the list of active timers, + // add it back to the array to be processed in the next tick by the onTick + // function. + if (this._state === NOT_IN_LIST) { fastTimers.push(this) - if (!fastNowTimeout || fastTimers.length === 1) { - refreshTimeout() - } } - this.state = 0 + // If the timer is the only active timer, refresh the fastNowTimeout for + // better resolution. + if (!fastNowTimeout || fastTimers.length === 1) { + refreshTimeout() + } + + // Setting the state to PENDING will cause the timer to be reset in the + // next tick by the onTick function. + this._state = PENDING } + /** + * The `clear` method cancels the timer, preventing it from executing. + * + * @returns {void} + * @private + */ clear () { - this.state = -1 + // Set the state to TO_BE_CLEARED to mark the timer for removal in the next + // tick by the onTick function. + this._state = TO_BE_CLEARED + + // Reset the _idleStart value to -1 to indicate that the timer is no longer + // active. + this._idleStart = -1 } } +/** + * This module exports a setTimeout and clearTimeout function that can be + * used as a drop-in replacement for the native functions. + */ module.exports = { - setTimeout (callback, delay, opaque) { - return delay <= 1e3 - ? setTimeout(callback, delay, opaque) - : new Timeout(callback, delay, opaque) + /** + * The setTimeout() method sets a timer which executes a function once the + * timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {NodeJS.Timeout|FastTimer} + */ + setTimeout (callback, delay, arg) { + // If the delay is less than or equal to the RESOLUTION_MS value return a + // native Node.js Timer instance. + return delay <= RESOLUTION_MS + ? setTimeout(callback, delay, arg) + : new FastTimer(callback, delay, arg) }, + /** + * The clearTimeout method cancels an instantiated Timer previously created + * by calling setTimeout. + * + * @param {NodeJS.Timeout|FastTimer} timeout + */ clearTimeout (timeout) { - if (timeout instanceof Timeout) { + // If the timeout is a FastTimer, call its own clear method. + if (timeout[kFastTimer]) { + /** + * @type {FastTimer} + */ timeout.clear() + // Otherwise it is an instance of a native NodeJS.Timeout, so call the + // Node.js native clearTimeout function. } else { clearTimeout(timeout) } - } + }, + /** + * The setFastTimeout() method sets a fastTimer which executes a function once + * the timer expires. + * @param {Function} callback A function to be executed after the timer + * expires. + * @param {number} delay The time, in milliseconds that the timer should + * wait before the specified function or code is executed. + * @param {*} [arg] An optional argument to be passed to the callback function + * when the timer expires. + * @returns {FastTimer} + */ + setFastTimeout (callback, delay, arg) { + return new FastTimer(callback, delay, arg) + }, + /** + * The clearTimeout method cancels an instantiated FastTimer previously + * created by calling setFastTimeout. + * + * @param {FastTimer} timeout + */ + clearFastTimeout (timeout) { + timeout.clear() + }, + /** + * The now method returns the value of the internal fast timer clock. + * + * @returns {number} + */ + now () { + return fastNow + }, + /** + * Trigger the onTick function to process the fastTimers array. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + * @param {number} [delay=0] The delay in milliseconds to add to the now value. + */ + tick (delay = 0) { + fastNow += delay - RESOLUTION_MS + 1 + onTick() + onTick() + }, + /** + * Reset FastTimers. + * Exported for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + reset () { + fastNow = 0 + fastTimers.length = 0 + clearTimeout(fastNowTimeout) + fastNowTimeout = null + }, + /** + * Exporting for testing purposes only. + * Marking as deprecated to discourage any use outside of testing. + * @deprecated + */ + kFastTimer } @@ -42842,28 +43327,31 @@ module.exports = { "use strict"; -const corsSafeListedMethods = ['GET', 'HEAD', 'POST'] +const corsSafeListedMethods = /** @type {const} */ (['GET', 'HEAD', 'POST']) const corsSafeListedMethodsSet = new Set(corsSafeListedMethods) -const nullBodyStatus = [101, 204, 205, 304] +const nullBodyStatus = /** @type {const} */ ([101, 204, 205, 304]) -const redirectStatus = [301, 302, 303, 307, 308] +const redirectStatus = /** @type {const} */ ([301, 302, 303, 307, 308]) const redirectStatusSet = new Set(redirectStatus) -// https://fetch.spec.whatwg.org/#block-bad-port -const badPorts = [ +/** + * @see https://fetch.spec.whatwg.org/#block-bad-port + */ +const badPorts = /** @type {const} */ ([ '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79', '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137', '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532', '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723', '2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679', '6697', '10080' -] - +]) const badPortsSet = new Set(badPorts) -// https://w3c.github.io/webappsec-referrer-policy/#referrer-policies -const referrerPolicy = [ +/** + * @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies + */ +const referrerPolicy = /** @type {const} */ ([ '', 'no-referrer', 'no-referrer-when-downgrade', @@ -42873,29 +43361,31 @@ const referrerPolicy = [ 'origin-when-cross-origin', 'strict-origin-when-cross-origin', 'unsafe-url' -] +]) const referrerPolicySet = new Set(referrerPolicy) -const requestRedirect = ['follow', 'manual', 'error'] +const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error']) -const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE'] +const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE']) const safeMethodsSet = new Set(safeMethods) -const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors'] +const requestMode = /** @type {const} */ (['navigate', 'same-origin', 'no-cors', 'cors']) -const requestCredentials = ['omit', 'same-origin', 'include'] +const requestCredentials = /** @type {const} */ (['omit', 'same-origin', 'include']) -const requestCache = [ +const requestCache = /** @type {const} */ ([ 'default', 'no-store', 'reload', 'no-cache', 'force-cache', 'only-if-cached' -] +]) -// https://fetch.spec.whatwg.org/#request-body-header-name -const requestBodyHeader = [ +/** + * @see https://fetch.spec.whatwg.org/#request-body-header-name + */ +const requestBodyHeader = /** @type {const} */ ([ 'content-encoding', 'content-language', 'content-location', @@ -42905,18 +43395,22 @@ const requestBodyHeader = [ // removed in the Headers implementation. However, undici doesn't // filter out headers, so we add it here. 'content-length' -] +]) -// https://fetch.spec.whatwg.org/#enumdef-requestduplex -const requestDuplex = [ +/** + * @see https://fetch.spec.whatwg.org/#enumdef-requestduplex + */ +const requestDuplex = /** @type {const} */ ([ 'half' -] +]) -// http://fetch.spec.whatwg.org/#forbidden-method -const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK'] +/** + * @see http://fetch.spec.whatwg.org/#forbidden-method + */ +const forbiddenMethods = /** @type {const} */ (['CONNECT', 'TRACE', 'TRACK']) const forbiddenMethodsSet = new Set(forbiddenMethods) -const subresource = [ +const subresource = /** @type {const} */ ([ 'audio', 'audioworklet', 'font', @@ -42929,7 +43423,7 @@ const subresource = [ 'video', 'xslt', '' -] +]) const subresourceSet = new Set(subresource) module.exports = { @@ -43992,11 +44486,21 @@ function multipartFormDataParser (input, mimeType) { // the first byte. const position = { position: 0 } - // Note: undici addition, allow \r\n before the body. - if (input[0] === 0x0d && input[1] === 0x0a) { + // Note: undici addition, allows leading and trailing CRLFs. + while (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) { position.position += 2 } + let trailing = input.length + + while (input[trailing - 1] === 0x0a && input[trailing - 2] === 0x0d) { + trailing -= 2 + } + + if (trailing !== input.length) { + input = input.subarray(0, trailing) + } + // 5. While true: while (true) { // 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D @@ -47526,9 +48030,15 @@ async function httpNetworkFetch ( finishFlush: zlib.constants.Z_SYNC_FLUSH })) } else if (coding === 'deflate') { - decoders.push(createInflate()) + decoders.push(createInflate({ + flush: zlib.constants.Z_SYNC_FLUSH, + finishFlush: zlib.constants.Z_SYNC_FLUSH + })) } else if (coding === 'br') { - decoders.push(zlib.createBrotliDecompress()) + decoders.push(zlib.createBrotliDecompress({ + flush: zlib.constants.BROTLI_OPERATION_FLUSH, + finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH + })) } else { decoders.length = 0 break @@ -47536,13 +48046,19 @@ async function httpNetworkFetch ( } } + const onError = this.onError.bind(this) + resolve({ status, statusText, headersList, body: decoders.length - ? pipeline(this.body, ...decoders, () => { }) - : this.body.on('error', () => { }) + ? pipeline(this.body, ...decoders, (err) => { + if (err) { + this.onError(err) + } + }).on('error', onError) + : this.body.on('error', onError) }) return true @@ -50651,6 +51167,14 @@ function buildContentRange (rangeStart, rangeEnd, fullLength) { // interpreted as a zlib stream, otherwise it's interpreted as a // raw deflate stream. class InflateStream extends Transform { + #zlibOptions + + /** @param {zlib.ZlibOptions} [zlibOptions] */ + constructor (zlibOptions) { + super() + this.#zlibOptions = zlibOptions + } + _transform (chunk, encoding, callback) { if (!this._inflateStream) { if (chunk.length === 0) { @@ -50658,8 +51182,8 @@ class InflateStream extends Transform { return } this._inflateStream = (chunk[0] & 0x0F) === 0x08 - ? zlib.createInflate() - : zlib.createInflateRaw() + ? zlib.createInflate(this.#zlibOptions) + : zlib.createInflateRaw(this.#zlibOptions) this._inflateStream.on('data', this.push.bind(this)) this._inflateStream.on('end', () => this.push(null)) @@ -50678,8 +51202,12 @@ class InflateStream extends Transform { } } -function createInflate () { - return new InflateStream() +/** + * @param {zlib.ZlibOptions} [zlibOptions] + * @returns {InflateStream} + */ +function createInflate (zlibOptions) { + return new InflateStream(zlibOptions) } /**