From a71c40ac349c56b81a817bf27abc797428df6b90 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:01:57 +0000 Subject: [PATCH 1/2] Bump undici from 5.26.4 to 5.28.3 Bumps [undici](https://github.com/nodejs/undici) from 5.26.4 to 5.28.3. - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](https://github.com/nodejs/undici/compare/v5.26.4...v5.28.3) --- updated-dependencies: - dependency-name: undici dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index ea9d0988..9d181ceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7480,9 +7480,9 @@ } }, "node_modules/undici": { - "version": "5.26.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", - "integrity": "sha512-OG+QOf0fTLtazL9P9X7yqWxQ+Z0395Wk6DSkyTxtaq3wQEjIroVe7Y4asCX/vcCxYpNGMnwz8F0qbRYUoaQVMw==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -13174,9 +13174,9 @@ } }, "undici": { - "version": "5.26.4", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.4.tgz", - "integrity": "sha512-OG+QOf0fTLtazL9P9X7yqWxQ+Z0395Wk6DSkyTxtaq3wQEjIroVe7Y4asCX/vcCxYpNGMnwz8F0qbRYUoaQVMw==", + "version": "5.28.3", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.3.tgz", + "integrity": "sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==", "requires": { "@fastify/busboy": "^2.0.0" } From 4406af41d42c45fb4148424a0a943f0cd459b255 Mon Sep 17 00:00:00 2001 From: Node Builder <217586+NodeBuilder[bot]@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:02:31 +0000 Subject: [PATCH 2/2] npm run build --- dist/main.js | 1322 +++++++++++++++++++++++++++++++++++--------------- 1 file changed, 921 insertions(+), 401 deletions(-) diff --git a/dist/main.js b/dist/main.js index 7c27bd5b..58cdbba0 100644 --- a/dist/main.js +++ b/dist/main.js @@ -942,7 +942,7 @@ var tunnel$2 = {}; var tls$1 = require$$1$1; var http$2 = require$$2$1; var https$1 = require$$3; -var events$2 = require$$0$2; +var events$1 = require$$0$2; var util$k = require$$1; @@ -1005,7 +1005,7 @@ function TunnelingAgent(options) { self.removeSocket(socket); }); } -util$k.inherits(TunnelingAgent, events$2.EventEmitter); +util$k.inherits(TunnelingAgent, events$1.EventEmitter); TunnelingAgent.prototype.addRequest = function addRequest(req, host, port, localAddress) { var self = this; @@ -1263,7 +1263,9 @@ var symbols$4 = { kHTTP2BuildRequest: Symbol('http2 build request'), kHTTP1BuildRequest: Symbol('http1 build request'), kHTTP2CopyHeaders: Symbol('http2 copy headers'), - kHTTPConnVersion: Symbol('http connection version') + kHTTPConnVersion: Symbol('http connection version'), + kRetryHandlerDefaultRetry: Symbol('retry agent default retry'), + kConstruct: Symbol('constructable') }; let UndiciError$2 = class UndiciError extends Error { @@ -1459,6 +1461,19 @@ let ResponseExceededMaxSizeError$1 = class ResponseExceededMaxSizeError extends } }; +let RequestRetryError$1 = class RequestRetryError extends UndiciError$2 { + constructor (message, code, { headers, data }) { + super(message); + Error.captureStackTrace(this, RequestRetryError); + this.name = 'RequestRetryError'; + this.message = message || 'Request retry error'; + this.code = 'UND_ERR_REQ_RETRY'; + this.statusCode = code; + this.data = data; + this.headers = headers; + } +}; + var errors$1 = { HTTPParserError: HTTPParserError$1, UndiciError: UndiciError$2, @@ -1478,10 +1493,11 @@ var errors$1 = { NotSupportedError: NotSupportedError$2, ResponseContentLengthMismatchError: ResponseContentLengthMismatchError$1, BalancedPoolMissingUpstreamError: BalancedPoolMissingUpstreamError$1, - ResponseExceededMaxSizeError: ResponseExceededMaxSizeError$1 + ResponseExceededMaxSizeError: ResponseExceededMaxSizeError$1, + RequestRetryError: RequestRetryError$1 }; -const assert$8 = require$$0$3; +const assert$9 = require$$0$3; const { kDestroyed: kDestroyed$1, kBodyUsed: kBodyUsed$1 } = symbols$4; const { IncomingMessage } = require$$2$1; const stream$1 = require$$0$4; @@ -1605,14 +1621,14 @@ function getHostname (host) { if (host[0] === '[') { const idx = host.indexOf(']'); - assert$8(idx !== -1); - return host.substr(1, idx - 1) + assert$9(idx !== -1); + return host.substring(1, idx) } const idx = host.indexOf(':'); if (idx === -1) return host - return host.substr(0, idx) + return host.substring(0, idx) } // IP addresses are not valid server names per RFC6066 @@ -1622,7 +1638,7 @@ function getServerName (host) { return null } - assert$8.strictEqual(typeof host, 'string'); + assert$9.strictEqual(typeof host, 'string'); const servername = getHostname(host); if (net$2.isIP(servername)) { @@ -1671,7 +1687,7 @@ function isReadableAborted (stream) { } function destroy (stream, err) { - if (!isStream(stream) || isDestroyed(stream)) { + if (stream == null || !isStream(stream) || isDestroyed(stream)) { return } @@ -1699,7 +1715,7 @@ function parseKeepAliveTimeout (val) { return m ? parseInt(m[1], 10) * 1000 : null } -function parseHeaders (headers, obj = {}) { +function parseHeaders$1 (headers, obj = {}) { // For H2 support if (!Array.isArray(headers)) return headers @@ -1709,7 +1725,7 @@ function parseHeaders (headers, obj = {}) { if (!val) { if (Array.isArray(headers[i + 1])) { - obj[key] = headers[i + 1]; + obj[key] = headers[i + 1].map(x => x.toString('utf8')); } else { obj[key] = headers[i + 1].toString('utf8'); } @@ -1800,7 +1816,7 @@ function validateHandler (handler, method, upgrade) { // A body is disturbed if it has been read from and it cannot // be re-used without losing state or data. -function isDisturbed (body) { +function isDisturbed$1 (body) { return !!(body && ( stream$1.isDisturbed ? stream$1.isDisturbed(body) || body[kBodyUsed$1] // TODO (fix): Why is body[kBodyUsed] needed? @@ -1912,16 +1928,7 @@ function throwIfAborted (signal) { } } -let events$1; function addAbortListener$1 (signal, listener) { - if (typeof Symbol.dispose === 'symbol') { - if (!events$1) { - events$1 = require$$0$2; - } - if (typeof events$1.addAbortListener === 'function' && 'aborted' in signal) { - return events$1.addAbortListener(signal, listener) - } - } if ('addEventListener' in signal) { signal.addEventListener('abort', listener, { once: true }); return () => signal.removeEventListener('abort', listener) @@ -1945,13 +1952,28 @@ function toUSVString$2 (val) { return `${val}` } +// Parsed accordingly to RFC 9110 +// https://www.rfc-editor.org/rfc/rfc9110#field.content-range +function parseRangeHeader$1 (range) { + if (range == null || range === '') return { start: 0, end: null, size: null } + + const m = range ? range.match(/^bytes (\d+)-(\d+)\/(\d+)?$/) : null; + return m + ? { + start: parseInt(m[1]), + end: m[2] ? parseInt(m[2]) : null, + size: m[3] ? parseInt(m[3]) : null + } + : null +} + const kEnumerableProperty = Object.create(null); kEnumerableProperty.enumerable = true; var util$j = { kEnumerableProperty, nop: nop$1, - isDisturbed, + isDisturbed: isDisturbed$1, isErrored, isReadable, toUSVString: toUSVString$2, @@ -1965,7 +1987,7 @@ var util$j = { isAsyncIterable, isDestroyed, parseRawHeaders, - parseHeaders, + parseHeaders: parseHeaders$1, parseKeepAliveTimeout, destroy, bodyLength, @@ -1978,9 +2000,11 @@ var util$j = { buildURL: buildURL$2, throwIfAborted, addAbortListener: addAbortListener$1, + parseRangeHeader: parseRangeHeader$1, nodeMajor, nodeMinor, - nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13) + nodeHasAutoSelectFamily: nodeMajor > 18 || (nodeMajor === 18 && nodeMinor >= 13), + safeHTTPMethods: ['GET', 'HEAD', 'OPTIONS', 'TRACE'] }; let fastNow = Date.now(); @@ -3512,10 +3536,12 @@ function requireConstants$3 () { const { MessageChannel, receiveMessageOnPort } = require$$0$6; const corsSafeListedMethods = ['GET', 'HEAD', 'POST']; + const corsSafeListedMethodsSet = new Set(corsSafeListedMethods); const nullBodyStatus = [101, 204, 205, 304]; const redirectStatus = [301, 302, 303, 307, 308]; + const redirectStatusSet = new Set(redirectStatus); // https://fetch.spec.whatwg.org/#block-bad-port const badPorts = [ @@ -3527,6 +3553,8 @@ function requireConstants$3 () { '10080' ]; + const badPortsSet = new Set(badPorts); + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policies const referrerPolicy = [ '', @@ -3539,10 +3567,12 @@ function requireConstants$3 () { 'strict-origin-when-cross-origin', 'unsafe-url' ]; + const referrerPolicySet = new Set(referrerPolicy); const requestRedirect = ['follow', 'manual', 'error']; const safeMethods = ['GET', 'HEAD', 'OPTIONS', 'TRACE']; + const safeMethodsSet = new Set(safeMethods); const requestMode = ['navigate', 'same-origin', 'no-cors', 'cors']; @@ -3577,6 +3607,7 @@ function requireConstants$3 () { // http://fetch.spec.whatwg.org/#forbidden-method const forbiddenMethods = ['CONNECT', 'TRACE', 'TRACK']; + const forbiddenMethodsSet = new Set(forbiddenMethods); const subresource = [ 'audio', @@ -3592,6 +3623,7 @@ function requireConstants$3 () { 'xslt', '' ]; + const subresourceSet = new Set(subresource); /** @type {globalThis['DOMException']} */ const DOMException = globalThis.DOMException ?? (() => { @@ -3641,7 +3673,14 @@ function requireConstants$3 () { nullBodyStatus, safeMethods, badPorts, - requestDuplex + requestDuplex, + subresourceSet, + badPortsSet, + redirectStatusSet, + corsSafeListedMethodsSet, + safeMethodsSet, + forbiddenMethodsSet, + referrerPolicySet }; return constants$4; } @@ -3701,7 +3740,7 @@ function requireUtil$4 () { if (hasRequiredUtil$4) return util$i; hasRequiredUtil$4 = 1; - const { redirectStatus, badPorts, referrerPolicy: referrerPolicyTokens } = requireConstants$3(); + const { redirectStatusSet, referrerPolicySet: referrerPolicyTokens, badPortsSet } = requireConstants$3(); const { getGlobalOrigin } = requireGlobal(); const { performance } = require$$2$2; const { isBlobLike, toUSVString, ReadableStreamFrom } = util$j; @@ -3730,7 +3769,7 @@ function requireUtil$4 () { // https://fetch.spec.whatwg.org/#concept-response-location-url function responseLocationURL (response, requestFragment) { // 1. If response’s status is not a redirect status, then return null. - if (!redirectStatus.includes(response.status)) { + if (!redirectStatusSet.has(response.status)) { return null } @@ -3765,7 +3804,7 @@ function requireUtil$4 () { // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port, // then return blocked. - if (urlIsHttpHttpsScheme(url) && badPorts.includes(url.port)) { + if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) { return 'blocked' } @@ -3804,52 +3843,57 @@ function requireUtil$4 () { return true } - function isTokenChar (c) { - return !( - c >= 0x7f || - c <= 0x20 || - c === '(' || - c === ')' || - c === '<' || - c === '>' || - c === '@' || - c === ',' || - c === ';' || - c === ':' || - c === '\\' || - c === '"' || - c === '/' || - c === '[' || - c === ']' || - c === '?' || - c === '=' || - c === '{' || - c === '}' - ) + /** + * @see https://tools.ietf.org/html/rfc7230#section-3.2.6 + * @param {number} c + */ + function isTokenCharCode (c) { + switch (c) { + case 0x22: + case 0x28: + case 0x29: + case 0x2c: + case 0x2f: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + case 0x40: + case 0x5b: + case 0x5c: + case 0x5d: + case 0x7b: + case 0x7d: + // DQUOTE and "(),/:;<=>?@[\]{}" + return false + default: + // VCHAR %x21-7E + return c >= 0x21 && c <= 0x7e + } } - // See RFC 7230, Section 3.2.6. - // https://github.com/chromium/chromium/blob/d7da0240cae77824d1eda25745c4022757499131/third_party/blink/renderer/platform/network/http_parsers.cc#L321 + /** + * @param {string} characters + */ function isValidHTTPToken (characters) { - if (!characters || typeof characters !== 'string') { + if (characters.length === 0) { return false } for (let i = 0; i < characters.length; ++i) { - const c = characters.charCodeAt(i); - if (c > 0x7f || !isTokenChar(c)) { + if (!isTokenCharCode(characters.charCodeAt(i))) { return false } } return true } - // https://fetch.spec.whatwg.org/#header-name - // https://github.com/chromium/chromium/blob/b3d37e6f94f87d59e44662d6078f6a12de845d17/net/http/http_util.cc#L342 + /** + * @see https://fetch.spec.whatwg.org/#header-name + * @param {string} potentialValue + */ function isValidHeaderName (potentialValue) { - if (potentialValue.length === 0) { - return false - } - return isValidHTTPToken(potentialValue) } @@ -3907,7 +3951,7 @@ function requireUtil$4 () { // The left-most policy is the fallback. for (let i = policyHeader.length; i !== 0; i--) { const token = policyHeader[i - 1].trim(); - if (referrerPolicyTokens.includes(token)) { + if (referrerPolicyTokens.has(token)) { policy = token; break } @@ -4393,11 +4437,30 @@ function requireUtil$4 () { fetchParams.controller.state === 'terminated' } - // https://fetch.spec.whatwg.org/#concept-method-normalize + const normalizeMethodRecord = { + delete: 'DELETE', + DELETE: 'DELETE', + get: 'GET', + GET: 'GET', + head: 'HEAD', + HEAD: 'HEAD', + options: 'OPTIONS', + OPTIONS: 'OPTIONS', + post: 'POST', + POST: 'POST', + put: 'PUT', + PUT: 'PUT' + }; + + // Note: object prototypes should not be able to be referenced. e.g. `Object#hasOwnProperty`. + Object.setPrototypeOf(normalizeMethodRecord, null); + + /** + * @see https://fetch.spec.whatwg.org/#concept-method-normalize + * @param {string} method + */ function normalizeMethod (method) { - return /^(DELETE|GET|HEAD|OPTIONS|POST|PUT)$/i.test(method) - ? method.toUpperCase() - : method + return normalizeMethodRecord[method.toLowerCase()] ?? method } // https://infra.spec.whatwg.org/#serialize-a-javascript-value-to-a-json-string @@ -4742,7 +4805,8 @@ function requireUtil$4 () { urlIsLocal, urlHasHttpsScheme, urlIsHttpHttpsScheme, - readAllBytes + readAllBytes, + normalizeMethodRecord }; return util$i; } @@ -5199,12 +5263,10 @@ function requireWebidl () { // 2. If the value of any element of x is greater than // 255, then throw a TypeError. for (let index = 0; index < x.length; index++) { - const charCode = x.charCodeAt(index); - - if (charCode > 255) { + if (x.charCodeAt(index) > 255) { throw new TypeError( 'Cannot convert argument to a ByteString because the character at ' + - `index ${index} has a value of ${charCode} which is greater than 255.` + `index ${index} has a value of ${x.charCodeAt(index)} which is greater than 255.` ) } } @@ -5548,17 +5610,14 @@ function requireDataURL () { * @param {boolean} excludeFragment */ function URLSerializer (url, excludeFragment = false) { - const href = url.href; - if (!excludeFragment) { - return href + return url.href } - const hash = href.lastIndexOf('#'); - if (hash === -1) { - return href - } - return href.slice(0, hash) + const href = url.href; + const hashLength = url.hash.length; + + return hashLength === 0 ? href : href.substring(0, href.length - hashLength) } // https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points @@ -6074,6 +6133,7 @@ function requireFile () { const { webidl } = requireWebidl(); const { parseMIMEType, serializeAMimeType } = requireDataURL(); const { kEnumerableProperty } = util$j; + const encoder = new TextEncoder(); class File extends Blob { constructor (fileBits, fileName, options = {}) { @@ -6347,7 +6407,7 @@ function requireFile () { } // 3. Append the result of UTF-8 encoding s to bytes. - bytes.push(new TextEncoder().encode(s)); + bytes.push(encoder.encode(s)); } else if ( types.isAnyArrayBuffer(element) || types.isTypedArray(element) @@ -6717,6 +6777,8 @@ function requireBody () { /** @type {globalThis['File']} */ const File = NativeFile ?? UndiciFile; + const textEncoder = new TextEncoder(); + const textDecoder = new TextDecoder(); // https://fetch.spec.whatwg.org/#concept-bodyinit-extract function extractBody (object, keepalive = false) { @@ -6740,7 +6802,7 @@ function requireBody () { stream = new ReadableStream({ async pull (controller) { controller.enqueue( - typeof source === 'string' ? new TextEncoder().encode(source) : source + typeof source === 'string' ? textEncoder.encode(source) : source ); queueMicrotask(() => readableStreamClose(controller)); }, @@ -6810,7 +6872,6 @@ function requireBody () { // - That the content-length is calculated in advance. // - And that all parts are pre-encoded and ready to be sent. - const enc = new TextEncoder(); const blobParts = []; const rn = new Uint8Array([13, 10]); // '\r\n' length = 0; @@ -6818,13 +6879,13 @@ function requireBody () { for (const [name, value] of object) { if (typeof value === 'string') { - const chunk = enc.encode(prefix + + const chunk = textEncoder.encode(prefix + `; name="${escape(normalizeLinefeeds(name))}"` + `\r\n\r\n${normalizeLinefeeds(value)}\r\n`); blobParts.push(chunk); length += chunk.byteLength; } else { - const chunk = enc.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + + const chunk = textEncoder.encode(`${prefix}; name="${escape(normalizeLinefeeds(name))}"` + (value.name ? `; filename="${escape(value.name)}"` : '') + '\r\n' + `Content-Type: ${ value.type || 'application/octet-stream' @@ -6838,7 +6899,7 @@ function requireBody () { } } - const chunk = enc.encode(`--${boundary}--`); + const chunk = textEncoder.encode(`--${boundary}--`); blobParts.push(chunk); length += chunk.byteLength; if (hasUnknownSizeValue) { @@ -7134,14 +7195,16 @@ function requireBody () { let text = ''; // application/x-www-form-urlencoded parser will keep the BOM. // https://url.spec.whatwg.org/#concept-urlencoded-parser - const textDecoder = new TextDecoder('utf-8', { ignoreBOM: true }); + // Note that streaming decoder is stateful and cannot be reused + const streamingDecoder = new TextDecoder('utf-8', { ignoreBOM: true }); + for await (const chunk of consumeBody(this[kState].body)) { if (!isUint8Array(chunk)) { throw new TypeError('Expected Uint8Array chunk') } - text += textDecoder.decode(chunk, { stream: true }); + text += streamingDecoder.decode(chunk, { stream: true }); } - text += textDecoder.decode(); + text += streamingDecoder.decode(); entries = new URLSearchParams(text); } catch (err) { // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. @@ -7256,7 +7319,7 @@ function requireBody () { // 3. Process a queue with an instance of UTF-8’s // decoder, ioQueue, output, and "replacement". - const output = new TextDecoder().decode(buffer); + const output = textDecoder.decode(buffer); // 4. Return output. return output @@ -7298,7 +7361,7 @@ const { InvalidArgumentError: InvalidArgumentError$k, NotSupportedError: NotSupportedError$1 } = errors$1; -const assert$7 = require$$0$3; +const assert$8 = require$$0$3; const { kHTTP2BuildRequest: kHTTP2BuildRequest$1, kHTTP2CopyHeaders: kHTTP2CopyHeaders$1, kHTTP1BuildRequest: kHTTP1BuildRequest$1 } = symbols$4; const util$h = util$j; @@ -7406,10 +7469,29 @@ let Request$1 = class Request { this.method = method; + this.abort = null; + if (body == null) { this.body = null; } else if (util$h.isStream(body)) { this.body = body; + + const rState = this.body._readableState; + if (!rState || !rState.autoDestroy) { + this.endHandler = function autoDestroy () { + util$h.destroy(this); + }; + this.body.on('end', this.endHandler); + } + + this.errorHandler = err => { + if (this.abort) { + this.abort(err); + } else { + this.error = err; + } + }; + this.body.on('error', this.errorHandler); } else if (util$h.isBuffer(body)) { this.body = body.byteLength ? body : null; } else if (ArrayBuffer.isView(body)) { @@ -7505,9 +7587,9 @@ let Request$1 = class Request { onBodySent (chunk) { if (this[kHandler].onBodySent) { try { - this[kHandler].onBodySent(chunk); + return this[kHandler].onBodySent(chunk) } catch (err) { - this.onError(err); + this.abort(err); } } } @@ -7516,51 +7598,83 @@ let Request$1 = class Request { if (channels$1.bodySent.hasSubscribers) { channels$1.bodySent.publish({ request: this }); } + + if (this[kHandler].onRequestSent) { + try { + return this[kHandler].onRequestSent() + } catch (err) { + this.abort(err); + } + } } onConnect (abort) { - assert$7(!this.aborted); - assert$7(!this.completed); + assert$8(!this.aborted); + assert$8(!this.completed); - return this[kHandler].onConnect(abort) + if (this.error) { + abort(this.error); + } else { + this.abort = abort; + return this[kHandler].onConnect(abort) + } } onHeaders (statusCode, headers, resume, statusText) { - assert$7(!this.aborted); - assert$7(!this.completed); + assert$8(!this.aborted); + assert$8(!this.completed); if (channels$1.headers.hasSubscribers) { channels$1.headers.publish({ request: this, response: { statusCode, headers, statusText } }); } - return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + try { + return this[kHandler].onHeaders(statusCode, headers, resume, statusText) + } catch (err) { + this.abort(err); + } } onData (chunk) { - assert$7(!this.aborted); - assert$7(!this.completed); + assert$8(!this.aborted); + assert$8(!this.completed); - return this[kHandler].onData(chunk) + try { + return this[kHandler].onData(chunk) + } catch (err) { + this.abort(err); + return false + } } onUpgrade (statusCode, headers, socket) { - assert$7(!this.aborted); - assert$7(!this.completed); + assert$8(!this.aborted); + assert$8(!this.completed); return this[kHandler].onUpgrade(statusCode, headers, socket) } onComplete (trailers) { - assert$7(!this.aborted); + this.onFinally(); + + assert$8(!this.aborted); this.completed = true; if (channels$1.trailers.hasSubscribers) { channels$1.trailers.publish({ request: this, trailers }); } - return this[kHandler].onComplete(trailers) + + try { + return this[kHandler].onComplete(trailers) + } catch (err) { + // TODO (fix): This might be a bad idea? + this.onError(err); + } } onError (error) { + this.onFinally(); + if (channels$1.error.hasSubscribers) { channels$1.error.publish({ request: this, error }); } @@ -7569,9 +7683,22 @@ let Request$1 = class Request { return } this.aborted = true; + return this[kHandler].onError(error) } + onFinally () { + if (this.errorHandler) { + this.body.off('error', this.errorHandler); + this.errorHandler = null; + } + + if (this.endHandler) { + this.body.off('end', this.endHandler); + this.endHandler = null; + } + } + // TODO: adjust to support H2 addHeader (key, value) { processHeader(this, key, value); @@ -7938,7 +8065,7 @@ let DispatcherBase$4 = class DispatcherBase extends Dispatcher$2 { var dispatcherBase = DispatcherBase$4; const net$1 = require$$0$5; -const assert$6 = require$$0$3; +const assert$7 = require$$0$3; const util$g = util$j; const { InvalidArgumentError: InvalidArgumentError$i, ConnectTimeoutError } = errors$1; @@ -8030,7 +8157,7 @@ function buildConnector$4 ({ allowH2, maxCachedSessions, socketPath, timeout, .. const sessionKey = servername || hostname; const session = sessionCache.get(sessionKey) || null; - assert$6(sessionKey); + assert$7(sessionKey); socket = tls.connect({ highWaterMark: 16384, // TLS in node can't have bigger HWM anyway... @@ -8051,7 +8178,7 @@ function buildConnector$4 ({ allowH2, maxCachedSessions, socketPath, timeout, .. sessionCache.set(sessionKey, session); }); } else { - assert$6(!httpSocket, 'httpSocket can only be sent on TLS update'); + assert$7(!httpSocket, 'httpSocket can only be sent on TLS update'); socket = net$1.connect({ highWaterMark: 64 * 1024, // Same as nodejs fs streams. ...options, @@ -8434,7 +8561,7 @@ function requireConstants$2 () { const util$f = util$j; const { kBodyUsed } = symbols$4; -const assert$5 = require$$0$3; +const assert$6 = require$$0$3; const { InvalidArgumentError: InvalidArgumentError$h } = errors$1; const EE = require$$0$2; @@ -8449,7 +8576,7 @@ class BodyAsyncIterable { } async * [Symbol.asyncIterator] () { - assert$5(!this[kBodyUsed], 'disturbed'); + assert$6(!this[kBodyUsed], 'disturbed'); this[kBodyUsed] = true; yield * this[kBody$1]; } @@ -8478,7 +8605,7 @@ let RedirectHandler$2 = class RedirectHandler { if (util$f.bodyLength(this.opts.body) === 0) { this.opts.body .on('data', function () { - assert$5(false); + assert$6(false); }); } @@ -8622,7 +8749,7 @@ function cleanRequestHeaders (headers, removeContent, unknownOrigin) { } } } else { - assert$5(headers == null, 'headers must be an object or an array'); + assert$6(headers == null, 'headers must be an object or an array'); } return ret } @@ -8671,7 +8798,7 @@ function requireLlhttp_simdWasm () { /* global WebAssembly */ -const assert$4 = require$$0$3; +const assert$5 = require$$0$3; const net = require$$0$5; const http$1 = require$$2$1; const { pipeline: pipeline$1 } = require$$0$4; @@ -9092,7 +9219,7 @@ let Client$4 = class Client extends DispatcherBase$3 { }; function onHttp2SessionError (err) { - assert$4(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID'); + assert$5(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID'); this[kSocket][kError] = err; @@ -9120,7 +9247,7 @@ function onHTTP2GoAway (code) { client[kHTTP2Session] = null; if (client.destroyed) { - assert$4(this[kPending$2] === 0); + assert$5(this[kPending$2] === 0); // Fail entire queue. const requests = client[kQueue$1].splice(client[kRunningIdx]); @@ -9138,7 +9265,7 @@ function onHTTP2GoAway (code) { client[kPendingIdx] = client[kRunningIdx]; - assert$4(client[kRunning$3] === 0); + assert$5(client[kRunning$3] === 0); client.emit('disconnect', client[kUrl$3], @@ -9178,35 +9305,35 @@ async function lazyllhttp () { return 0 }, wasm_on_status: (p, at, len) => { - assert$4.strictEqual(currentParser.ptr, p); + assert$5.strictEqual(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$4.strictEqual(currentParser.ptr, p); + assert$5.strictEqual(currentParser.ptr, p); return currentParser.onMessageBegin() || 0 }, wasm_on_header_field: (p, at, len) => { - assert$4.strictEqual(currentParser.ptr, p); + assert$5.strictEqual(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$4.strictEqual(currentParser.ptr, p); + assert$5.strictEqual(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$4.strictEqual(currentParser.ptr, p); + assert$5.strictEqual(currentParser.ptr, p); return currentParser.onHeadersComplete(statusCode, Boolean(upgrade), Boolean(shouldKeepAlive)) || 0 }, wasm_on_body: (p, at, len) => { - assert$4.strictEqual(currentParser.ptr, p); + assert$5.strictEqual(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$4.strictEqual(currentParser.ptr, p); + assert$5.strictEqual(currentParser.ptr, p); return currentParser.onMessageComplete() || 0 } @@ -9230,7 +9357,7 @@ const TIMEOUT_IDLE = 3; class Parser { constructor (client, socket, { exports }) { - assert$4(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0); + assert$5(Number.isFinite(client[kMaxHeadersSize]) && client[kMaxHeadersSize] > 0); this.llhttp = exports; this.ptr = this.llhttp.llhttp_alloc(constants$2.TYPE.RESPONSE); @@ -9284,12 +9411,12 @@ class Parser { return } - assert$4(this.ptr != null); - assert$4(currentParser == null); + assert$5(this.ptr != null); + assert$5(currentParser == null); this.llhttp.llhttp_resume(this.ptr); - assert$4(this.timeoutType === TIMEOUT_BODY); + assert$5(this.timeoutType === TIMEOUT_BODY); if (this.timeout) { // istanbul ignore else: only for jest if (this.timeout.refresh) { @@ -9313,9 +9440,9 @@ class Parser { } execute (data) { - assert$4(this.ptr != null); - assert$4(currentParser == null); - assert$4(!this.paused); + assert$5(this.ptr != null); + assert$5(currentParser == null); + assert$5(!this.paused); const { socket, llhttp } = this; @@ -9375,8 +9502,8 @@ class Parser { } destroy () { - assert$4(this.ptr != null); - assert$4(currentParser == null); + assert$5(this.ptr != null); + assert$5(currentParser == null); this.llhttp.llhttp_free(this.ptr); this.ptr = null; @@ -9451,21 +9578,21 @@ class Parser { onUpgrade (head) { const { upgrade, client, socket, headers, statusCode } = this; - assert$4(upgrade); + assert$5(upgrade); const request = client[kQueue$1][client[kRunningIdx]]; - assert$4(request); + assert$5(request); - assert$4(!socket.destroyed); - assert$4(socket === client[kSocket]); - assert$4(!this.paused); - assert$4(request.upgrade || request.method === 'CONNECT'); + assert$5(!socket.destroyed); + assert$5(socket === client[kSocket]); + assert$5(!this.paused); + assert$5(request.upgrade || request.method === 'CONNECT'); this.statusCode = null; this.statusText = ''; this.shouldKeepAlive = null; - assert$4(this.headers.length % 2 === 0); + assert$5(this.headers.length % 2 === 0); this.headers = []; this.headersSize = 0; @@ -9510,8 +9637,8 @@ class Parser { return -1 } - assert$4(!this.upgrade); - assert$4(this.statusCode < 200); + assert$5(!this.upgrade); + assert$5(this.statusCode < 200); if (statusCode === 100) { util$e.destroy(socket, new SocketError$2('bad response', util$e.getSocketInfo(socket))); @@ -9524,7 +9651,7 @@ class Parser { return -1 } - assert$4.strictEqual(this.timeoutType, TIMEOUT_HEADERS); + assert$5.strictEqual(this.timeoutType, TIMEOUT_HEADERS); this.statusCode = statusCode; this.shouldKeepAlive = ( @@ -9546,18 +9673,18 @@ class Parser { } if (request.method === 'CONNECT') { - assert$4(client[kRunning$3] === 1); + assert$5(client[kRunning$3] === 1); this.upgrade = true; return 2 } if (upgrade) { - assert$4(client[kRunning$3] === 1); + assert$5(client[kRunning$3] === 1); this.upgrade = true; return 2 } - assert$4(this.headers.length % 2 === 0); + assert$5(this.headers.length % 2 === 0); this.headers = []; this.headersSize = 0; @@ -9582,11 +9709,9 @@ class Parser { socket[kReset] = true; } - let pause; - try { - pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false; - } catch (err) { - util$e.destroy(socket, err); + const pause = request.onHeaders(statusCode, headers, this.resume, statusText) === false; + + if (request.aborted) { return -1 } @@ -9614,9 +9739,9 @@ class Parser { } const request = client[kQueue$1][client[kRunningIdx]]; - assert$4(request); + assert$5(request); - assert$4.strictEqual(this.timeoutType, TIMEOUT_BODY); + assert$5.strictEqual(this.timeoutType, TIMEOUT_BODY); if (this.timeout) { // istanbul ignore else: only for jest if (this.timeout.refresh) { @@ -9624,7 +9749,7 @@ class Parser { } } - assert$4(statusCode >= 200); + assert$5(statusCode >= 200); if (maxResponseSize > -1 && this.bytesRead + buf.length > maxResponseSize) { util$e.destroy(socket, new ResponseExceededMaxSizeError()); @@ -9633,13 +9758,8 @@ class Parser { this.bytesRead += buf.length; - try { - if (request.onData(buf) === false) { - return constants$2.ERROR.PAUSED - } - } catch (err) { - util$e.destroy(socket, err); - return -1 + if (request.onData(buf) === false) { + return constants$2.ERROR.PAUSED } } @@ -9655,9 +9775,9 @@ class Parser { } const request = client[kQueue$1][client[kRunningIdx]]; - assert$4(request); + assert$5(request); - assert$4(statusCode >= 100); + assert$5(statusCode >= 100); this.statusCode = null; this.statusText = ''; @@ -9666,7 +9786,7 @@ class Parser { this.keepAlive = ''; this.connection = ''; - assert$4(this.headers.length % 2 === 0); + assert$5(this.headers.length % 2 === 0); this.headers = []; this.headersSize = 0; @@ -9680,16 +9800,12 @@ class Parser { return -1 } - try { - request.onComplete(headers); - } catch (err) { - errorRequest(client, request, err); - } + request.onComplete(headers); client[kQueue$1][client[kRunningIdx]++] = null; if (socket[kWriting]) { - assert$4.strictEqual(client[kRunning$3], 0); + assert$5.strictEqual(client[kRunning$3], 0); // Response completed before request. util$e.destroy(socket, new InformationalError('reset')); return constants$2.ERROR.PAUSED @@ -9720,7 +9836,7 @@ function onParserTimeout (parser) { /* istanbul ignore else */ if (timeoutType === TIMEOUT_HEADERS) { if (!socket[kWriting] || socket.writableNeedDrain || client[kRunning$3] > 1) { - assert$4(!parser.paused, 'cannot be paused while waiting for headers'); + assert$5(!parser.paused, 'cannot be paused while waiting for headers'); util$e.destroy(socket, new HeadersTimeoutError()); } } else if (timeoutType === TIMEOUT_BODY) { @@ -9728,7 +9844,7 @@ function onParserTimeout (parser) { util$e.destroy(socket, new BodyTimeoutError()); } } else if (timeoutType === TIMEOUT_IDLE) { - assert$4(client[kRunning$3] === 0 && client[kKeepAliveTimeoutValue]); + assert$5(client[kRunning$3] === 0 && client[kKeepAliveTimeoutValue]); util$e.destroy(socket, new InformationalError('socket idle timeout')); } } @@ -9743,7 +9859,7 @@ function onSocketReadable () { function onSocketError (err) { const { [kClient$1]: client, [kParser]: parser } = this; - assert$4(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID'); + assert$5(err.code !== 'ERR_TLS_CERT_ALTNAME_INVALID'); if (client[kHTTPConnVersion] !== 'h2') { // On Mac OS, we get an ECONNRESET even if there is a full body to be forwarded @@ -9769,14 +9885,14 @@ function onError (client, err) { // Error is not caused by running request and not a recoverable // socket error. - assert$4(client[kPendingIdx] === client[kRunningIdx]); + assert$5(client[kPendingIdx] === client[kRunningIdx]); const requests = client[kQueue$1].splice(client[kRunningIdx]); for (let i = 0; i < requests.length; i++) { const request = requests[i]; errorRequest(client, request, err); } - assert$4(client[kSize$4] === 0); + assert$5(client[kSize$4] === 0); } } @@ -9812,7 +9928,7 @@ function onSocketClose () { client[kSocket] = null; if (client.destroyed) { - assert$4(client[kPending$2] === 0); + assert$5(client[kPending$2] === 0); // Fail entire queue. const requests = client[kQueue$1].splice(client[kRunningIdx]); @@ -9830,7 +9946,7 @@ function onSocketClose () { client[kPendingIdx] = client[kRunningIdx]; - assert$4(client[kRunning$3] === 0); + assert$5(client[kRunning$3] === 0); client.emit('disconnect', client[kUrl$3], [client], err); @@ -9838,8 +9954,8 @@ function onSocketClose () { } async function connect$1 (client) { - assert$4(!client[kConnecting]); - assert$4(!client[kSocket]); + assert$5(!client[kConnecting]); + assert$5(!client[kSocket]); let { host, hostname, protocol, port } = client[kUrl$3]; @@ -9847,10 +9963,10 @@ async function connect$1 (client) { if (hostname[0] === '[') { const idx = hostname.indexOf(']'); - assert$4(idx !== -1); - const ip = hostname.substr(1, idx - 1); + assert$5(idx !== -1); + const ip = hostname.substring(1, idx); - assert$4(net.isIP(ip)); + assert$5(net.isIP(ip)); hostname = ip; } @@ -9895,7 +10011,7 @@ async function connect$1 (client) { client[kConnecting] = false; - assert$4(socket); + assert$5(socket); const isH2 = socket.alpnProtocol === 'h2'; if (isH2) { @@ -9987,7 +10103,7 @@ async function connect$1 (client) { } if (err.code === 'ERR_TLS_CERT_ALTNAME_INVALID') { - assert$4(client[kRunning$3] === 0); + assert$5(client[kRunning$3] === 0); while (client[kPending$2] > 0 && client[kQueue$1][client[kPendingIdx]].servername === client[kServerName]) { const request = client[kQueue$1][client[kPendingIdx]++]; errorRequest(client, request, err); @@ -10027,7 +10143,7 @@ function resume (client, sync) { function _resume (client, sync) { while (true) { if (client.destroyed) { - assert$4(client[kPending$2] === 0); + assert$5(client[kPending$2] === 0); return } @@ -10127,23 +10243,7 @@ function _resume (client, sync) { return } - if (util$e.isStream(request.body) && util$e.bodyLength(request.body) === 0) { - request.body - .on('data', /* istanbul ignore next */ function () { - /* istanbul ignore next */ - assert$4(false); - }) - .on('error', function (err) { - errorRequest(client, request, err); - }) - .on('end', function () { - util$e.destroy(this); - }); - - request.body = null; - } - - if (client[kRunning$3] > 0 && + if (client[kRunning$3] > 0 && util$e.bodyLength(request.body) !== 0 && (util$e.isStream(request.body) || util$e.isAsyncIterable(request.body))) { // Request with stream or iterator body can error while other requests // are inflight and indirectly error those as well. @@ -10164,6 +10264,11 @@ function _resume (client, sync) { } } +// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.2 +function shouldSendContentLength (method) { + return method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS' && method !== 'TRACE' && method !== 'CONNECT' +} + function write (client, request) { if (client[kHTTPConnVersion] === 'h2') { writeH2(client, client[kHTTP2Session], request); @@ -10192,7 +10297,9 @@ function write (client, request) { body.read(0); } - let contentLength = util$e.bodyLength(body); + const bodyLength = util$e.bodyLength(body); + + let contentLength = bodyLength; if (contentLength === null) { contentLength = request.contentLength; @@ -10207,7 +10314,9 @@ function write (client, request) { contentLength = null; } - if (request.contentLength !== null && request.contentLength !== contentLength) { + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength !== null && request.contentLength !== contentLength) { if (client[kStrictContentLength]) { errorRequest(client, request, new RequestContentLengthMismatchError()); return false @@ -10288,16 +10397,16 @@ function write (client, request) { } /* istanbul ignore else: assertion */ - if (!body) { + if (!body || bodyLength === 0) { if (contentLength === 0) { socket.write(`${header}content-length: 0\r\n\r\n`, 'latin1'); } else { - assert$4(contentLength === null, 'no body must not have content length'); + assert$5(contentLength === null, 'no body must not have content length'); socket.write(`${header}\r\n`, 'latin1'); } request.onRequestSent(); } else if (util$e.isBuffer(body)) { - assert$4(contentLength === body.byteLength, 'buffer body must have content length'); + assert$5(contentLength === body.byteLength, 'buffer body must have content length'); socket.cork(); socket.write(`${header}content-length: ${contentLength}\r\n\r\n`, 'latin1'); @@ -10319,7 +10428,7 @@ function write (client, request) { } else if (util$e.isIterable(body)) { writeIterable({ body, client, request, socket, contentLength, header, expectsPayload }); } else { - assert$4(false); + assert$5(false); } return true @@ -10354,6 +10463,7 @@ function writeH2 (client, session, request) { return false } + /** @type {import('node:http2').ClientHttp2Stream} */ let stream; const h2State = client[kHTTP2SessionState]; @@ -10428,7 +10538,9 @@ function writeH2 (client, session, request) { contentLength = null; } - if (request.contentLength != null && request.contentLength !== contentLength) { + // https://github.com/nodejs/undici/issues/2046 + // A user agent may send a Content-Length header with 0 value, this should be allowed. + if (shouldSendContentLength(method) && contentLength > 0 && request.contentLength != null && request.contentLength !== contentLength) { if (client[kStrictContentLength]) { errorRequest(client, request, new RequestContentLengthMismatchError()); return false @@ -10438,7 +10550,7 @@ function writeH2 (client, session, request) { } if (contentLength != null) { - assert$4(body, 'no body must not have content length'); + assert$5(body, 'no body must not have content length'); headers[HTTP2_HEADER_CONTENT_LENGTH] = `${contentLength}`; } @@ -10447,14 +10559,10 @@ function writeH2 (client, session, request) { const shouldEndStream = method === 'GET' || method === 'HEAD'; if (expectContinue) { headers[HTTP2_HEADER_EXPECT] = '100-continue'; - /** - * @type {import('node:http2').ClientHttp2Stream} - */ stream = session.request(headers, { endStream: shouldEndStream, signal }); stream.once('continue', writeBodyH2); } else { - /** @type {import('node:http2').ClientHttp2Stream} */ stream = session.request(headers, { endStream: shouldEndStream, signal @@ -10466,7 +10574,9 @@ function writeH2 (client, session, request) { ++h2State.openStreams; stream.once('response', headers => { - if (request.onHeaders(Number(headers[HTTP2_HEADER_STATUS]), headers, stream.resume.bind(stream), '') === false) { + const { [HTTP2_HEADER_STATUS]: statusCode, ...realHeaders } = headers; + + if (request.onHeaders(Number(statusCode), realHeaders, stream.resume.bind(stream), '') === false) { stream.pause(); } }); @@ -10476,13 +10586,17 @@ function writeH2 (client, session, request) { }); stream.on('data', (chunk) => { - if (request.onData(chunk) === false) stream.pause(); + if (request.onData(chunk) === false) { + stream.pause(); + } }); stream.once('close', () => { h2State.openStreams -= 1; // TODO(HTTP/2): unref only if current streams count is 0 - if (h2State.openStreams === 0) session.unref(); + if (h2State.openStreams === 0) { + session.unref(); + } }); stream.once('error', function (err) { @@ -10525,7 +10639,7 @@ function writeH2 (client, session, request) { if (!body) { request.onRequestSent(); } else if (util$e.isBuffer(body)) { - assert$4(contentLength === body.byteLength, 'buffer body must have content length'); + assert$5(contentLength === body.byteLength, 'buffer body must have content length'); stream.cork(); stream.write(body); stream.uncork(); @@ -10579,13 +10693,13 @@ function writeH2 (client, session, request) { socket: client[kSocket] }); } else { - assert$4(false); + assert$5(false); } } } function writeStream ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { - assert$4(contentLength !== 0 || client[kRunning$3] === 0, 'stream body cannot be pipelined'); + assert$5(contentLength !== 0 || client[kRunning$3] === 0, 'stream body cannot be pipelined'); if (client[kHTTPConnVersion] === 'h2') { // For HTTP/2, is enough to pipe the stream @@ -10642,7 +10756,11 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength, } }; const onAbort = function () { - onFinished(new RequestAbortedError$8()); + if (finished) { + return + } + const err = new RequestAbortedError$8(); + queueMicrotask(() => onFinished(err)); }; const onFinished = function (err) { if (finished) { @@ -10651,7 +10769,7 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength, finished = true; - assert$4(socket.destroyed || (socket[kWriting] && client[kRunning$3] <= 1)); + assert$5(socket.destroyed || (socket[kWriting] && client[kRunning$3] <= 1)); socket .off('drain', onDrain) @@ -10696,7 +10814,7 @@ function writeStream ({ h2stream, body, client, request, socket, contentLength, } async function writeBlob ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { - assert$4(contentLength === body.size, 'blob body must have content length'); + assert$5(contentLength === body.size, 'blob body must have content length'); const isH2 = client[kHTTPConnVersion] === 'h2'; try { @@ -10731,7 +10849,7 @@ async function writeBlob ({ h2stream, body, client, request, socket, contentLeng } async function writeIterable ({ h2stream, body, client, request, socket, contentLength, header, expectsPayload }) { - assert$4(contentLength !== 0 || client[kRunning$3] === 0, 'iterator body cannot be pipelined'); + assert$5(contentLength !== 0 || client[kRunning$3] === 0, 'iterator body cannot be pipelined'); let callback = null; function onDrain () { @@ -10743,7 +10861,7 @@ async function writeIterable ({ h2stream, body, client, request, socket, content } const waitForDrain = () => new Promise((resolve, reject) => { - assert$4(callback === null); + assert$5(callback === null); if (socket[kError]) { reject(socket[kError]); @@ -10939,7 +11057,7 @@ class AsyncWriter { socket[kWriting] = false; if (err) { - assert$4(client[kRunning$3] <= 1, 'pipeline should only contain this request'); + assert$5(client[kRunning$3] <= 1, 'pipeline should only contain this request'); util$e.destroy(socket, err); } } @@ -10948,7 +11066,7 @@ class AsyncWriter { function errorRequest (client, request, err) { try { request.onError(err); - assert$4(request.aborted); + assert$5(request.aborted); } catch (err) { client.emit('error', err); } @@ -11357,7 +11475,7 @@ let Pool$5 = class Pool extends PoolBase$1 { maxCachedSessions, allowH2, socketPath, - timeout: connectTimeout == null ? 10e3 : connectTimeout, + timeout: connectTimeout, ...(util$d.nodeHasAutoSelectFamily && autoSelectFamily ? { autoSelectFamily, autoSelectFamilyAttemptTimeout } : undefined), ...connect }); @@ -11778,7 +11896,9 @@ var agent = Agent$4; var api$1 = {}; -const assert$3 = require$$0$3; +var apiRequest = {exports: {}}; + +const assert$4 = require$$0$3; const { Readable: Readable$2 } = require$$0$4; const { RequestAbortedError: RequestAbortedError$7, NotSupportedError, InvalidArgumentError: InvalidArgumentError$c } = errors$1; const util$b = util$j; @@ -11792,6 +11912,8 @@ const kBody = Symbol('kBody'); const kAbort = Symbol('abort'); const kContentType = Symbol('kContentType'); +const noop = () => {}; + var readable = class BodyReadable extends Readable$2 { constructor ({ resume, @@ -11919,43 +12041,56 @@ var readable = class BodyReadable extends Readable$2 { if (this[kConsume]) { // TODO: Is this the best way to force a lock? this[kBody].getReader(); // Ensure stream is locked. - assert$3(this[kBody].locked); + assert$4(this[kBody].locked); } } return this[kBody] } - async dump (opts) { + dump (opts) { let limit = opts && Number.isFinite(opts.limit) ? opts.limit : 262144; const signal = opts && opts.signal; - const abortFn = () => { - this.destroy(); - }; - let signalListenerCleanup; + if (signal) { - if (typeof signal !== 'object' || !('aborted' in signal)) { - throw new InvalidArgumentError$c('signal must be an AbortSignal') - } - util$b.throwIfAborted(signal); - signalListenerCleanup = util$b.addAbortListener(signal, abortFn); - } - try { - for await (const chunk of this) { - util$b.throwIfAborted(signal); - limit -= Buffer.byteLength(chunk); - if (limit < 0) { - return + try { + if (typeof signal !== 'object' || !('aborted' in signal)) { + throw new InvalidArgumentError$c('signal must be an AbortSignal') } - } - } catch { - util$b.throwIfAborted(signal); - } finally { - if (typeof signalListenerCleanup === 'function') { - signalListenerCleanup(); - } else if (signalListenerCleanup) { - signalListenerCleanup[Symbol.dispose](); + util$b.throwIfAborted(signal); + } catch (err) { + return Promise.reject(err) } } + + if (this.closed) { + return Promise.resolve(null) + } + + return new Promise((resolve, reject) => { + const signalListenerCleanup = signal + ? util$b.addAbortListener(signal, () => { + this.destroy(); + }) + : noop; + + this + .on('close', function () { + signalListenerCleanup(); + if (signal && signal.aborted) { + reject(signal.reason || Object.assign(new Error('The operation was aborted'), { name: 'AbortError' })); + } else { + resolve(null); + } + }) + .on('error', noop) + .on('data', function (chunk) { + limit -= chunk.length; + if (limit <= 0) { + this.destroy(); + } + }) + .resume(); + }) } }; @@ -11975,7 +12110,7 @@ async function consume (stream, type) { throw new TypeError('unusable') } - assert$3(!stream[kConsume]); + assert$4(!stream[kConsume]); return new Promise((resolve, reject) => { stream[kConsume] = { @@ -12082,14 +12217,14 @@ function consumeFinish (consume, err) { consume.body = null; } -const assert$2 = require$$0$3; +const assert$3 = require$$0$3; const { ResponseStatusCodeError } = errors$1; const { toUSVString } = util$j; async function getResolveErrorBodyCallback$2 ({ callback, body, contentType, statusCode, statusMessage, headers }) { - assert$2(body); + assert$3(body); let chunks = []; let limit = 0; @@ -12360,7 +12495,10 @@ function request$2 (opts, callback) { } } -var apiRequest = request$2; +apiRequest.exports = request$2; +apiRequest.exports.RequestHandler = RequestHandler; + +var apiRequestExports = apiRequest.exports; const { finished, PassThrough: PassThrough$1 } = require$$0$4; const { @@ -12594,7 +12732,7 @@ const { const util$7 = util$j; const { AsyncResource: AsyncResource$2 } = require$$4; const { addSignal: addSignal$2, removeSignal: removeSignal$2 } = abortSignal; -const assert$1 = require$$0$3; +const assert$2 = require$$0$3; const kResume = Symbol('resume'); @@ -12728,7 +12866,7 @@ class PipelineHandler extends AsyncResource$2 { onConnect (abort, context) { const { ret, res } = this; - assert$1(!res, 'pipeline cannot be retried'); + assert$2(!res, 'pipeline cannot be retried'); if (ret.destroyed) { throw new RequestAbortedError$3() @@ -12833,7 +12971,7 @@ const { InvalidArgumentError: InvalidArgumentError$8, RequestAbortedError: Reque const { AsyncResource: AsyncResource$1 } = require$$4; const util$6 = util$j; const { addSignal: addSignal$1, removeSignal: removeSignal$1 } = abortSignal; -const assert = require$$0$3; +const assert$1 = require$$0$3; class UpgradeHandler extends AsyncResource$1 { constructor (opts, callback) { @@ -12878,7 +13016,7 @@ class UpgradeHandler extends AsyncResource$1 { onUpgrade (statusCode, rawHeaders, socket) { const { callback, opaque, context } = this; - assert.strictEqual(statusCode, 101); + assert$1.strictEqual(statusCode, 101); removeSignal$1(this); @@ -13036,7 +13174,7 @@ function connect (opts, callback) { var apiConnect = connect; -api$1.request = apiRequest; +api$1.request = apiRequestExports; api$1.stream = apiStream; api$1.pipeline = apiPipeline; api$1.upgrade = apiUpgrade; @@ -14055,6 +14193,9 @@ let ProxyAgent$1 = class ProxyAgent extends DispatcherBase { this[kProxyTls] = opts.proxyTls; this[kProxyHeaders] = opts.headers || {}; + const resolvedUrl = new URL$1(opts.uri); + const { origin, port, host, username, password } = resolvedUrl; + if (opts.auth && opts.token) { throw new InvalidArgumentError$2('opts.auth cannot be used in combination with opts.token') } else if (opts.auth) { @@ -14062,11 +14203,10 @@ let ProxyAgent$1 = class ProxyAgent extends DispatcherBase { this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}`; } else if (opts.token) { this[kProxyHeaders]['proxy-authorization'] = opts.token; + } else if (username && password) { + this[kProxyHeaders]['proxy-authorization'] = `Basic ${Buffer.from(`${decodeURIComponent(username)}:${decodeURIComponent(password)}`).toString('base64')}`; } - const resolvedUrl = new URL$1(opts.uri); - const { origin, port, host } = resolvedUrl; - const connect = buildConnector$1({ ...opts.proxyTls }); this[kConnectEndpoint] = buildConnector$1({ ...opts.requestTls }); this[kClient] = clientFactory(resolvedUrl, { connect }); @@ -14090,7 +14230,7 @@ let ProxyAgent$1 = class ProxyAgent extends DispatcherBase { }); if (statusCode !== 200) { socket.on('error', () => {}).destroy(); - callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')); + callback(new RequestAbortedError(`Proxy response (${statusCode}) !== 200 when HTTP Tunneling`)); } if (opts.protocol !== 'https:') { callback(null, socket); @@ -14176,6 +14316,343 @@ function throwIfProxyAuthIsSent (headers) { var proxyAgent = ProxyAgent$1; +const assert = require$$0$3; + +const { kRetryHandlerDefaultRetry } = symbols$4; +const { RequestRetryError } = errors$1; +const { isDisturbed, parseHeaders, parseRangeHeader } = util$j; + +function calculateRetryAfterHeader (retryAfter) { + const current = Date.now(); + const diff = new Date(retryAfter).getTime() - current; + + return diff +} + +let RetryHandler$1 = class RetryHandler { + constructor (opts, handlers) { + const { retryOptions, ...dispatchOpts } = opts; + const { + // Retry scoped + retry: retryFn, + maxRetries, + maxTimeout, + minTimeout, + timeoutFactor, + // Response scoped + methods, + errorCodes, + retryAfter, + statusCodes + } = retryOptions ?? {}; + + this.dispatch = handlers.dispatch; + this.handler = handlers.handler; + this.opts = dispatchOpts; + this.abort = null; + this.aborted = false; + this.retryOpts = { + retry: retryFn ?? RetryHandler[kRetryHandlerDefaultRetry], + retryAfter: retryAfter ?? true, + maxTimeout: maxTimeout ?? 30 * 1000, // 30s, + timeout: minTimeout ?? 500, // .5s + timeoutFactor: timeoutFactor ?? 2, + maxRetries: maxRetries ?? 5, + // What errors we should retry + methods: methods ?? ['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE', 'TRACE'], + // Indicates which errors to retry + statusCodes: statusCodes ?? [500, 502, 503, 504, 429], + // List of errors to retry + errorCodes: errorCodes ?? [ + 'ECONNRESET', + 'ECONNREFUSED', + 'ENOTFOUND', + 'ENETDOWN', + 'ENETUNREACH', + 'EHOSTDOWN', + 'EHOSTUNREACH', + 'EPIPE' + ] + }; + + this.retryCount = 0; + this.start = 0; + this.end = null; + this.etag = null; + this.resume = null; + + // Handle possible onConnect duplication + this.handler.onConnect(reason => { + this.aborted = true; + if (this.abort) { + this.abort(reason); + } else { + this.reason = reason; + } + }); + } + + onRequestSent () { + if (this.handler.onRequestSent) { + this.handler.onRequestSent(); + } + } + + onUpgrade (statusCode, headers, socket) { + if (this.handler.onUpgrade) { + this.handler.onUpgrade(statusCode, headers, socket); + } + } + + onConnect (abort) { + if (this.aborted) { + abort(this.reason); + } else { + this.abort = abort; + } + } + + onBodySent (chunk) { + if (this.handler.onBodySent) return this.handler.onBodySent(chunk) + } + + static [kRetryHandlerDefaultRetry] (err, { state, opts }, cb) { + const { statusCode, code, headers } = err; + const { method, retryOptions } = opts; + const { + maxRetries, + timeout, + maxTimeout, + timeoutFactor, + statusCodes, + errorCodes, + methods + } = retryOptions; + let { counter, currentTimeout } = state; + + currentTimeout = + currentTimeout != null && currentTimeout > 0 ? currentTimeout : timeout; + + // Any code that is not a Undici's originated and allowed to retry + if ( + code && + code !== 'UND_ERR_REQ_RETRY' && + code !== 'UND_ERR_SOCKET' && + !errorCodes.includes(code) + ) { + cb(err); + return + } + + // If a set of method are provided and the current method is not in the list + if (Array.isArray(methods) && !methods.includes(method)) { + cb(err); + return + } + + // If a set of status code are provided and the current status code is not in the list + if ( + statusCode != null && + Array.isArray(statusCodes) && + !statusCodes.includes(statusCode) + ) { + cb(err); + return + } + + // If we reached the max number of retries + if (counter > maxRetries) { + cb(err); + return + } + + let retryAfterHeader = headers != null && headers['retry-after']; + if (retryAfterHeader) { + retryAfterHeader = Number(retryAfterHeader); + retryAfterHeader = isNaN(retryAfterHeader) + ? calculateRetryAfterHeader(retryAfterHeader) + : retryAfterHeader * 1e3; // Retry-After is in seconds + } + + const retryTimeout = + retryAfterHeader > 0 + ? Math.min(retryAfterHeader, maxTimeout) + : Math.min(currentTimeout * timeoutFactor ** counter, maxTimeout); + + state.currentTimeout = retryTimeout; + + setTimeout(() => cb(null), retryTimeout); + } + + onHeaders (statusCode, rawHeaders, resume, statusMessage) { + const headers = parseHeaders(rawHeaders); + + this.retryCount += 1; + + if (statusCode >= 300) { + this.abort( + new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }) + ); + return false + } + + // Checkpoint for resume from where we left it + if (this.resume != null) { + this.resume = null; + + if (statusCode !== 206) { + return true + } + + const contentRange = parseRangeHeader(headers['content-range']); + // If no content range + if (!contentRange) { + this.abort( + new RequestRetryError('Content-Range mismatch', statusCode, { + headers, + count: this.retryCount + }) + ); + return false + } + + // Let's start with a weak etag check + if (this.etag != null && this.etag !== headers.etag) { + this.abort( + new RequestRetryError('ETag mismatch', statusCode, { + headers, + count: this.retryCount + }) + ); + return false + } + + const { start, size, end = size } = contentRange; + + assert(this.start === start, 'content-range mismatch'); + assert(this.end == null || this.end === end, 'content-range mismatch'); + + this.resume = resume; + return true + } + + if (this.end == null) { + if (statusCode === 206) { + // First time we receive 206 + const range = parseRangeHeader(headers['content-range']); + + if (range == null) { + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const { start, size, end = size } = range; + + assert( + start != null && Number.isFinite(start) && this.start !== start, + 'content-range mismatch' + ); + assert(Number.isFinite(start)); + assert( + end != null && Number.isFinite(end) && this.end !== end, + 'invalid content-length' + ); + + this.start = start; + this.end = end; + } + + // We make our best to checkpoint the body for further range headers + if (this.end == null) { + const contentLength = headers['content-length']; + this.end = contentLength != null ? Number(contentLength) : null; + } + + assert(Number.isFinite(this.start)); + assert( + this.end == null || Number.isFinite(this.end), + 'invalid content-length' + ); + + this.resume = resume; + this.etag = headers.etag != null ? headers.etag : null; + + return this.handler.onHeaders( + statusCode, + rawHeaders, + resume, + statusMessage + ) + } + + const err = new RequestRetryError('Request failed', statusCode, { + headers, + count: this.retryCount + }); + + this.abort(err); + + return false + } + + onData (chunk) { + this.start += chunk.length; + + return this.handler.onData(chunk) + } + + onComplete (rawTrailers) { + this.retryCount = 0; + return this.handler.onComplete(rawTrailers) + } + + onError (err) { + if (this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + this.retryOpts.retry( + err, + { + state: { counter: this.retryCount++, currentTimeout: this.retryAfter }, + opts: { retryOptions: this.retryOpts, ...this.opts } + }, + onRetry.bind(this) + ); + + function onRetry (err) { + if (err != null || this.aborted || isDisturbed(this.opts.body)) { + return this.handler.onError(err) + } + + if (this.start !== 0) { + this.opts = { + ...this.opts, + headers: { + ...this.opts.headers, + range: `bytes=${this.start}-${this.end ?? ''}` + } + }; + } + + try { + this.dispatch(this.opts, this); + } catch (err) { + this.handler.onError(err); + } + } + } +}; + +var RetryHandler_1 = RetryHandler$1; + // We include a version number for the Dispatcher API. In case of breaking changes, // this version number must be increased to avoid conflicts. const globalDispatcher = Symbol.for('undici.globalDispatcher.1'); @@ -14248,7 +14725,7 @@ function requireHeaders () { if (hasRequiredHeaders) return headers; hasRequiredHeaders = 1; - const { kHeadersList } = symbols$4; + const { kHeadersList, kConstruct } = symbols$4; const { kGuard } = requireSymbols$3(); const { kEnumerableProperty } = util$j; const { @@ -14262,6 +14739,13 @@ function requireHeaders () { const kHeadersMap = Symbol('headers map'); const kHeadersSortedMap = Symbol('headers map sorted'); + /** + * @param {number} code + */ + function isHTTPWhiteSpaceCharCode (code) { + return code === 0x00a || code === 0x00d || code === 0x009 || code === 0x020 + } + /** * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize * @param {string} potentialValue @@ -14270,12 +14754,12 @@ function requireHeaders () { // To normalize a byte sequence potentialValue, remove // any leading and trailing HTTP whitespace bytes from // potentialValue. + let i = 0; let j = potentialValue.length; - // Trimming the end with `.replace()` and a RegExp is typically subject to - // ReDoS. This is safer and faster. - let i = potentialValue.length; - while (/[\r\n\t ]/.test(potentialValue.charAt(--i))); - return potentialValue.slice(0, i + 1).replace(/^[\r\n\t ]+/, '') + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j; + while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i; + + return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j) } function fill (headers, object) { @@ -14284,7 +14768,8 @@ function requireHeaders () { // 1. If object is a sequence, then for each header in object: // Note: webidl conversion to array has already been done. if (Array.isArray(object)) { - for (const header of object) { + for (let i = 0; i < object.length; ++i) { + const header = object[i]; // 1. If header does not contain exactly two items, then throw a TypeError. if (header.length !== 2) { throw webidl.errors.exception({ @@ -14294,15 +14779,16 @@ function requireHeaders () { } // 2. Append (header’s first item, header’s second item) to headers. - headers.append(header[0], header[1]); + appendHeader(headers, header[0], header[1]); } } else if (typeof object === 'object' && object !== null) { // Note: null should throw // 2. Otherwise, object is a record, then for each key → value in object, // append (key, value) to headers - for (const [key, value] of Object.entries(object)) { - headers.append(key, value); + const keys = Object.keys(object); + for (let i = 0; i < keys.length; ++i) { + appendHeader(headers, keys[i], object[keys[i]]); } } else { throw webidl.errors.conversionFailed({ @@ -14313,6 +14799,47 @@ function requireHeaders () { } } + /** + * @see https://fetch.spec.whatwg.org/#concept-headers-append + */ + function appendHeader (headers, name, value) { + // 1. Normalize value. + value = headerValueNormalize(value); + + // 2. If name is not a header name or value is not a + // header value, then throw a TypeError. + if (!isValidHeaderName(name)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + throw webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value, + type: 'header value' + }) + } + + // 3. If headers’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if headers’s guard is "request" and name is a + // forbidden header name, return. + // Note: undici does not implement forbidden header names + if (headers[kGuard] === 'immutable') { + throw new TypeError('immutable') + } else if (headers[kGuard] === 'request-no-cors') ; + + // 6. Otherwise, if headers’s guard is "response" and name is a + // forbidden response-header name, return. + + // 7. Append (name, value) to headers’s header list. + return headers[kHeadersList].append(name, value) + + // 8. If headers’s guard is "request-no-cors", then remove + // privileged no-CORS request headers from headers + } + class HeadersList { /** @type {[string, string][]|null} */ cookies = null @@ -14321,7 +14848,7 @@ function requireHeaders () { if (init instanceof HeadersList) { this[kHeadersMap] = new Map(init[kHeadersMap]); this[kHeadersSortedMap] = init[kHeadersSortedMap]; - this.cookies = init.cookies; + this.cookies = init.cookies === null ? null : [...init.cookies]; } else { this[kHeadersMap] = new Map(init); this[kHeadersSortedMap] = null; @@ -14383,7 +14910,7 @@ function requireHeaders () { // the first such header to value and remove the // others. // 2. Otherwise, append header (name, value) to list. - return this[kHeadersMap].set(lowercaseName, { name, value }) + this[kHeadersMap].set(lowercaseName, { name, value }); } // https://fetch.spec.whatwg.org/#concept-header-list-delete @@ -14396,20 +14923,18 @@ function requireHeaders () { this.cookies = null; } - return this[kHeadersMap].delete(name) + this[kHeadersMap].delete(name); } // https://fetch.spec.whatwg.org/#concept-header-list-get get (name) { - // 1. If list does not contain name, then return null. - if (!this.contains(name)) { - return null - } + const value = this[kHeadersMap].get(name.toLowerCase()); + // 1. If list does not contain name, then return null. // 2. Return the values of all headers in list whose name // is a byte-case-insensitive match for name, // separated from each other by 0x2C 0x20, in order. - return this[kHeadersMap].get(name.toLowerCase())?.value ?? null + return value === undefined ? null : value.value } * [Symbol.iterator] () { @@ -14435,6 +14960,9 @@ function requireHeaders () { // https://fetch.spec.whatwg.org/#headers-class class Headers { constructor (init = undefined) { + if (init === kConstruct) { + return + } this[kHeadersList] = new HeadersList(); // The new Headers(init) constructor steps are: @@ -14458,40 +14986,7 @@ function requireHeaders () { name = webidl.converters.ByteString(name); value = webidl.converters.ByteString(value); - // 1. Normalize value. - value = headerValueNormalize(value); - - // 2. If name is not a header name or value is not a - // header value, then throw a TypeError. - if (!isValidHeaderName(name)) { - throw webidl.errors.invalidArgument({ - prefix: 'Headers.append', - value: name, - type: 'header name' - }) - } else if (!isValidHeaderValue(value)) { - throw webidl.errors.invalidArgument({ - prefix: 'Headers.append', - value, - type: 'header value' - }) - } - - // 3. If headers’s guard is "immutable", then throw a TypeError. - // 4. Otherwise, if headers’s guard is "request" and name is a - // forbidden header name, return. - // Note: undici does not implement forbidden header names - if (this[kGuard] === 'immutable') { - throw new TypeError('immutable') - } else if (this[kGuard] === 'request-no-cors') ; - - // 6. Otherwise, if headers’s guard is "response" and name is a - // forbidden response-header name, return. - - // 7. Append (name, value) to headers’s header list. - // 8. If headers’s guard is "request-no-cors", then remove - // privileged no-CORS request headers from headers - return this[kHeadersList].append(name, value) + return appendHeader(this, name, value) } // https://fetch.spec.whatwg.org/#dom-headers-delete @@ -14534,7 +15029,7 @@ function requireHeaders () { // 7. Delete name from this’s header list. // 8. If this’s guard is "request-no-cors", then remove // privileged no-CORS request headers from this. - return this[kHeadersList].delete(name) + this[kHeadersList].delete(name); } // https://fetch.spec.whatwg.org/#dom-headers-get @@ -14625,7 +15120,7 @@ function requireHeaders () { // 7. Set (name, value) in this’s header list. // 8. If this’s guard is "request-no-cors", then remove // privileged no-CORS request headers from this - return this[kHeadersList].set(name, value) + this[kHeadersList].set(name, value); } // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie @@ -14661,7 +15156,8 @@ function requireHeaders () { const cookies = this[kHeadersList].cookies; // 3. For each name of names: - for (const [name, value] of names) { + for (let i = 0; i < names.length; ++i) { + const [name, value] = names[i]; // 1. If name is `set-cookie`, then: if (name === 'set-cookie') { // 1. Let values be a list of all values of headers in list whose name @@ -14669,8 +15165,8 @@ function requireHeaders () { // 2. For each value of values: // 1. Append (name, value) to headers. - for (const value of cookies) { - headers.push([name, value]); + for (let j = 0; j < cookies.length; ++j) { + headers.push([name, cookies[j]]); } } else { // 2. Otherwise: @@ -14694,6 +15190,12 @@ function requireHeaders () { keys () { webidl.brandCheck(this, Headers); + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap]; + return makeIterator(() => value, 'Headers', + 'key') + } + return makeIterator( () => [...this[kHeadersSortedMap].values()], 'Headers', @@ -14704,6 +15206,12 @@ function requireHeaders () { values () { webidl.brandCheck(this, Headers); + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap]; + return makeIterator(() => value, 'Headers', + 'value') + } + return makeIterator( () => [...this[kHeadersSortedMap].values()], 'Headers', @@ -14714,6 +15222,12 @@ function requireHeaders () { entries () { webidl.brandCheck(this, Headers); + if (this[kGuard] === 'immutable') { + const value = this[kHeadersSortedMap]; + return makeIterator(() => value, 'Headers', + 'key+value') + } + return makeIterator( () => [...this[kHeadersSortedMap].values()], 'Headers', @@ -14813,7 +15327,7 @@ function requireResponse () { isomorphicEncode } = requireUtil$4(); const { - redirectStatus, + redirectStatusSet, nullBodyStatus, DOMException } = requireConstants$3(); @@ -14822,11 +15336,12 @@ function requireResponse () { const { FormData } = requireFormdata(); const { getGlobalOrigin } = requireGlobal(); const { URLSerializer } = requireDataURL(); - const { kHeadersList } = symbols$4; + const { kHeadersList, kConstruct } = symbols$4; const assert = require$$0$3; const { types } = require$$1; const ReadableStream = globalThis.ReadableStream || require$$13.ReadableStream; + const textEncoder = new TextEncoder('utf-8'); // https://fetch.spec.whatwg.org/#response-class class Response { @@ -14856,7 +15371,7 @@ function requireResponse () { } // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. - const bytes = new TextEncoder('utf-8').encode( + const bytes = textEncoder.encode( serializeJavascriptValueToJSONString(data) ); @@ -14901,7 +15416,7 @@ function requireResponse () { } // 3. If status is not a redirect status, then throw a RangeError. - if (!redirectStatus.includes(status)) { + if (!redirectStatusSet.has(status)) { throw new RangeError('Invalid status code ' + status) } @@ -14942,7 +15457,7 @@ function requireResponse () { // 2. Set this’s headers to a new Headers object with this’s relevant // Realm, whose header list is this’s response’s header list and guard // is "response". - this[kHeaders] = new Headers(); + this[kHeaders] = new Headers(kConstruct); this[kHeaders][kGuard] = 'response'; this[kHeaders][kHeadersList] = this[kState].headersList; this[kHeaders][kRealm] = this[kRealm]; @@ -15312,11 +15827,7 @@ function requireResponse () { return webidl.converters.Blob(V, { strict: false }) } - if ( - types.isAnyArrayBuffer(V) || - types.isTypedArray(V) || - types.isDataView(V) - ) { + if (types.isArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) { return webidl.converters.BufferSource(V) } @@ -15391,11 +15902,12 @@ function requireRequest () { isValidHTTPToken, sameOrigin, normalizeMethod, - makePolicyContainer + makePolicyContainer, + normalizeMethodRecord } = requireUtil$4(); const { - forbiddenMethods, - corsSafeListedMethods, + forbiddenMethodsSet, + corsSafeListedMethodsSet, referrerPolicy, requestRedirect, requestMode, @@ -15408,13 +15920,12 @@ function requireRequest () { const { webidl } = requireWebidl(); const { getGlobalOrigin } = requireGlobal(); const { URLSerializer } = requireDataURL(); - const { kHeadersList } = symbols$4; + const { kHeadersList, kConstruct } = symbols$4; const assert = require$$0$3; const { getMaxListeners, setMaxListeners, getEventListeners, defaultMaxListeners } = require$$0$2; let TransformStream = globalThis.TransformStream; - const kInit = Symbol('init'); const kAbortController = Symbol('abortController'); const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { @@ -15425,7 +15936,7 @@ function requireRequest () { class Request { // https://fetch.spec.whatwg.org/#dom-request constructor (input, init = {}) { - if (input === kInit) { + if (input === kConstruct) { return } @@ -15564,8 +16075,10 @@ function requireRequest () { urlList: [...request.urlList] }); + const initHasKey = Object.keys(init).length !== 0; + // 13. If init is not empty, then: - if (Object.keys(init).length > 0) { + if (initHasKey) { // 1. If request’s mode is "navigate", then set it to "same-origin". if (request.mode === 'navigate') { request.mode = 'same-origin'; @@ -15680,7 +16193,7 @@ function requireRequest () { } // 23. If init["integrity"] exists, then set request’s integrity metadata to it. - if (init.integrity !== undefined && init.integrity != null) { + if (init.integrity != null) { request.integrity = String(init.integrity); } @@ -15696,16 +16209,16 @@ function requireRequest () { // 2. If method is not a method or method is a forbidden method, then // throw a TypeError. - if (!isValidHTTPToken(init.method)) { - throw TypeError(`'${init.method}' is not a valid HTTP method.`) + if (!isValidHTTPToken(method)) { + throw new TypeError(`'${method}' is not a valid HTTP method.`) } - if (forbiddenMethods.indexOf(method.toUpperCase()) !== -1) { - throw TypeError(`'${init.method}' HTTP method is unsupported.`) + if (forbiddenMethodsSet.has(method.toUpperCase())) { + throw new TypeError(`'${method}' HTTP method is unsupported.`) } // 3. Normalize method. - method = normalizeMethod(init.method); + method = normalizeMethodRecord[method] ?? normalizeMethod(method); // 4. Set request’s method to method. request.method = method; @@ -15776,7 +16289,7 @@ function requireRequest () { // 30. Set this’s headers to a new Headers object with this’s relevant // Realm, whose header list is request’s header list and guard is // "request". - this[kHeaders] = new Headers(); + this[kHeaders] = new Headers(kConstruct); this[kHeaders][kHeadersList] = request.headersList; this[kHeaders][kGuard] = 'request'; this[kHeaders][kRealm] = this[kRealm]; @@ -15785,7 +16298,7 @@ function requireRequest () { if (mode === 'no-cors') { // 1. If this’s request’s method is not a CORS-safelisted method, // then throw a TypeError. - if (!corsSafeListedMethods.includes(request.method)) { + if (!corsSafeListedMethodsSet.has(request.method)) { throw new TypeError( `'${request.method} is unsupported in no-cors mode.` ) @@ -15796,25 +16309,25 @@ function requireRequest () { } // 32. If init is not empty, then: - if (Object.keys(init).length !== 0) { + if (initHasKey) { + /** @type {HeadersList} */ + const headersList = this[kHeaders][kHeadersList]; // 1. Let headers be a copy of this’s headers and its associated header // list. - let headers = new Headers(this[kHeaders]); - // 2. If init["headers"] exists, then set headers to init["headers"]. - if (init.headers !== undefined) { - headers = init.headers; - } + const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList); // 3. Empty this’s headers’s header list. - this[kHeaders][kHeadersList].clear(); + headersList.clear(); // 4. If headers is a Headers object, then for each header in its header // list, append header’s name/header’s value to this’s headers. - if (headers.constructor.name === 'Headers') { + if (headers instanceof HeadersList) { for (const [key, val] of headers) { - this[kHeaders].append(key, val); + headersList.append(key, val); } + // Note: Copy the `set-cookie` meta-data. + headersList.cookies = headers.cookies; } else { // 5. Otherwise, fill this’s headers with headers. fillHeaders(this[kHeaders], headers); @@ -16103,10 +16616,10 @@ function requireRequest () { // 3. Let clonedRequestObject be the result of creating a Request object, // given clonedRequest, this’s headers’s guard, and this’s relevant Realm. - const clonedRequestObject = new Request(kInit); + const clonedRequestObject = new Request(kConstruct); clonedRequestObject[kState] = clonedRequest; clonedRequestObject[kRealm] = this[kRealm]; - clonedRequestObject[kHeaders] = new Headers(); + clonedRequestObject[kHeaders] = new Headers(kConstruct); clonedRequestObject[kHeaders][kHeadersList] = clonedRequest.headersList; clonedRequestObject[kHeaders][kGuard] = this[kHeaders][kGuard]; clonedRequestObject[kHeaders][kRealm] = this[kHeaders][kRealm]; @@ -16377,11 +16890,11 @@ function requireFetch () { const assert = require$$0$3; const { safelyExtractBody } = requireBody(); const { - redirectStatus, + redirectStatusSet, nullBodyStatus, - safeMethods, + safeMethodsSet, requestBodyHeader, - subresource, + subresourceSet, DOMException } = requireConstants$3(); const { kHeadersList } = symbols$4; @@ -16393,6 +16906,7 @@ function requireFetch () { const { getGlobalDispatcher } = global$1; const { webidl } = requireWebidl(); const { STATUS_CODES } = require$$2$1; + const GET_OR_HEAD = ['GET', 'HEAD']; /** @type {import('buffer').resolveObjectURL} */ let resolveObjectURL; @@ -16452,7 +16966,7 @@ function requireFetch () { } // https://fetch.spec.whatwg.org/#fetch-method - async function fetch (input, init = {}) { + function fetch (input, init = {}) { webidl.argumentLengthCheck(arguments, 1, { header: 'globalThis.fetch' }); // 1. Let p be a new promise. @@ -16535,7 +17049,7 @@ function requireFetch () { const processResponse = (response) => { // 1. If locallyAborted is true, terminate these substeps. if (locallyAborted) { - return + return Promise.resolve() } // 2. If response’s aborted flag is set, then: @@ -16548,7 +17062,7 @@ function requireFetch () { // deserializedError. abortFetch(p, request, responseObject, controller.serializedAbortReason); - return + return Promise.resolve() } // 3. If response is a network error, then reject p with a TypeError @@ -16557,7 +17071,7 @@ function requireFetch () { p.reject( Object.assign(new TypeError('fetch failed'), { cause: response.error }) ); - return + return Promise.resolve() } // 4. Set responseObject to the result of creating a Response object, @@ -16616,7 +17130,7 @@ function requireFetch () { } // 8. If response’s timing allow passed flag is not set, then: - if (!timingInfo.timingAllowPassed) { + if (!response.timingAllowPassed) { // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo. timingInfo = createOpaqueTimingInfo({ startTime: timingInfo.startTime @@ -16838,7 +17352,7 @@ function requireFetch () { if (request.priority === null) ; // 15. If request is a subresource request, then: - if (subresource.includes(request.destination)) ; + if (subresourceSet.has(request.destination)) ; // 16. Run main fetch given fetchParams. mainFetch(fetchParams) @@ -17092,13 +17606,13 @@ function requireFetch () { // https://fetch.spec.whatwg.org/#concept-scheme-fetch // given a fetch params fetchParams - async function schemeFetch (fetchParams) { + function schemeFetch (fetchParams) { // Note: since the connection is destroyed on redirect, which sets fetchParams to a // cancelled state, we do not want this condition to trigger *unless* there have been // no redirects. See https://github.com/nodejs/undici/issues/1776 // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams. if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) { - return makeAppropriateNetworkError(fetchParams) + return Promise.resolve(makeAppropriateNetworkError(fetchParams)) } // 2. Let request be fetchParams’s request. @@ -17114,7 +17628,7 @@ function requireFetch () { // and body is the empty byte sequence as a body. // Otherwise, return a network error. - return makeNetworkError('about scheme is not supported') + return Promise.resolve(makeNetworkError('about scheme is not supported')) } case 'blob:': { if (!resolveObjectURL) { @@ -17127,7 +17641,7 @@ function requireFetch () { // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56 // Buffer.resolveObjectURL does not ignore URL queries. if (blobURLEntry.search.length !== 0) { - return makeNetworkError('NetworkError when attempting to fetch resource.') + return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.')) } const blobURLEntryObject = resolveObjectURL(blobURLEntry.toString()); @@ -17135,7 +17649,7 @@ function requireFetch () { // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s // object is not a Blob object, then return a network error. if (request.method !== 'GET' || !isBlobLike(blobURLEntryObject)) { - return makeNetworkError('invalid method') + return Promise.resolve(makeNetworkError('invalid method')) } // 3. Let bodyWithType be the result of safely extracting blobURLEntry’s object. @@ -17162,7 +17676,7 @@ function requireFetch () { response.body = body; - return response + return Promise.resolve(response) } case 'data:': { // 1. Let dataURLStruct be the result of running the @@ -17173,7 +17687,7 @@ function requireFetch () { // 2. If dataURLStruct is failure, then return a // network error. if (dataURLStruct === 'failure') { - return makeNetworkError('failed to fetch the data URL') + return Promise.resolve(makeNetworkError('failed to fetch the data URL')) } // 3. Let mimeType be dataURLStruct’s MIME type, serialized. @@ -17182,28 +17696,28 @@ function requireFetch () { // 4. Return a response whose status message is `OK`, // header list is « (`Content-Type`, mimeType) », // and body is dataURLStruct’s body as a body. - return makeResponse({ + return Promise.resolve(makeResponse({ statusText: 'OK', headersList: [ ['content-type', { name: 'Content-Type', value: mimeType }] ], body: safelyExtractBody(dataURLStruct.body)[0] - }) + })) } case 'file:': { // For now, unfortunate as it is, file URLs are left as an exercise for the reader. // When in doubt, return a network error. - return makeNetworkError('not implemented... yet...') + return Promise.resolve(makeNetworkError('not implemented... yet...')) } case 'http:': case 'https:': { // Return the result of running HTTP fetch given fetchParams. - return await httpFetch(fetchParams) + return httpFetch(fetchParams) .catch((err) => makeNetworkError(err)) } default: { - return makeNetworkError('unknown scheme') + return Promise.resolve(makeNetworkError('unknown scheme')) } } } @@ -17222,7 +17736,7 @@ function requireFetch () { } // https://fetch.spec.whatwg.org/#fetch-finale - async function fetchFinale (fetchParams, response) { + function fetchFinale (fetchParams, response) { // 1. If response is a network error, then: if (response.type === 'error') { // 1. Set response’s URL list to « fetchParams’s request’s URL list[0] ». @@ -17306,8 +17820,9 @@ function requireFetch () { } else { // 4. Otherwise, fully read response’s body given processBody, processBodyError, // and fetchParams’s task destination. - await fullyReadBody(response.body, processBody, processBodyError); + return fullyReadBody(response.body, processBody, processBodyError) } + return Promise.resolve() } } @@ -17376,7 +17891,7 @@ function requireFetch () { } // 8. If actualResponse’s status is a redirect status, then: - if (redirectStatus.includes(actualResponse.status)) { + if (redirectStatusSet.has(actualResponse.status)) { // 1. If actualResponse’s status is not 303, request’s body is not null, // and the connection uses HTTP/2, then user agents may, and are even // encouraged to, transmit an RST_STREAM frame. @@ -17413,7 +17928,7 @@ function requireFetch () { } // https://fetch.spec.whatwg.org/#http-redirect-fetch - async function httpRedirectFetch (fetchParams, response) { + function httpRedirectFetch (fetchParams, response) { // 1. Let request be fetchParams’s request. const request = fetchParams.request; @@ -17439,18 +17954,18 @@ function requireFetch () { } } catch (err) { // 5. If locationURL is failure, then return a network error. - return makeNetworkError(err) + return Promise.resolve(makeNetworkError(err)) } // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network // error. if (!urlIsHttpHttpsScheme(locationURL)) { - return makeNetworkError('URL scheme must be a HTTP(S) scheme') + return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme')) } // 7. If request’s redirect count is 20, then return a network error. if (request.redirectCount === 20) { - return makeNetworkError('redirect count exceeded') + return Promise.resolve(makeNetworkError('redirect count exceeded')) } // 8. Increase request’s redirect count by 1. @@ -17464,7 +17979,7 @@ function requireFetch () { (locationURL.username || locationURL.password) && !sameOrigin(request, locationURL) ) { - return makeNetworkError('cross origin not allowed for request mode "cors"') + return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"')) } // 10. If request’s response tainting is "cors" and locationURL includes @@ -17473,9 +17988,9 @@ function requireFetch () { request.responseTainting === 'cors' && (locationURL.username || locationURL.password) ) { - return makeNetworkError( + return Promise.resolve(makeNetworkError( 'URL cannot contain credentials for request mode "cors"' - ) + )) } // 11. If actualResponse’s status is not 303, request’s body is non-null, @@ -17485,7 +18000,7 @@ function requireFetch () { request.body != null && request.body.source == null ) { - return makeNetworkError() + return Promise.resolve(makeNetworkError()) } // 12. If one of the following is true @@ -17494,7 +18009,7 @@ function requireFetch () { if ( ([301, 302].includes(actualResponse.status) && request.method === 'POST') || (actualResponse.status === 303 && - !['GET', 'HEAD'].includes(request.method)) + !GET_OR_HEAD.includes(request.method)) ) { // then: // 1. Set request’s method to `GET` and request’s body to null. @@ -17515,6 +18030,9 @@ function requireFetch () { // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name request.headersList.delete('authorization'); + // https://fetch.spec.whatwg.org/#authentication-entries + request.headersList.delete('proxy-authorization', true); + // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement. request.headersList.delete('cookie'); request.headersList.delete('host'); @@ -17756,7 +18274,7 @@ function requireFetch () { // responses in httpCache, as per the "Invalidation" chapter of HTTP // Caching, and set storedResponse to null. [HTTP-CACHING] if ( - !safeMethods.includes(httpRequest.method) && + !safeMethodsSet.has(httpRequest.method) && forwardResponse.status >= 200 && forwardResponse.status <= 399 ) ; @@ -18212,7 +18730,7 @@ function requireFetch () { path: url.pathname + url.search, origin: url.origin, method: request.method, - body: fetchParams.controller.dispatcher.isMockActive ? request.body && request.body.source : body, + body: fetchParams.controller.dispatcher.isMockActive ? request.body && (request.body.source || request.body.stream) : body, headers: request.headersList.entries, maxRedirections: 0, upgrade: request.mode === 'websocket' ? 'websocket' : undefined @@ -18257,7 +18775,7 @@ function requireFetch () { location = val; } - headers.append(key, val); + headers[kHeadersList].append(key, val); } } else { const keys = Object.keys(headersList); @@ -18271,7 +18789,7 @@ function requireFetch () { location = val; } - headers.append(key, val); + headers[kHeadersList].append(key, val); } } @@ -18281,7 +18799,7 @@ function requireFetch () { const willFollow = request.redirect === 'follow' && location && - redirectStatus.includes(status); + redirectStatusSet.has(status); // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) { @@ -18375,7 +18893,7 @@ function requireFetch () { const key = headersList[n + 0].toString('latin1'); const val = headersList[n + 1].toString('latin1'); - headers.append(key, val); + headers[kHeadersList].append(key, val); } resolve({ @@ -19563,7 +20081,7 @@ function requireSymbols$1 () { hasRequiredSymbols$1 = 1; symbols$1 = { - kConstruct: Symbol('constructable') + kConstruct: symbols$4.kConstruct }; return symbols$1; } @@ -23450,6 +23968,7 @@ const MockAgent = mockAgent; const MockPool = mockPool; const mockErrors = mockErrors$1; const ProxyAgent = proxyAgent; +const RetryHandler = RetryHandler_1; const { getGlobalDispatcher, setGlobalDispatcher } = global$1; const DecoratorHandler = DecoratorHandler_1; const RedirectHandler = RedirectHandler_1; @@ -23471,6 +23990,7 @@ undici.Pool = Pool; undici.BalancedPool = BalancedPool; undici.Agent = Agent; undici.ProxyAgent = ProxyAgent; +undici.RetryHandler = RetryHandler; undici.DecoratorHandler = DecoratorHandler; undici.RedirectHandler = RedirectHandler;