From cb402effa92270c39eebfd8482225b92410f9d3b Mon Sep 17 00:00:00 2001 From: Rich Trott Date: Sat, 9 Jul 2022 17:46:02 -0700 Subject: [PATCH] deps: update undici to 5.6.1 --- deps/undici/src/README.md | 7 + deps/undici/src/docs/api/Dispatcher.md | 10 +- deps/undici/src/lib/api/api-connect.js | 22 +- deps/undici/src/lib/api/api-request.js | 34 +- deps/undici/src/lib/client.js | 8 +- deps/undici/src/lib/core/errors.js | 12 +- deps/undici/src/lib/fetch/body.js | 154 ++- deps/undici/src/lib/fetch/constants.js | 12 + deps/undici/src/lib/fetch/file.js | 142 +- deps/undici/src/lib/fetch/formdata.js | 156 ++- deps/undici/src/lib/fetch/headers.js | 306 +++-- deps/undici/src/lib/fetch/index.js | 55 +- deps/undici/src/lib/fetch/request.js | 136 +- deps/undici/src/lib/fetch/response.js | 146 ++- deps/undici/src/lib/fetch/util.js | 47 +- deps/undici/src/lib/fetch/webidl.js | 594 +++++++++ deps/undici/src/lib/handler/redirect.js | 2 +- deps/undici/src/lib/proxy-agent.js | 100 +- deps/undici/src/package.json | 6 +- .../undici/src/types/diagnostics-channel.d.ts | 1 + deps/undici/src/types/errors.d.ts | 11 + deps/undici/src/types/file.d.ts | 11 +- deps/undici/undici.js | 1144 +++++++++++++---- 23 files changed, 2485 insertions(+), 631 deletions(-) create mode 100644 deps/undici/src/lib/fetch/webidl.js diff --git a/deps/undici/src/README.md b/deps/undici/src/README.md index c2639bb7633d89..432eb265932aa9 100644 --- a/deps/undici/src/README.md +++ b/deps/undici/src/README.md @@ -283,6 +283,13 @@ const headers = await fetch(url) .then(res => res.headers) ``` +However, if you want to get only headers, it might be better to use `HEAD` request method. Usage of this method will obviate the need for consumption or cancelling of the response body. See [MDN - HTTP - HTTP request methods - HEAD](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD) for more details. + +```js +const headers = await fetch(url, { method: 'HEAD' }) + .then(res => res.headers) +``` + ##### Forbidden and Safelisted Header Names * https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name diff --git a/deps/undici/src/docs/api/Dispatcher.md b/deps/undici/src/docs/api/Dispatcher.md index ffe72cceb1b7b1..32ccb57993f3ec 100644 --- a/deps/undici/src/docs/api/Dispatcher.md +++ b/deps/undici/src/docs/api/Dispatcher.md @@ -461,14 +461,14 @@ Arguments: * **options** `RequestOptions` * **callback** `(error: Error | null, data: ResponseData) => void` (optional) -Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed +Returns: `void | Promise` - Only returns a `Promise` if no `callback` argument was passed. #### Parameter: `RequestOptions` Extends: [`DispatchOptions`](#parameter-dispatchoptions) -* **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData` -* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null` +* **opaque** `unknown` (optional) - Default: `null` - Used for passing through context to `ResponseData`. +* **signal** `AbortSignal | events.EventEmitter | null` (optional) - Default: `null`. * **onInfo** `({statusCode: number, headers: Record}) => void | null` (optional) - Default: `null` - Callback collecting all the info headers (HTTP 100-199) received. The `RequestOptions.method` property should not be value `'CONNECT'`. @@ -476,7 +476,7 @@ The `RequestOptions.method` property should not be value `'CONNECT'`. #### Parameter: `ResponseData` * **statusCode** `number` -* **headers** `http.IncomingHttpHeaders` +* **headers** `http.IncomingHttpHeaders` - Note that all header keys are lower-cased, e. g. `content-type`. * **body** `stream.Readable` which also implements [the body mixin from the Fetch Standard](https://fetch.spec.whatwg.org/#body-mixin). * **trailers** `Record` - This object starts out as empty and will be mutated to contain trailers after `body` has emitted `'end'`. @@ -497,6 +497,8 @@ The `RequestOptions.method` property should not be value `'CONNECT'`. - `dump({ limit: Integer })`, dump the response by reading up to `limit` bytes without killing the socket (optional) - Default: 262144. +Note that body will still be a `Readable` even if it is empty, but attempting to deserialize it with `json()` will result in an exception. Recommended way to ensure there is a body to deserialize is to check if status code is not 204, and `content-type` header starts with `application/json`. + #### Example 1 - Basic GET Request ```js diff --git a/deps/undici/src/lib/api/api-connect.js b/deps/undici/src/lib/api/api-connect.js index e4981af00de1b7..0503b1a2f0eb10 100644 --- a/deps/undici/src/lib/api/api-connect.js +++ b/deps/undici/src/lib/api/api-connect.js @@ -15,7 +15,7 @@ class ConnectHandler extends AsyncResource { throw new InvalidArgumentError('invalid callback') } - const { signal, opaque, responseHeaders, httpTunnel } = opts + const { signal, opaque, responseHeaders } = opts if (signal && typeof signal.on !== 'function' && typeof signal.addEventListener !== 'function') { throw new InvalidArgumentError('signal must be an EventEmitter or EventTarget') @@ -27,7 +27,6 @@ class ConnectHandler extends AsyncResource { this.responseHeaders = responseHeaders || null this.callback = callback this.abort = null - this.httpTunnel = httpTunnel addSignal(this, signal) } @@ -41,23 +40,8 @@ class ConnectHandler extends AsyncResource { this.context = context } - onHeaders (statusCode) { - // when httpTunnel headers are allowed - if (this.httpTunnel) { - const { callback, opaque } = this - if (statusCode !== 200) { - if (callback) { - this.callback = null - const err = new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling') - queueMicrotask(() => { - this.runInAsyncScope(callback, null, err, { opaque }) - }) - } - return 1 - } - } else { - throw new SocketError('bad connect', null) - } + onHeaders () { + throw new SocketError('bad connect', null) } onUpgrade (statusCode, rawHeaders, socket) { diff --git a/deps/undici/src/lib/api/api-request.js b/deps/undici/src/lib/api/api-request.js index 2fc5afa991ca48..b4674878d2e9d7 100644 --- a/deps/undici/src/lib/api/api-request.js +++ b/deps/undici/src/lib/api/api-request.js @@ -84,7 +84,8 @@ class RequestHandler extends AsyncResource { } const parsedHeaders = util.parseHeaders(rawHeaders) - const body = new Readable(resume, abort, parsedHeaders['content-type']) + const contentType = parsedHeaders['content-type'] + const body = new Readable(resume, abort, contentType) this.callback = null this.res = body @@ -92,8 +93,8 @@ class RequestHandler extends AsyncResource { if (callback !== null) { if (this.throwOnError && statusCode >= 400) { - this.runInAsyncScope(callback, null, - new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers) + this.runInAsyncScope(getResolveErrorBodyCallback, null, + { callback, body, contentType, statusCode, statusMessage, headers } ) return } @@ -152,6 +153,33 @@ class RequestHandler extends AsyncResource { } } +async function getResolveErrorBodyCallback ({ callback, body, contentType, statusCode, statusMessage, headers }) { + if (statusCode === 204 || !contentType) { + body.dump() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) + return + } + + try { + if (contentType.startsWith('application/json')) { + const payload = await body.json() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + + if (contentType.startsWith('text/')) { + const payload = await body.text() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers, payload)) + return + } + } catch (err) { + // Process in a fallback if error + } + + body.dump() + process.nextTick(callback, new ResponseStatusCodeError(`Response status code ${statusCode}${statusMessage ? `: ${statusMessage}` : ''}`, statusCode, headers)) +} + function request (opts, callback) { if (callback === undefined) { return new Promise((resolve, reject) => { diff --git a/deps/undici/src/lib/client.js b/deps/undici/src/lib/client.js index fea887e92b25c0..fb0b985faab585 100644 --- a/deps/undici/src/lib/client.js +++ b/deps/undici/src/lib/client.js @@ -720,7 +720,7 @@ class Parser { } } - if (request.method === 'CONNECT' && statusCode >= 200 && statusCode < 300) { + if (request.method === 'CONNECT') { assert(client[kRunning] === 1) this.upgrade = true return 2 @@ -889,10 +889,8 @@ function onParserTimeout (parser) { /* istanbul ignore else */ if (timeoutType === TIMEOUT_HEADERS) { - if (!socket[kWriting]) { - assert(!parser.paused, 'cannot be paused while waiting for headers') - util.destroy(socket, new HeadersTimeoutError()) - } + assert(!parser.paused, 'cannot be paused while waiting for headers') + util.destroy(socket, new HeadersTimeoutError()) } else if (timeoutType === TIMEOUT_BODY) { if (!parser.paused) { util.destroy(socket, new BodyTimeoutError()) diff --git a/deps/undici/src/lib/core/errors.js b/deps/undici/src/lib/core/errors.js index a36fd067c9f884..1f87dbf52dcb0f 100644 --- a/deps/undici/src/lib/core/errors.js +++ b/deps/undici/src/lib/core/errors.js @@ -1,13 +1,5 @@ 'use strict' -class AbortError extends Error { - constructor () { - super('The operation was aborted') - this.code = 'ABORT_ERR' - this.name = 'AbortError' - } -} - class UndiciError extends Error { constructor (message) { super(message) @@ -57,12 +49,13 @@ class BodyTimeoutError extends UndiciError { } class ResponseStatusCodeError extends UndiciError { - constructor (message, statusCode, headers) { + constructor (message, statusCode, headers, body) { super(message) Error.captureStackTrace(this, ResponseStatusCodeError) this.name = 'ResponseStatusCodeError' this.message = message || 'Response Status Code Error' this.code = 'UND_ERR_RESPONSE_STATUS_CODE' + this.body = body this.status = statusCode this.statusCode = statusCode this.headers = headers @@ -191,7 +184,6 @@ class HTTPParserError extends Error { } module.exports = { - AbortError, HTTPParserError, UndiciError, HeadersTimeoutError, diff --git a/deps/undici/src/lib/fetch/body.js b/deps/undici/src/lib/fetch/body.js index beb1e027c98af6..2228376d889394 100644 --- a/deps/undici/src/lib/fetch/body.js +++ b/deps/undici/src/lib/fetch/body.js @@ -4,6 +4,7 @@ const util = require('../core/util') const { ReadableStreamFrom, toUSVString, isBlobLike } = require('./util') const { FormData } = require('./formdata') const { kState } = require('./symbols') +const { webidl } = require('./webidl') const { Blob } = require('buffer') const { kBodyUsed } = require('../core/symbols') const assert = require('assert') @@ -262,100 +263,135 @@ function cloneBody (body) { } } -const methods = { - async blob () { - const chunks = [] +function bodyMixinMethods (instance) { + const methods = { + async blob () { + if (!(this instanceof instance)) { + throw new TypeError('Illegal invocation') + } - if (this[kState].body) { - if (isUint8Array(this[kState].body)) { - chunks.push(this[kState].body) - } else { - const stream = this[kState].body.stream + const chunks = [] - if (util.isDisturbed(stream)) { - throw new TypeError('disturbed') - } + if (this[kState].body) { + if (isUint8Array(this[kState].body)) { + chunks.push(this[kState].body) + } else { + const stream = this[kState].body.stream - if (stream.locked) { - throw new TypeError('locked') - } + if (util.isDisturbed(stream)) { + throw new TypeError('disturbed') + } + + if (stream.locked) { + throw new TypeError('locked') + } - // Compat. - stream[kBodyUsed] = true + // Compat. + stream[kBodyUsed] = true - for await (const chunk of stream) { - chunks.push(chunk) + for await (const chunk of stream) { + chunks.push(chunk) + } } } - } - return new Blob(chunks, { type: this.headers.get('Content-Type') || '' }) - }, + return new Blob(chunks, { type: this.headers.get('Content-Type') || '' }) + }, - async arrayBuffer () { - const blob = await this.blob() - return await blob.arrayBuffer() - }, + async arrayBuffer () { + if (!(this instanceof instance)) { + throw new TypeError('Illegal invocation') + } - async text () { - const blob = await this.blob() - return toUSVString(await blob.text()) - }, + const blob = await this.blob() + return await blob.arrayBuffer() + }, - async json () { - return JSON.parse(await this.text()) - }, + async text () { + if (!(this instanceof instance)) { + throw new TypeError('Illegal invocation') + } + + const blob = await this.blob() + return toUSVString(await blob.text()) + }, - async formData () { - const contentType = this.headers.get('Content-Type') - - // If mimeType’s essence is "multipart/form-data", then: - if (/multipart\/form-data/.test(contentType)) { - throw new NotSupportedError('multipart/form-data not supported') - } else if (/application\/x-www-form-urlencoded/.test(contentType)) { - // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: - - // 1. Let entries be the result of parsing bytes. - let entries - try { - entries = new URLSearchParams(await this.text()) - } catch (err) { - // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. - // 2. If entries is failure, then throw a TypeError. - throw Object.assign(new TypeError(), { cause: err }) + async json () { + if (!(this instanceof instance)) { + throw new TypeError('Illegal invocation') } - // 3. Return a new FormData object whose entries are entries. - const formData = new FormData() - for (const [name, value] of entries) { - formData.append(name, value) + return JSON.parse(await this.text()) + }, + + async formData () { + if (!(this instanceof instance)) { + throw new TypeError('Illegal invocation') + } + + const contentType = this.headers.get('Content-Type') + + // If mimeType’s essence is "multipart/form-data", then: + if (/multipart\/form-data/.test(contentType)) { + throw new NotSupportedError('multipart/form-data not supported') + } else if (/application\/x-www-form-urlencoded/.test(contentType)) { + // Otherwise, if mimeType’s essence is "application/x-www-form-urlencoded", then: + + // 1. Let entries be the result of parsing bytes. + let entries + try { + entries = new URLSearchParams(await this.text()) + } catch (err) { + // istanbul ignore next: Unclear when new URLSearchParams can fail on a string. + // 2. If entries is failure, then throw a TypeError. + throw Object.assign(new TypeError(), { cause: err }) + } + + // 3. Return a new FormData object whose entries are entries. + const formData = new FormData() + for (const [name, value] of entries) { + formData.append(name, value) + } + return formData + } else { + // Otherwise, throw a TypeError. + webidl.errors.exception({ + header: `${instance.name}.formData`, + value: 'Could not parse content as FormData.' + }) } - return formData - } else { - // Otherwise, throw a TypeError. - throw new TypeError() } } + + return methods } const properties = { body: { enumerable: true, get () { + if (!this || !this[kState]) { + throw new TypeError('Illegal invocation') + } + return this[kState].body ? this[kState].body.stream : null } }, bodyUsed: { enumerable: true, get () { + if (!this || !this[kState]) { + throw new TypeError('Illegal invocation') + } + return !!this[kState].body && util.isDisturbed(this[kState].body.stream) } } } function mixinBody (prototype) { - Object.assign(prototype, methods) - Object.defineProperties(prototype, properties) + Object.assign(prototype.prototype, bodyMixinMethods(prototype)) + Object.defineProperties(prototype.prototype, properties) } module.exports = { diff --git a/deps/undici/src/lib/fetch/constants.js b/deps/undici/src/lib/fetch/constants.js index 4358cd3ae088c1..6d201e01cff72b 100644 --- a/deps/undici/src/lib/fetch/constants.js +++ b/deps/undici/src/lib/fetch/constants.js @@ -60,7 +60,19 @@ const subresource = [ '' ] +/** @type {globalThis['DOMException']} */ +const DOMException = globalThis.DOMException ?? (() => { + // DOMException was only made a global in Node v17.0.0, + // but fetch supports >= v16.5. + try { + atob('~') + } catch (err) { + return Object.getPrototypeOf(err).constructor + } +})() + module.exports = { + DOMException, subresource, forbiddenMethods, requestBodyHeader, diff --git a/deps/undici/src/lib/fetch/file.js b/deps/undici/src/lib/fetch/file.js index a9c2355fc89a3b..647fc5ff38e29d 100644 --- a/deps/undici/src/lib/fetch/file.js +++ b/deps/undici/src/lib/fetch/file.js @@ -1,19 +1,27 @@ 'use strict' const { Blob } = require('buffer') +const { types } = require('util') const { kState } = require('./symbols') +const { isBlobLike } = require('./util') +const { webidl } = require('./webidl') class File extends Blob { constructor (fileBits, fileName, options = {}) { - // TODO: argument idl type check - // The File constructor is invoked with two or three parameters, depending // on whether the optional dictionary parameter is used. When the File() // constructor is invoked, user agents must run the following steps: + if (arguments.length < 2) { + throw new TypeError('2 arguments required') + } + + fileBits = webidl.converters['sequence'](fileBits) + fileName = webidl.converters.USVString(fileName) + options = webidl.converters.FilePropertyBag(options) // 1. Let bytes be the result of processing blob parts given fileBits and // options. - // TODO + // Note: Blob handles this for us // 2. Let n be the fileName argument to the constructor. const n = fileName @@ -25,17 +33,14 @@ class File extends Blob { // be set to the type dictionary member. If t contains any characters // outside the range U+0020 to U+007E, then set t to the empty string // and return from these substeps. - // TODO - const t = options.type - // 2. Convert every character in t to ASCII lowercase. - // TODO + // Note: Blob handles both of these steps for us // 3. If the lastModified member is provided, let d be set to the // lastModified dictionary member. If it is not provided, set d to the // current date and time represented as the number of milliseconds since // the Unix Epoch (which is the equivalent of Date.now() [ECMA-262]). - const d = options.lastModified ?? Date.now() + const d = options.lastModified // 4. Return a new File object F such that: // F refers to the bytes byte sequence. @@ -43,9 +48,8 @@ class File extends Blob { // F.name is set to n. // F.type is set to t. // F.lastModified is set to d. - // TODO - super(fileBits, { type: t }) + super(processBlobParts(fileBits, options), { type: options.type }) this[kState] = { name: n, lastModified: d @@ -190,4 +194,120 @@ class FileLike { } } -module.exports = { File: globalThis.File ?? File, FileLike } +webidl.converters.Blob = webidl.interfaceConverter(Blob) + +webidl.converters.BlobPart = function (V, opts) { + if (webidl.util.Type(V) === 'Object') { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }) + } + + return webidl.converters.BufferSource(V, opts) + } else { + return webidl.converters.USVString(V, opts) + } +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.BlobPart +) + +// https://www.w3.org/TR/FileAPI/#dfn-FilePropertyBag +webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ + { + key: 'lastModified', + converter: webidl.converters['long long'], + get defaultValue () { + return Date.now() + } + }, + { + key: 'type', + converter: webidl.converters.DOMString, + defaultValue: '' + }, + { + key: 'endings', + converter: (value) => { + value = webidl.converters.DOMString(value) + value = value.toLowerCase() + + if (value !== 'native') { + value = 'transparent' + } + + return value + }, + defaultValue: 'transparent' + } +]) + +/** + * @see https://www.w3.org/TR/FileAPI/#process-blob-parts + * @param {(NodeJS.TypedArray|Blob|string)[]} parts + * @param {{ type: string, endings: string }} options + */ +function processBlobParts (parts, options) { + // 1. Let bytes be an empty sequence of bytes. + /** @type {NodeJS.TypedArray[]} */ + const bytes = [] + + // 2. For each element in parts: + for (const element of parts) { + // 1. If element is a USVString, run the following substeps: + if (typeof element === 'string') { + // 1. Let s be element. + let s = element + + // 2. If the endings member of options is "native", set s + // to the result of converting line endings to native + // of element. + if (options.endings === 'native') { + s = convertLineEndingsNative(s) + } + + // 3. Append the result of UTF-8 encoding s to bytes. + bytes.push(new TextEncoder().encode(s)) + } else if ( + types.isAnyArrayBuffer(element) || + types.isTypedArray(element) + ) { + // 2. If element is a BufferSource, get a copy of the + // bytes held by the buffer source, and append those + // bytes to bytes. + if (!element.buffer) { // ArrayBuffer + bytes.push(new Uint8Array(element)) + } else { + bytes.push(element.buffer) + } + } else if (isBlobLike(element)) { + // 3. If element is a Blob, append the bytes it represents + // to bytes. + bytes.push(element) + } + } + + // 3. Return bytes. + return bytes +} + +/** + * @see https://www.w3.org/TR/FileAPI/#convert-line-endings-to-native + * @param {string} s + */ +function convertLineEndingsNative (s) { + // 1. Let native line ending be be the code point U+000A LF. + let nativeLineEnding = '\n' + + // 2. If the underlying platform’s conventions are to + // represent newlines as a carriage return and line feed + // sequence, set native line ending to the code point + // U+000D CR followed by the code point U+000A LF. + if (process.platform === 'win32') { + nativeLineEnding = '\r\n' + } + + return s.replace(/\r?\n/g, nativeLineEnding) +} + +module.exports = { File, FileLike } diff --git a/deps/undici/src/lib/fetch/formdata.js b/deps/undici/src/lib/fetch/formdata.js index 965e47c3a7f6ca..e4b9841bbfa309 100644 --- a/deps/undici/src/lib/fetch/formdata.js +++ b/deps/undici/src/lib/fetch/formdata.js @@ -3,43 +3,51 @@ const { isBlobLike, isFileLike, toUSVString, makeIterator } = require('./util') const { kState } = require('./symbols') const { File, FileLike } = require('./file') +const { webidl } = require('./webidl') const { Blob } = require('buffer') +// https://xhr.spec.whatwg.org/#formdata class FormData { static name = 'FormData' - constructor (...args) { - if (args.length > 0 && !(args[0]?.constructor?.name === 'HTMLFormElement')) { - throw new TypeError( - "Failed to construct 'FormData': parameter 1 is not of type 'HTMLFormElement'" - ) + constructor (form) { + if (arguments.length > 0 && form != null) { + webidl.errors.conversionFailed({ + prefix: 'FormData constructor', + argument: 'Argument 1', + types: ['null'] + }) } this[kState] = [] } - append (...args) { + append (name, value, filename = undefined) { if (!(this instanceof FormData)) { throw new TypeError('Illegal invocation') } - if (args.length < 2) { + if (arguments.length < 2) { throw new TypeError( - `Failed to execute 'append' on 'FormData': 2 arguments required, but only ${args.length} present.` + `Failed to execute 'append' on 'FormData': 2 arguments required, but only ${arguments.length} present.` ) } - if (args.length === 3 && !isBlobLike(args[1])) { + if (arguments.length === 3 && !isBlobLike(value)) { throw new TypeError( "Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'" ) } - const name = toUSVString(args[0]) - const filename = args.length === 3 ? toUSVString(args[2]) : undefined - // 1. Let value be value if given; otherwise blobValue. - const value = isBlobLike(args[1]) ? args[1] : toUSVString(args[1]) + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? webidl.converters.USVString(filename) + : undefined // 2. Let entry be the result of creating an entry with // name, value, and filename if given. @@ -49,18 +57,18 @@ class FormData { this[kState].push(entry) } - delete (...args) { + delete (name) { if (!(this instanceof FormData)) { throw new TypeError('Illegal invocation') } - if (args.length < 1) { + if (arguments.length < 1) { throw new TypeError( - `Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${args.length} present.` + `Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${arguments.length} present.` ) } - const name = toUSVString(args[0]) + name = webidl.converters.USVString(name) // The delete(name) method steps are to remove all entries whose name // is name from this’s entry list. @@ -74,18 +82,18 @@ class FormData { this[kState] = next } - get (...args) { + get (name) { if (!(this instanceof FormData)) { throw new TypeError('Illegal invocation') } - if (args.length < 1) { + if (arguments.length < 1) { throw new TypeError( - `Failed to execute 'get' on 'FormData': 1 arguments required, but only ${args.length} present.` + `Failed to execute 'get' on 'FormData': 1 arguments required, but only ${arguments.length} present.` ) } - const name = toUSVString(args[0]) + name = webidl.converters.USVString(name) // 1. If there is no entry whose name is name in this’s entry list, // then return null. @@ -99,18 +107,18 @@ class FormData { return this[kState][idx].value } - getAll (...args) { + getAll (name) { if (!(this instanceof FormData)) { throw new TypeError('Illegal invocation') } - if (args.length < 1) { + if (arguments.length < 1) { throw new TypeError( - `Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${args.length} present.` + `Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${arguments.length} present.` ) } - const name = toUSVString(args[0]) + name = webidl.converters.USVString(name) // 1. If there is no entry whose name is name in this’s entry list, // then return the empty list. @@ -121,48 +129,53 @@ class FormData { .map((entry) => entry.value) } - has (...args) { + has (name) { if (!(this instanceof FormData)) { throw new TypeError('Illegal invocation') } - if (args.length < 1) { + if (arguments.length < 1) { throw new TypeError( - `Failed to execute 'has' on 'FormData': 1 arguments required, but only ${args.length} present.` + `Failed to execute 'has' on 'FormData': 1 arguments required, but only ${arguments.length} present.` ) } - const name = toUSVString(args[0]) + name = webidl.converters.USVString(name) // The has(name) method steps are to return true if there is an entry // whose name is name in this’s entry list; otherwise false. return this[kState].findIndex((entry) => entry.name === name) !== -1 } - set (...args) { + set (name, value, filename = undefined) { if (!(this instanceof FormData)) { throw new TypeError('Illegal invocation') } - if (args.length < 2) { + if (arguments.length < 2) { throw new TypeError( - `Failed to execute 'set' on 'FormData': 2 arguments required, but only ${args.length} present.` + `Failed to execute 'set' on 'FormData': 2 arguments required, but only ${arguments.length} present.` ) } - if (args.length === 3 && !isBlobLike(args[1])) { + if (arguments.length === 3 && !isBlobLike(value)) { throw new TypeError( "Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'" ) } - const name = toUSVString(args[0]) - const filename = args.length === 3 ? toUSVString(args[2]) : undefined // The set(name, value) and set(name, blobValue, filename) method steps // are: // 1. Let value be value if given; otherwise blobValue. - const value = isBlobLike(args[1]) ? args[1] : toUSVString(args[1]) + + name = webidl.converters.USVString(name) + value = isBlobLike(value) + ? webidl.converters.Blob(value, { strict: false }) + : webidl.converters.USVString(value) + filename = arguments.length === 3 + ? toUSVString(filename) + : undefined // 2. Let entry be the result of creating an entry with name, value, and // filename if given. @@ -249,45 +262,46 @@ class FormData { FormData.prototype[Symbol.iterator] = FormData.prototype.entries +/** + * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry + * @param {string} name + * @param {string|Blob} value + * @param {?string} filename + * @returns + */ function makeEntry (name, value, filename) { - // To create an entry for name, value, and optionally a filename, run these - // steps: - - // 1. Let entry be a new entry. - const entry = { - name: null, - value: null - } - - // 2. Set entry’s name to name. - entry.name = name - - // 3. If value is a Blob object and not a File object, then set value to a new File - // object, representing the same bytes, whose name attribute value is "blob". - if (isBlobLike(value) && !isFileLike(value)) { - value = value instanceof Blob - ? new File([value], 'blob', value) - : new FileLike(value, 'blob', value) - } + // 1. Set name to the result of converting name into a scalar value string. + // "To convert a string into a scalar value string, replace any surrogates + // with U+FFFD." + // see: https://nodejs.org/dist/latest-v18.x/docs/api/buffer.html#buftostringencoding-start-end + name = Buffer.from(name).toString('utf8') + + // 2. If value is a string, then set value to the result of converting + // value into a scalar value string. + if (typeof value === 'string') { + value = Buffer.from(value).toString('utf8') + } else { + // 3. Otherwise: + + // 1. If value is not a File object, then set value to a new File object, + // representing the same bytes, whose name attribute value is "blob" + if (!isFileLike(value)) { + value = value instanceof Blob + ? new File([value], 'blob', { type: value.type }) + : new FileLike(value, 'blob', { type: value.type }) + } - // 4. If value is (now) a File object and filename is given, then set value to a - // new File object, representing the same bytes, whose name attribute value is - // filename. - // TODO: This is a bit weird... What if passed value is a File? - // Do we just override the name attribute? Since it says "if value is (now)" - // does that mean that this lives inside the previous condition? In which case - // creating one more File instance doesn't make much sense.... - if (isFileLike(value) && filename != null) { - value = value instanceof File - ? new File([value], filename, value) - : new FileLike(value, filename, value) + // 2. If filename is given, then set value to a new File object, + // representing the same bytes, whose name attribute is filename. + if (filename !== undefined) { + value = value instanceof File + ? new File([value], filename, { type: value.type }) + : new FileLike(value, filename, { type: value.type }) + } } - // 5. Set entry’s value to value. - entry.value = value - - // 6. Return entry. - return entry + // 4. Return an entry whose name is name and whose value is value. + return { name, value } } function * makeIterable (entries, type) { diff --git a/deps/undici/src/lib/fetch/headers.js b/deps/undici/src/lib/fetch/headers.js index 582e0c0f576ff1..cca961634c7920 100644 --- a/deps/undici/src/lib/fetch/headers.js +++ b/deps/undici/src/lib/fetch/headers.js @@ -2,75 +2,65 @@ 'use strict' -const { validateHeaderName, validateHeaderValue } = require('http') const { kHeadersList } = require('../core/symbols') const { kGuard } = require('./symbols') const { kEnumerableProperty } = require('../core/util') -const { makeIterator } = require('./util') +const { + makeIterator, + isValidHeaderName, + isValidHeaderValue +} = require('./util') +const { webidl } = require('./webidl') const kHeadersMap = Symbol('headers map') const kHeadersSortedMap = Symbol('headers map sorted') -function normalizeAndValidateHeaderName (name) { - if (name === undefined) { - throw new TypeError(`Header name ${name}`) - } - const normalizedHeaderName = name.toLocaleLowerCase() - validateHeaderName(normalizedHeaderName) - return normalizedHeaderName -} - -function normalizeAndValidateHeaderValue (name, value) { - if (value === undefined) { - throw new TypeError(value, name) - } - const normalizedHeaderValue = `${value}`.replace( - /^[\n\t\r\x20]+|[\n\t\r\x20]+$/g, +/** + * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize + * @param {string} potentialValue + */ +function headerValueNormalize (potentialValue) { + // To normalize a byte sequence potentialValue, remove + // any leading and trailing HTTP whitespace bytes from + // potentialValue. + return potentialValue.replace( + /^[\r\n\t ]+|[\r\n\t ]+$/g, '' ) - validateHeaderValue(name, normalizedHeaderValue) - return normalizedHeaderValue } function fill (headers, object) { // To fill a Headers object headers with a given object object, run these steps: - if (object[Symbol.iterator]) { - // 1. If object is a sequence, then for each header in object: - // TODO: How to check if sequence? - for (let header of object) { + // 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) { // 1. If header does not contain exactly two items, then throw a TypeError. - if (!header[Symbol.iterator]) { - // TODO: Spec doesn't define what to do here? - throw new TypeError() - } - - if (typeof header === 'string') { - // TODO: Spec doesn't define what to do here? - throw new TypeError() - } - - if (!Array.isArray(header)) { - header = [...header] - } - if (header.length !== 2) { - throw new TypeError() + webidl.errors.exception({ + header: 'Headers constructor', + message: `expected name/value pair to be length 2, found ${header.length}.` + }) } - // 2. Append header’s first item/header’s second item to headers. + // 2. Append (header’s first item, header’s second item) to headers. headers.append(header[0], header[1]) } - } else if (object && typeof object === 'object') { - // Otherwise, object is a record, then for each key → value in object, - // append key/value to headers. - // TODO: How to check if record? - for (const header of Object.entries(object)) { - headers.append(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) } } else { - // TODO: Spec doesn't define what to do here? - throw TypeError() + webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) } } @@ -85,48 +75,75 @@ class HeadersList { } } + // https://fetch.spec.whatwg.org/#header-list-contains + contains (name) { + // A header list list contains a header name name if list + // contains a header whose name is a byte-case-insensitive + // match for name. + name = name.toLowerCase() + + return this[kHeadersMap].has(name) + } + clear () { this[kHeadersMap].clear() this[kHeadersSortedMap] = null } + // https://fetch.spec.whatwg.org/#concept-header-list-append append (name, value) { this[kHeadersSortedMap] = null - const normalizedName = normalizeAndValidateHeaderName(name) - const normalizedValue = normalizeAndValidateHeaderValue(name, value) - - const exists = this[kHeadersMap].get(normalizedName) + // 1. If list contains name, then set name to the first such + // header’s name. + name = name.toLowerCase() + const exists = this[kHeadersMap].get(name) + // 2. Append (name, value) to list. if (exists) { - this[kHeadersMap].set(normalizedName, `${exists}, ${normalizedValue}`) + this[kHeadersMap].set(name, `${exists}, ${value}`) } else { - this[kHeadersMap].set(normalizedName, `${normalizedValue}`) + this[kHeadersMap].set(name, `${value}`) } } + // https://fetch.spec.whatwg.org/#concept-header-list-set set (name, value) { this[kHeadersSortedMap] = null - const normalizedName = normalizeAndValidateHeaderName(name) - return this[kHeadersMap].set(normalizedName, value) + // 1. If list contains name, then set the value of + // the first such header to value and remove the + // others. + // 2. Otherwise, append header (name, value) to list. + return this[kHeadersMap].set(name, value) } + // https://fetch.spec.whatwg.org/#concept-header-list-delete delete (name) { this[kHeadersSortedMap] = null - const normalizedName = normalizeAndValidateHeaderName(name) - return this[kHeadersMap].delete(normalizedName) + name = name.toLowerCase() + return this[kHeadersMap].delete(name) } + // https://fetch.spec.whatwg.org/#concept-header-list-get get (name) { - const normalizedName = normalizeAndValidateHeaderName(name) - return this[kHeadersMap].get(normalizedName) ?? null + name = name.toLowerCase() + + // 1. If list does not contain name, then return null. + if (!this.contains(name)) { + 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) ?? null } has (name) { - const normalizedName = normalizeAndValidateHeaderName(name) - return this[kHeadersMap].has(normalizedName) + name = name.toLowerCase() + return this[kHeadersMap].has(name) } keys () { @@ -148,17 +165,7 @@ class HeadersList { // https://fetch.spec.whatwg.org/#headers-class class Headers { - constructor (...args) { - if ( - args[0] !== undefined && - !(typeof args[0] === 'object' && args[0] != null) && - !Array.isArray(args[0]) - ) { - throw new TypeError( - "Failed to construct 'Headers': The provided value is not of type '(record or sequence>" - ) - } - const init = args.length >= 1 ? args[0] ?? {} : {} + constructor (init = undefined) { this[kHeadersList] = new HeadersList() // The new Headers(init) constructor steps are: @@ -167,7 +174,10 @@ class Headers { this[kGuard] = 'none' // 2. If init is given, then fill this with init. - fill(this, init) + if (init !== undefined) { + init = webidl.converters.HeadersInit(init) + fill(this, init) + } } get [Symbol.toStringTag] () { @@ -186,14 +196,46 @@ class Headers { ) } + 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)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.append', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + 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') { + // 5. Otherwise, if headers’s guard is "request-no-cors": // TODO } - return this[kHeadersList].append(String(name), String(value)) + // 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) } // https://fetch.spec.whatwg.org/#dom-headers-delete @@ -208,6 +250,26 @@ class Headers { ) } + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.delete', + value: name, + type: 'header name' + }) + } + + // 2. If this’s guard is "immutable", then throw a TypeError. + // 3. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 4. Otherwise, if this’s guard is "request-no-cors", name + // is not a no-CORS-safelisted request-header name, and + // name is not a privileged no-CORS request-header name, + // return. + // 5. Otherwise, if this’s guard is "response" and name is + // a forbidden response-header name, return. // Note: undici does not implement forbidden header names if (this[kGuard] === 'immutable') { throw new TypeError('immutable') @@ -215,7 +277,16 @@ class Headers { // TODO } - return this[kHeadersList].delete(String(name)) + // 6. If this’s header list does not contain name, then + // return. + if (!this[kHeadersList].contains(name)) { + return + } + + // 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) } // https://fetch.spec.whatwg.org/#dom-headers-get @@ -230,7 +301,20 @@ class Headers { ) } - return this[kHeadersList].get(String(name)) + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.get', + value: name, + type: 'header name' + }) + } + + // 2. Return the result of getting name from this’s header + // list. + return this[kHeadersList].get(name) } // https://fetch.spec.whatwg.org/#dom-headers-has @@ -245,7 +329,20 @@ class Headers { ) } - return this[kHeadersList].has(String(name)) + name = webidl.converters.ByteString(name) + + // 1. If name is not a header name, then throw a TypeError. + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.has', + value: name, + type: 'header name' + }) + } + + // 2. Return true if this’s header list contains name; + // otherwise false. + return this[kHeadersList].contains(name) } // https://fetch.spec.whatwg.org/#dom-headers-set @@ -260,6 +357,36 @@ class Headers { ) } + 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)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value: name, + type: 'header name' + }) + } else if (!isValidHeaderValue(value)) { + webidl.errors.invalidArgument({ + prefix: 'Headers.set', + value, + type: 'header value' + }) + } + + // 3. If this’s guard is "immutable", then throw a TypeError. + // 4. Otherwise, if this’s guard is "request" and name is a + // forbidden header name, return. + // 5. Otherwise, if this’s guard is "request-no-cors" and + // name/value is not a no-CORS-safelisted request-header, + // return. + // 6. Otherwise, if this’s guard is "response" and name is a + // forbidden response-header name, return. // Note: undici does not implement forbidden header names if (this[kGuard] === 'immutable') { throw new TypeError('immutable') @@ -267,7 +394,10 @@ class Headers { // TODO } - return this[kHeadersList].set(String(name), String(value)) + // 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) } get [kHeadersSortedMap] () { @@ -348,10 +478,24 @@ Object.defineProperties(Headers.prototype, { forEach: kEnumerableProperty }) +webidl.converters.HeadersInit = function (V) { + if (webidl.util.Type(V) === 'Object') { + if (V[Symbol.iterator]) { + return webidl.converters['sequence>'](V) + } + + return webidl.converters['record'](V) + } + + webidl.errors.conversionFailed({ + prefix: 'Headers constructor', + argument: 'Argument 1', + types: ['sequence>', 'record'] + }) +} + module.exports = { fill, Headers, - HeadersList, - normalizeAndValidateHeaderName, - normalizeAndValidateHeaderValue + HeadersList } diff --git a/deps/undici/src/lib/fetch/index.js b/deps/undici/src/lib/fetch/index.js index ddf4b03d4cbab8..cf91a5d378e98b 100644 --- a/deps/undici/src/lib/fetch/index.js +++ b/deps/undici/src/lib/fetch/index.js @@ -36,7 +36,6 @@ const { isAborted } = require('./util') const { kState, kHeaders, kGuard, kRealm } = require('./symbols') -const { AbortError } = require('../core/errors') const assert = require('assert') const { safelyExtractBody, extractBody } = require('./body') const { @@ -44,7 +43,8 @@ const { nullBodyStatus, safeMethods, requestBodyHeader, - subresource + subresource, + DOMException } = require('./constants') const { kHeadersList } = require('../core/symbols') const EE = require('events') @@ -57,6 +57,10 @@ const { TransformStream } = require('stream/web') let resolveObjectURL let ReadableStream +const nodeVersion = process.versions.node.split('.') +const nodeMajor = Number(nodeVersion[0]) +const nodeMinor = Number(nodeVersion[1]) + class Fetch extends EE { constructor (dispatcher) { super() @@ -82,7 +86,7 @@ class Fetch extends EE { return } - const reason = new AbortError() + const reason = new DOMException('The operation was aborted.', 'AbortError') this.state = 'aborted' this.connection?.destroy(reason) @@ -91,24 +95,12 @@ class Fetch extends EE { } // https://fetch.spec.whatwg.org/#fetch-method -async function fetch (...args) { - if (args.length < 1) { +async function fetch (input, init = {}) { + if (arguments.length < 1) { throw new TypeError( - `Failed to execute 'fetch' on 'Window': 1 argument required, but only ${args.length} present.` + `Failed to execute 'fetch' on 'Window': 1 argument required, but only ${arguments.length} present.` ) } - if ( - args.length >= 1 && - typeof args[1] !== 'object' && - args[1] !== undefined - ) { - throw new TypeError( - "Failed to execute 'fetch' on 'Window': cannot convert to dictionary." - ) - } - - const resource = args[0] - const init = args.length >= 1 ? args[1] ?? {} : {} // 1. Let p be a new promise. const p = createDeferredPromise() @@ -116,7 +108,14 @@ async function fetch (...args) { // 2. Let requestObject be the result of invoking the initial value of // Request as constructor with input and init as arguments. If this throws // an exception, reject p with it and return p. - const requestObject = new Request(resource, init) + let requestObject + + try { + requestObject = new Request(input, init) + } catch (e) { + p.reject(e) + return p.promise + } // 3. Let request be requestObject’s request. const request = requestObject[kState] @@ -288,14 +287,16 @@ function finalizeAndReportTiming (response, initiatorType = 'other') { } // https://w3c.github.io/resource-timing/#dfn-mark-resource-timing -function markResourceTiming () { - // TODO +function markResourceTiming (timingInfo, originalURL, initiatorType, globalThis, cacheState) { + if (nodeMajor >= 18 && nodeMinor >= 2) { + performance.markResourceTiming(timingInfo, originalURL, initiatorType, globalThis, cacheState) + } } // https://fetch.spec.whatwg.org/#abort-fetch function abortFetch (p, request, responseObject) { // 1. Let error be an "AbortError" DOMException. - const error = new AbortError() + const error = new DOMException('The operation was aborted.', 'AbortError') // 2. Reject promise with error. p.reject(error) @@ -1058,7 +1059,7 @@ async function httpFetch (fetchParams) { // 2. Switch on request’s redirect mode: if (request.redirect === 'error') { // Set response to a network error. - response = makeNetworkError() + response = makeNetworkError('unexpected redirect') } else if (request.redirect === 'manual') { // Set response to an opaque-redirect filtered response whose internal // response is actualResponse. @@ -1555,7 +1556,7 @@ async function httpNetworkFetch ( destroy (err) { if (!this.destroyed) { this.destroyed = true - this.abort?.(err ?? new AbortError()) + this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError')) } } } @@ -1885,7 +1886,9 @@ async function httpNetworkFetch ( // 2. If stream is readable, error stream with an "AbortError" DOMException. if (isReadable(stream)) { - fetchParams.controller.controller.error(new AbortError()) + fetchParams.controller.controller.error( + new DOMException('The operation was aborted.', 'AbortError') + ) } } else { // 3. Otherwise, if stream is readable, error stream with a TypeError. @@ -1926,7 +1929,7 @@ async function httpNetworkFetch ( const { connection } = fetchParams.controller if (connection.destroyed) { - abort(new AbortError()) + abort(new DOMException('The operation was aborted.', 'AbortError')) } else { fetchParams.controller.on('terminated', abort) this.abort = connection.abort = abort diff --git a/deps/undici/src/lib/fetch/request.js b/deps/undici/src/lib/fetch/request.js index 42c7eb447a2b07..7e1b3d8eb1d009 100644 --- a/deps/undici/src/lib/fetch/request.js +++ b/deps/undici/src/lib/fetch/request.js @@ -8,7 +8,6 @@ const util = require('../core/util') const { isValidHTTPToken, sameOrigin, - toUSVString, normalizeMethod } = require('./util') const { @@ -22,6 +21,7 @@ const { } = require('./constants') const { kEnumerableProperty } = util const { kHeaders, kSignal, kState, kGuard, kRealm } = require('./symbols') +const { webidl } = require('./webidl') const { kHeadersList } = require('../core/symbols') const assert = require('assert') @@ -36,27 +36,19 @@ const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => { // https://fetch.spec.whatwg.org/#request-class class Request { // https://fetch.spec.whatwg.org/#dom-request - constructor (...args) { - if (args[0] === kInit) { + constructor (input, init = {}) { + if (input === kInit) { return } - if (args.length < 1) { + if (arguments.length < 1) { throw new TypeError( - `Failed to construct 'Request': 1 argument required, but only ${args.length} present.` + `Failed to construct 'Request': 1 argument required, but only ${arguments.length} present.` ) } - if ( - args.length >= 1 && - typeof args[1] !== 'object' && - args[1] !== undefined - ) { - throw new TypeError( - "Failed to construct 'Request': cannot convert to dictionary." - ) - } - const input = args[0] instanceof Request ? args[0] : toUSVString(args[0]) - const init = args.length >= 1 ? args[1] ?? {} : {} + + input = webidl.converters.RequestInfo(input) + init = webidl.converters.RequestInit(init) // TODO this[kRealm] = { settingsObject: {} } @@ -266,7 +258,10 @@ class Request { // 17. If mode is "navigate", then throw a TypeError. if (mode === 'navigate') { - throw new TypeError() + webidl.errors.exception({ + header: 'Request constructor', + message: 'invalid request mode navigate.' + }) } // 18. If mode is non-null, set request’s mode to mode. @@ -748,7 +743,7 @@ class Request { } } -mixinBody(Request.prototype) +mixinBody(Request) function makeRequest (init) { // https://fetch.spec.whatwg.org/#requests @@ -823,4 +818,109 @@ Object.defineProperties(Request.prototype, { signal: kEnumerableProperty }) +webidl.converters.Request = webidl.interfaceConverter( + Request +) + +// https://fetch.spec.whatwg.org/#requestinfo +webidl.converters.RequestInfo = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (V instanceof Request) { + return webidl.converters.Request(V) + } + + return webidl.converters.USVString(V) +} + +webidl.converters.AbortSignal = webidl.interfaceConverter( + AbortSignal +) + +// https://fetch.spec.whatwg.org/#requestinit +webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: 'method', + converter: webidl.converters.ByteString + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + }, + { + key: 'body', + converter: webidl.nullableConverter( + webidl.converters.BodyInit + ) + }, + { + key: 'referrer', + converter: webidl.converters.USVString + }, + { + key: 'referrerPolicy', + converter: webidl.converters.DOMString, + // https://w3c.github.io/webappsec-referrer-policy/#referrer-policy + allowedValues: [ + '', 'no-referrer', 'no-referrer-when-downgrade', + 'same-origin', 'origin', 'strict-origin', + 'origin-when-cross-origin', 'strict-origin-when-cross-origin', + 'unsafe-url' + ] + }, + { + key: 'mode', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#concept-request-mode + allowedValues: [ + 'same-origin', 'cors', 'no-cors', 'navigate', 'websocket' + ] + }, + { + key: 'credentials', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcredentials + allowedValues: [ + 'omit', 'same-origin', 'include' + ] + }, + { + key: 'cache', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestcache + allowedValues: [ + 'default', 'no-store', 'reload', 'no-cache', 'force-cache', + 'only-if-cached' + ] + }, + { + key: 'redirect', + converter: webidl.converters.DOMString, + // https://fetch.spec.whatwg.org/#requestredirect + allowedValues: [ + 'follow', 'error', 'manual' + ] + }, + { + key: 'integrity', + converter: webidl.converters.DOMString + }, + { + key: 'keepalive', + converter: webidl.converters.boolean + }, + { + key: 'signal', + converter: webidl.nullableConverter( + webidl.converters.AbortSignal + ) + }, + { + key: 'window', + converter: webidl.converters.any + } +]) + module.exports = { Request, makeRequest } diff --git a/deps/undici/src/lib/fetch/response.js b/deps/undici/src/lib/fetch/response.js index 582876e050a34e..4649a5da907029 100644 --- a/deps/undici/src/lib/fetch/response.js +++ b/deps/undici/src/lib/fetch/response.js @@ -1,18 +1,30 @@ 'use strict' const { Headers, HeadersList, fill } = require('./headers') -const { AbortError } = require('../core/errors') const { extractBody, cloneBody, mixinBody } = require('./body') const util = require('../core/util') const { kEnumerableProperty } = util -const { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted, serializeJavascriptValueToJSONString } = require('./util') +const { + responseURL, + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString +} = require('./util') const { redirectStatus, - nullBodyStatus + nullBodyStatus, + DOMException } = require('./constants') const { kState, kHeaders, kGuard, kRealm } = require('./symbols') +const { webidl } = require('./webidl') +const { FormData } = require('./formdata') const { kHeadersList } = require('../core/symbols') const assert = require('assert') +const { types } = require('util') + +const ReadableStream = globalThis.ReadableStream || require('stream/web').ReadableStream // https://fetch.spec.whatwg.org/#response-class class Response { @@ -41,17 +53,8 @@ class Response { ) } - if (init === null || typeof init !== 'object') { - throw new TypeError( - `Failed to execute 'json' on 'Response': init must be a RequestInit, found ${typeof init}.` - ) - } - - init = { - status: 200, - statusText: '', - headers: new HeadersList(), - ...init + if (init !== null) { + init = webidl.converters.ResponseInit(init) } // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data. @@ -78,17 +81,17 @@ class Response { } // Creates a redirect Response that redirects to url with status status. - static redirect (...args) { + static redirect (url, status = 302) { const relevantRealm = { settingsObject: {} } - if (args.length < 1) { + if (arguments.length < 1) { throw new TypeError( - `Failed to execute 'redirect' on 'Response': 1 argument required, but only ${args.length} present.` + `Failed to execute 'redirect' on 'Response': 1 argument required, but only ${arguments.length} present.` ) } - const status = args.length >= 2 ? args[1] : 302 - const url = toUSVString(args[0]) + url = webidl.converters.USVString(url) + status = webidl.converters['unsigned short'](status) // 1. Let parsedURL be the result of parsing url with current settings // object’s API base URL. @@ -130,19 +133,12 @@ class Response { } // https://fetch.spec.whatwg.org/#dom-response - constructor (...args) { - if ( - args.length >= 1 && - typeof args[1] !== 'object' && - args[1] !== undefined - ) { - throw new TypeError( - "Failed to construct 'Request': cannot convert to dictionary." - ) + constructor (body = null, init = {}) { + if (body !== null) { + body = webidl.converters.BodyInit(body) } - const body = args.length >= 1 ? args[0] : null - const init = args.length >= 2 ? args[1] ?? {} : {} + init = webidl.converters.ResponseInit(init) // TODO this[kRealm] = { settingsObject: {} } @@ -269,7 +265,10 @@ class Response { // 1. If this is unusable, then throw a TypeError. if (this.bodyUsed || (this.body && this.body.locked)) { - throw new TypeError() + webidl.errors.exception({ + header: 'Response.clone', + message: 'Body has already been consumed.' + }) } // 2. Let clonedResponse be the result of cloning this’s response. @@ -287,7 +286,8 @@ class Response { return clonedResponseObject } } -mixinBody(Response.prototype) + +mixinBody(Response) Object.defineProperties(Response.prototype, { type: kEnumerableProperty, @@ -440,7 +440,7 @@ function makeAppropriateNetworkError (fetchParams) { // 2. Return an aborted network error if fetchParams is aborted; // otherwise return a network error. return isAborted(fetchParams) - ? makeNetworkError(new AbortError()) + ? makeNetworkError(new DOMException('The operation was aborted.', 'AbortError')) : makeNetworkError(fetchParams.controller.terminated.reason) } @@ -448,7 +448,7 @@ function makeAppropriateNetworkError (fetchParams) { function initializeResponse (response, init, body) { // 1. If init["status"] is not in the range 200 to 599, inclusive, then // throw a RangeError. - if (init.status != null && (init.status < 200 || init.status > 599)) { + if (init.status !== null && (init.status < 200 || init.status > 599)) { throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.') } @@ -481,7 +481,10 @@ function initializeResponse (response, init, body) { if (body) { // 1. If response's status is a null body status, then throw a TypeError. if (nullBodyStatus.includes(response.status)) { - throw new TypeError() + webidl.errors.exception({ + header: 'Response constructor', + message: 'Invalid response status code.' + }) } // 2. Set response's body to body's body. @@ -495,6 +498,79 @@ function initializeResponse (response, init, body) { } } +webidl.converters.ReadableStream = webidl.interfaceConverter( + ReadableStream +) + +webidl.converters.FormData = webidl.interfaceConverter( + FormData +) + +webidl.converters.URLSearchParams = webidl.interfaceConverter( + URLSearchParams +) + +// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit +webidl.converters.XMLHttpRequestBodyInit = function (V) { + if (typeof V === 'string') { + return webidl.converters.USVString(V) + } + + if (isBlobLike(V)) { + return webidl.converters.Blob(V) + } + + if ( + types.isAnyArrayBuffer(V) || + types.isTypedArray(V) || + types.isDataView(V) + ) { + return webidl.converters.BufferSource(V) + } + + if (V instanceof FormData) { + return webidl.converters.FormData(V) + } + + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V) + } + + return webidl.converters.DOMString(V) +} + +// https://fetch.spec.whatwg.org/#bodyinit +webidl.converters.BodyInit = function (V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V) + } + + // Note: the spec doesn't include async iterables, + // this is an undici extension. + if (V?.[Symbol.asyncIterator]) { + return V + } + + return webidl.converters.XMLHttpRequestBodyInit(V) +} + +webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: 'status', + converter: webidl.converters['unsigned short'], + defaultValue: 200 + }, + { + key: 'statusText', + converter: webidl.converters.ByteString, + defaultValue: '' + }, + { + key: 'headers', + converter: webidl.converters.HeadersInit + } +]) + module.exports = { makeNetworkError, makeResponse, diff --git a/deps/undici/src/lib/fetch/util.js b/deps/undici/src/lib/fetch/util.js index 3c088fbc6e92ad..e309e3263f4bcc 100644 --- a/deps/undici/src/lib/fetch/util.js +++ b/deps/undici/src/lib/fetch/util.js @@ -145,6 +145,49 @@ function isValidHTTPToken (characters) { return true } +// https://fetch.spec.whatwg.org/#header-name +// https://github.com/chromium/chromium/blob/b3d37e6f94f87d59e44662d6078f6a12de845d17/net/http/http_util.cc#L342 +function isValidHeaderName (potentialValue) { + if (potentialValue.length === 0) { + return false + } + + for (const char of potentialValue) { + if (!isValidHTTPToken(char)) { + return false + } + } + + return true +} + +/** + * @see https://fetch.spec.whatwg.org/#header-value + * @param {string} potentialValue + */ +function isValidHeaderValue (potentialValue) { + // - Has no leading or trailing HTTP tab or space bytes. + // - Contains no 0x00 (NUL) or HTTP newline bytes. + if ( + potentialValue.startsWith('\t') || + potentialValue.startsWith(' ') || + potentialValue.endsWith('\t') || + potentialValue.endsWith(' ') + ) { + return false + } + + if ( + potentialValue.includes('\0') || + potentialValue.includes('\r') || + potentialValue.includes('\n') + ) { + return false + } + + return true +} + // https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect function setRequestReferrerPolicyOnRedirect (request, actualResponse) { // Given a request request and a response actualResponse, this algorithm @@ -418,5 +461,7 @@ module.exports = { sameOrigin, normalizeMethod, serializeJavascriptValueToJSONString, - makeIterator + makeIterator, + isValidHeaderName, + isValidHeaderValue } diff --git a/deps/undici/src/lib/fetch/webidl.js b/deps/undici/src/lib/fetch/webidl.js new file mode 100644 index 00000000000000..252dab29b83d6b --- /dev/null +++ b/deps/undici/src/lib/fetch/webidl.js @@ -0,0 +1,594 @@ +'use strict' + +const { toUSVString, types } = require('util') + +const webidl = {} +webidl.converters = {} +webidl.util = {} +webidl.errors = {} + +/** + * + * @param {{ + * header: string + * message: string + * }} message + */ +webidl.errors.exception = function (message) { + throw new TypeError(`${message.header}: ${message.message}`) +} + +/** + * Throw an error when conversion from one type to another has failed + * @param {{ + * prefix: string + * argument: string + * types: string[] + * }} context + */ +webidl.errors.conversionFailed = function (context) { + const plural = context.types.length === 1 ? '' : ' one of' + const message = + `${context.argument} could not be converted to` + + `${plural}: ${context.types.join(', ')}.` + + return webidl.errors.exception({ + header: context.prefix, + message + }) +} + +/** + * Throw an error when an invalid argument is provided + * @param {{ + * prefix: string + * value: string + * type: string + * }} context + */ +webidl.errors.invalidArgument = function (context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }) +} + +// https://tc39.es/ecma262/#sec-ecmascript-data-types-and-values +webidl.util.Type = function (V) { + switch (typeof V) { + case 'undefined': return 'Undefined' + case 'boolean': return 'Boolean' + case 'string': return 'String' + case 'symbol': return 'Symbol' + case 'number': return 'Number' + case 'bigint': return 'BigInt' + case 'function': + case 'object': { + if (V === null) { + return 'Null' + } + + return 'Object' + } + } +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-converttoint +webidl.util.ConvertToInt = function (V, bitLength, signedness, opts = {}) { + let upperBound + let lowerBound + + // 1. If bitLength is 64, then: + if (bitLength === 64) { + // 1. Let upperBound be 2^53 − 1. + upperBound = Math.pow(2, 53) - 1 + + // 2. If signedness is "unsigned", then let lowerBound be 0. + if (signedness === 'unsigned') { + lowerBound = 0 + } else { + // 3. Otherwise let lowerBound be −2^53 + 1. + lowerBound = Math.pow(-2, 53) + 1 + } + } else if (signedness === 'unsigned') { + // 2. Otherwise, if signedness is "unsigned", then: + + // 1. Let lowerBound be 0. + lowerBound = 0 + + // 2. Let upperBound be 2^bitLength − 1. + upperBound = Math.pow(2, bitLength) - 1 + } else { + // 3. Otherwise: + + // 1. Let lowerBound be -2^bitLength − 1. + lowerBound = Math.pow(-2, bitLength) - 1 + + // 2. Let upperBound be 2^bitLength − 1 − 1. + upperBound = Math.pow(2, bitLength - 1) - 1 + } + + // 4. Let x be ? ToNumber(V). + let x = Number(V) + + // 5. If x is −0, then set x to +0. + if (Object.is(-0, x)) { + x = 0 + } + + // 6. If the conversion is to an IDL type associated + // with the [EnforceRange] extended attribute, then: + if (opts.enforceRange === true) { + // 1. If x is NaN, +∞, or −∞, then throw a TypeError. + if ( + Number.isNaN(x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + webidl.errors.exception({ + header: 'Integer conversion', + message: `Could not convert ${V} to an integer.` + }) + } + + // 2. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 3. If x < lowerBound or x > upperBound, then + // throw a TypeError. + if (x < lowerBound || x > upperBound) { + webidl.errors.exception({ + header: 'Integer conversion', + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }) + } + + // 4. Return x. + return x + } + + // 7. If x is not NaN and the conversion is to an IDL + // type associated with the [Clamp] extended + // attribute, then: + if (!Number.isNaN(x) && opts.clamp === true) { + // 1. Set x to min(max(x, lowerBound), upperBound). + x = Math.min(Math.max(x, lowerBound), upperBound) + + // 2. Round x to the nearest integer, choosing the + // even integer if it lies halfway between two, + // and choosing +0 rather than −0. + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x) + } else { + x = Math.ceil(x) + } + + // 3. Return x. + return x + } + + // 8. If x is NaN, +0, +∞, or −∞, then return +0. + if ( + Number.isNaN(x) || + Object.is(0, x) || + x === Number.POSITIVE_INFINITY || + x === Number.NEGATIVE_INFINITY + ) { + return 0 + } + + // 9. Set x to IntegerPart(x). + x = webidl.util.IntegerPart(x) + + // 10. Set x to x modulo 2^bitLength. + x = x % Math.pow(2, bitLength) + + // 11. If signedness is "signed" and x ≥ 2^bitLength − 1, + // then return x − 2^bitLength. + if (signedness === 'signed' && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength) + } + + // 12. Otherwise, return x. + return x +} + +// https://webidl.spec.whatwg.org/#abstract-opdef-integerpart +webidl.util.IntegerPart = function (n) { + // 1. Let r be floor(abs(n)). + const r = Math.floor(Math.abs(n)) + + // 2. If n < 0, then return -1 × r. + if (n < 0) { + return -1 * r + } + + // 3. Otherwise, return r. + return r +} + +// https://webidl.spec.whatwg.org/#es-sequence +webidl.sequenceConverter = function (converter) { + return (V) => { + // 1. If Type(V) is not Object, throw a TypeError. + if (webidl.util.Type(V) !== 'Object') { + webidl.errors.exception({ + header: 'Sequence', + message: `Value of type ${webidl.util.Type(V)} is not an Object.` + }) + } + + // 2. Let method be ? GetMethod(V, @@iterator). + /** @type {Generator} */ + const method = V?.[Symbol.iterator]?.() + const seq = [] + + // 3. If method is undefined, throw a TypeError. + if ( + method === undefined || + typeof method.next !== 'function' + ) { + webidl.errors.exception({ + header: 'Sequence', + message: 'Object is not an iterator.' + }) + } + + // https://webidl.spec.whatwg.org/#create-sequence-from-iterable + while (true) { + const { done, value } = method.next() + + if (done) { + break + } + + seq.push(converter(value)) + } + + return seq + } +} + +webidl.recordConverter = function (keyConverter, valueConverter) { + return (V) => { + const record = {} + const type = webidl.util.Type(V) + + if (type === 'Undefined' || type === 'Null') { + return record + } + + if (type !== 'Object') { + webidl.errors.exception({ + header: 'Record', + message: `Expected ${V} to be an Object type.` + }) + } + + for (let [key, value] of Object.entries(V)) { + key = keyConverter(key) + value = valueConverter(value) + + record[key] = value + } + + return record + } +} + +webidl.interfaceConverter = function (i) { + return (V, opts = {}) => { + if (opts.strict !== false && !(V instanceof i)) { + webidl.errors.exception({ + header: i.name, + message: `Expected ${V} to be an instance of ${i.name}.` + }) + } + + return V + } +} + +/** + * @param {{ + * key: string, + * defaultValue?: any, + * required?: boolean, + * converter: (...args: unknown[]) => unknown, + * allowedValues?: any[] + * }[]} converters + * @returns + */ +webidl.dictionaryConverter = function (converters) { + return (dictionary) => { + const type = webidl.util.Type(dictionary) + const dict = {} + + if (type !== 'Null' && type !== 'Undefined' && type !== 'Object') { + webidl.errors.exception({ + header: 'Dictionary', + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }) + } + + for (const options of converters) { + const { key, defaultValue, required, converter } = options + + if (required === true) { + if (!Object.hasOwn(dictionary, key)) { + webidl.errors.exception({ + header: 'Dictionary', + message: `Missing required key "${key}".` + }) + } + } + + let value = dictionary[key] + const hasDefault = Object.hasOwn(options, 'defaultValue') + + // Only use defaultValue if value is undefined and + // a defaultValue options was provided. + if (hasDefault && value !== null) { + value = value ?? defaultValue + } + + // A key can be optional and have no default value. + // When this happens, do not perform a conversion, + // and do not assign the key a value. + if (required || hasDefault || value !== undefined) { + value = converter(value) + + if ( + options.allowedValues && + !options.allowedValues.includes(value) + ) { + webidl.errors.exception({ + header: 'Dictionary', + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(', ')}.` + }) + } + + dict[key] = value + } + } + + return dict + } +} + +webidl.nullableConverter = function (converter) { + return (V) => { + if (V === null) { + return V + } + + return converter(V) + } +} + +// https://webidl.spec.whatwg.org/#es-DOMString +webidl.converters.DOMString = function (V, opts = {}) { + // 1. If V is null and the conversion is to an IDL type + // associated with the [LegacyNullToEmptyString] + // extended attribute, then return the DOMString value + // that represents the empty string. + if (V === null && opts.legacyNullToEmptyString) { + return '' + } + + // 2. Let x be ? ToString(V). + if (typeof V === 'symbol') { + throw new TypeError('Could not convert argument of type symbol to string.') + } + + // 3. Return the IDL DOMString value that represents the + // same sequence of code units as the one the + // ECMAScript String value x represents. + return String(V) +} + +// eslint-disable-next-line no-control-regex +const isNotLatin1 = /[^\u0000-\u00ff]/ + +// https://webidl.spec.whatwg.org/#es-ByteString +webidl.converters.ByteString = function (V) { + // 1. Let x be ? ToString(V). + // Note: DOMString converter perform ? ToString(V) + const x = webidl.converters.DOMString(V) + + // 2. If the value of any element of x is greater than + // 255, then throw a TypeError. + if (isNotLatin1.test(x)) { + throw new TypeError('Argument is not a ByteString') + } + + // 3. Return an IDL ByteString value whose length is the + // length of x, and where the value of each element is + // the value of the corresponding element of x. + return x +} + +// https://webidl.spec.whatwg.org/#es-USVString +// TODO: ensure that util.toUSVString follows webidl spec +webidl.converters.USVString = toUSVString + +// https://webidl.spec.whatwg.org/#es-boolean +webidl.converters.boolean = function (V) { + // 1. Let x be the result of computing ToBoolean(V). + const x = Boolean(V) + + // 2. Return the IDL boolean value that is the one that represents + // the same truth value as the ECMAScript Boolean value x. + return x +} + +// https://webidl.spec.whatwg.org/#es-any +webidl.converters.any = function (V) { + return V +} + +// https://webidl.spec.whatwg.org/#es-long-long +webidl.converters['long long'] = function (V, opts) { + // 1. Let x be ? ConvertToInt(V, 64, "signed"). + const x = webidl.util.ConvertToInt(V, 64, 'signed', opts) + + // 2. Return the IDL long long value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#es-unsigned-short +webidl.converters['unsigned short'] = function (V) { + // 1. Let x be ? ConvertToInt(V, 16, "unsigned"). + const x = webidl.util.ConvertToInt(V, 16, 'unsigned') + + // 2. Return the IDL unsigned short value that represents + // the same numeric value as x. + return x +} + +// https://webidl.spec.whatwg.org/#idl-ArrayBuffer +webidl.converters.ArrayBuffer = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have an + // [[ArrayBufferData]] internal slot, then throw a + // TypeError. + // see: https://tc39.es/ecma262/#sec-properties-of-the-arraybuffer-instances + // see: https://tc39.es/ecma262/#sec-properties-of-the-sharedarraybuffer-instances + if ( + webidl.util.Type(V) !== 'Object' || + !types.isAnyArrayBuffer(V) + ) { + webidl.errors.conversionFailed({ + prefix: `${V}`, + argument: `${V}`, + types: ['ArrayBuffer'] + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V) is true, then throw a + // TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V) is true, then throw a + // TypeError. + // Note: resizable ArrayBuffers are currently a proposal. + + // 4. Return the IDL ArrayBuffer value that is a + // reference to the same object as V. + return V +} + +webidl.converters.TypedArray = function (V, T, opts = {}) { + // 1. Let T be the IDL type V is being converted to. + + // 2. If Type(V) is not Object, or V does not have a + // [[TypedArrayName]] internal slot with a value + // equal to T’s name, then throw a TypeError. + if ( + webidl.util.Type(V) !== 'Object' || + !types.isTypedArray(V) || + V.constructor.name !== T.name + ) { + webidl.errors.conversionFailed({ + prefix: `${T.name}`, + argument: `${V}`, + types: [T.name] + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 4. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable array buffers are currently a proposal + + // 5. Return the IDL value of type T that is a reference + // to the same object as V. + return V +} + +webidl.converters.DataView = function (V, opts = {}) { + // 1. If Type(V) is not Object, or V does not have a + // [[DataView]] internal slot, then throw a TypeError. + if (webidl.util.Type(V) !== 'Object' || !types.isDataView(V)) { + webidl.errors.exception({ + header: 'DataView', + message: 'Object is not a DataView.' + }) + } + + // 2. If the conversion is not to an IDL type associated + // with the [AllowShared] extended attribute, and + // IsSharedArrayBuffer(V.[[ViewedArrayBuffer]]) is true, + // then throw a TypeError. + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + webidl.errors.exception({ + header: 'ArrayBuffer', + message: 'SharedArrayBuffer is not allowed.' + }) + } + + // 3. If the conversion is not to an IDL type associated + // with the [AllowResizable] extended attribute, and + // IsResizableArrayBuffer(V.[[ViewedArrayBuffer]]) is + // true, then throw a TypeError. + // Note: resizable ArrayBuffers are currently a proposal + + // 4. Return the IDL DataView value that is a reference + // to the same object as V. + return V +} + +// https://webidl.spec.whatwg.org/#BufferSource +webidl.converters.BufferSource = function (V, opts = {}) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, opts) + } + + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor) + } + + if (types.isDataView(V)) { + return webidl.converters.DataView(V, opts) + } + + throw new TypeError(`Could not convert ${V} to a BufferSource.`) +} + +webidl.converters['sequence'] = webidl.sequenceConverter( + webidl.converters.ByteString +) + +webidl.converters['sequence>'] = webidl.sequenceConverter( + webidl.converters['sequence'] +) + +webidl.converters['record'] = webidl.recordConverter( + webidl.converters.ByteString, + webidl.converters.ByteString +) + +module.exports = { + webidl +} diff --git a/deps/undici/src/lib/handler/redirect.js b/deps/undici/src/lib/handler/redirect.js index 32f74ffa3810cc..998a8c2352b9a4 100644 --- a/deps/undici/src/lib/handler/redirect.js +++ b/deps/undici/src/lib/handler/redirect.js @@ -99,7 +99,7 @@ class RedirectHandler { return this.handler.onHeaders(statusCode, headers, resume, statusText) } - const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin)) + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))) const path = search ? `${pathname}${search}` : pathname // Remove headers referring to the original URL. diff --git a/deps/undici/src/lib/proxy-agent.js b/deps/undici/src/lib/proxy-agent.js index 8799650e62d30e..bfc75d796ed677 100644 --- a/deps/undici/src/lib/proxy-agent.js +++ b/deps/undici/src/lib/proxy-agent.js @@ -1,10 +1,10 @@ 'use strict' -const { kProxy, kClose, kDestroy } = require('./core/symbols') +const { kClose, kDestroy } = require('./core/symbols') const Client = require('./agent') const Agent = require('./agent') const DispatcherBase = require('./dispatcher-base') -const { InvalidArgumentError } = require('./core/errors') +const { InvalidArgumentError, RequestAbortedError } = require('./core/errors') const buildConnector = require('./core/connect') const kAgent = Symbol('proxy agent') @@ -14,10 +14,22 @@ const kRequestTls = Symbol('request tls settings') const kProxyTls = Symbol('proxy tls settings') const kConnectEndpoint = Symbol('connect endpoint function') +function defaultProtocolPort (protocol) { + return protocol === 'https:' ? 443 : 80 +} + class ProxyAgent extends DispatcherBase { constructor (opts) { super(opts) - this[kProxy] = buildProxyOptions(opts) + + if (typeof opts === 'string') { + opts = { uri: opts } + } + + if (!opts || !opts.uri) { + throw new InvalidArgumentError('Proxy opts.uri is mandatory') + } + this[kRequestTls] = opts.requestTls this[kProxyTls] = opts.proxyTls this[kProxyHeaders] = {} @@ -26,10 +38,49 @@ class ProxyAgent extends DispatcherBase { this[kProxyHeaders]['proxy-authorization'] = `Basic ${opts.auth}` } + const { origin, port } = new URL(opts.uri) + const connect = buildConnector({ ...opts.proxyTls }) this[kConnectEndpoint] = buildConnector({ ...opts.requestTls }) this[kClient] = new Client({ origin: opts.origin, connect }) - this[kAgent] = new Agent({ ...opts, connect: this.connectTunnel.bind(this) }) + this[kAgent] = new Agent({ + ...opts, + connect: async (opts, callback) => { + let requestedHost = opts.host + if (!opts.port) { + requestedHost += `:${defaultProtocolPort(opts.protocol)}` + } + try { + const { socket, statusCode } = await this[kClient].connect({ + origin, + port, + path: requestedHost, + signal: opts.signal, + headers: { + ...this[kProxyHeaders], + host: opts.host + } + }) + if (statusCode !== 200) { + socket.on('error', () => {}).destroy() + callback(new RequestAbortedError('Proxy response !== 200 when HTTP Tunneling')) + } + if (opts.protocol !== 'https:') { + callback(null, socket) + return + } + let servername + if (this[kRequestTls]) { + servername = this[kRequestTls].servername + } else { + servername = opts.servername + } + this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) + } catch (err) { + callback(err) + } + } + }) } dispatch (opts, handler) { @@ -48,35 +99,6 @@ class ProxyAgent extends DispatcherBase { ) } - async connectTunnel (opts, callback) { - try { - const { socket } = await this[kClient].connect({ - origin: this[kProxy].origin, - port: this[kProxy].port, - path: opts.host, - signal: opts.signal, - headers: { - ...this[kProxyHeaders], - host: opts.host - }, - httpTunnel: true - }) - if (opts.protocol !== 'https:') { - callback(null, socket) - return - } - let servername - if (this[kRequestTls]) { - servername = this[kRequestTls].servername - } else { - servername = opts.servername - } - this[kConnectEndpoint]({ ...opts, servername, httpSocket: socket }, callback) - } catch (err) { - callback(err) - } - } - async [kClose] () { await this[kAgent].close() await this[kClient].close() @@ -88,18 +110,6 @@ class ProxyAgent extends DispatcherBase { } } -function buildProxyOptions (opts) { - if (typeof opts === 'string') { - opts = { uri: opts } - } - - if (!opts || !opts.uri) { - throw new InvalidArgumentError('Proxy opts.uri is mandatory') - } - - return new URL(opts.uri) -} - /** * @param {string[] | Record} headers * @returns {Record} diff --git a/deps/undici/src/package.json b/deps/undici/src/package.json index dd029b6cfb0e06..2bfb936dbe2ba6 100644 --- a/deps/undici/src/package.json +++ b/deps/undici/src/package.json @@ -1,6 +1,6 @@ { "name": "undici", - "version": "5.5.1", + "version": "5.6.1", "description": "An HTTP/1.1 client, written from scratch for Node.js", "homepage": "https://undici.nodejs.org", "bugs": { @@ -48,7 +48,7 @@ "lint:fix": "standard --fix | snazzy", "test": "npm run test:tap && npm run test:node-fetch && npm run test:fetch && npm run test:jest && tsd", "test:node-fetch": "node scripts/verifyVersion.js 16 || mocha test/node-fetch", - "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js)", + "test:fetch": "node scripts/verifyVersion.js 16 || (npm run build:node && tap test/fetch/*.js && tap test/webidl/*.js)", "test:jest": "jest", "test:tap": "tap test/*.js test/diagnostics-channel/*.js", "test:tdd": "tap test/*.js test/diagnostics-channel/*.js -w", @@ -93,7 +93,7 @@ "standard": "^17.0.0", "table": "^6.8.0", "tap": "^16.1.0", - "tsd": "^0.20.0", + "tsd": "^0.22.0", "wait-on": "^6.0.0" }, "engines": { diff --git a/deps/undici/src/types/diagnostics-channel.d.ts b/deps/undici/src/types/diagnostics-channel.d.ts index 8bb1926506e303..c6131482280dc6 100644 --- a/deps/undici/src/types/diagnostics-channel.d.ts +++ b/deps/undici/src/types/diagnostics-channel.d.ts @@ -1,4 +1,5 @@ import { Socket } from "net"; +import { URL } from "url"; import { connector } from "./connector"; import { HttpMethod } from "./dispatcher"; diff --git a/deps/undici/src/types/errors.d.ts b/deps/undici/src/types/errors.d.ts index 31997b05020490..ab0ecc801b4d5a 100644 --- a/deps/undici/src/types/errors.d.ts +++ b/deps/undici/src/types/errors.d.ts @@ -1,3 +1,5 @@ +import {IncomingHttpHeaders} from "http"; + export = Errors import { SocketInfo } from './client' @@ -16,6 +18,15 @@ declare namespace Errors { code: 'UND_ERR_BODY_TIMEOUT'; } + export class ResponseStatusCodeError extends UndiciError { + name: 'ResponseStatusCodeError'; + code: 'UND_ERR_RESPONSE_STATUS_CODE'; + body: null | Record | string + status: number + statusCode: number + headers: IncomingHttpHeaders | string[] | null; + } + /** A socket exceeds the `socketTimeout` option. */ export class SocketTimeoutError extends UndiciError { name: 'SocketTimeoutError'; diff --git a/deps/undici/src/types/file.d.ts b/deps/undici/src/types/file.d.ts index 93695baa22f21c..c695b7ab0baf87 100644 --- a/deps/undici/src/types/file.d.ts +++ b/deps/undici/src/types/file.d.ts @@ -1,9 +1,14 @@ // Based on https://github.com/octet-stream/form-data/blob/2d0f0dc371517444ce1f22cdde13f51995d0953a/lib/File.ts (MIT) /// -import { Blob, BlobOptions } from 'buffer' +import { Blob } from 'buffer' -export interface FileOptions extends BlobOptions { +export interface BlobPropertyBag { + type?: string + endings?: 'native' | 'transparent' +} + +export interface FilePropertyBag extends BlobPropertyBag { /** * The last modified date of the file as the number of milliseconds since the Unix epoch (January 1, 1970 at midnight). Files without a known last modified date return the current date. */ @@ -18,7 +23,7 @@ export declare class File extends Blob { * @param fileName The name of the file. * @param options An options object containing optional attributes for the file. */ - constructor(fileBits: ReadonlyArray, fileName: string, options?: FileOptions) + constructor(fileBits: ReadonlyArray, fileName: string, options?: FilePropertyBag) /** * Name of the file referenced by the File object. diff --git a/deps/undici/undici.js b/deps/undici/undici.js index 5716d965a9618b..860996c629ecb8 100644 --- a/deps/undici/undici.js +++ b/deps/undici/undici.js @@ -14,13 +14,6 @@ var __publicField = (obj, key, value) => { var require_errors = __commonJS({ "lib/core/errors.js"(exports2, module2) { "use strict"; - var AbortError = class extends Error { - constructor() { - super("The operation was aborted"); - this.code = "ABORT_ERR"; - this.name = "AbortError"; - } - }; var UndiciError = class extends Error { constructor(message) { super(message); @@ -65,12 +58,13 @@ var require_errors = __commonJS({ } }; var ResponseStatusCodeError = class extends UndiciError { - constructor(message, statusCode, headers) { + constructor(message, statusCode, headers, body) { super(message); Error.captureStackTrace(this, ResponseStatusCodeError); this.name = "ResponseStatusCodeError"; this.message = message || "Response Status Code Error"; this.code = "UND_ERR_RESPONSE_STATUS_CODE"; + this.body = body; this.status = statusCode; this.statusCode = statusCode; this.headers = headers; @@ -186,7 +180,6 @@ var require_errors = __commonJS({ } }; module2.exports = { - AbortError, HTTPParserError, UndiciError, HeadersTimeoutError, @@ -1021,7 +1014,15 @@ var require_constants = __commonJS({ "xslt", "" ]; + var DOMException = globalThis.DOMException ?? (() => { + try { + atob("~"); + } catch (err) { + return Object.getPrototypeOf(err).constructor; + } + })(); module2.exports = { + DOMException, subresource, forbiddenMethods, requestBodyHeader, @@ -1053,18 +1054,344 @@ var require_symbols2 = __commonJS({ } }); +// lib/fetch/webidl.js +var require_webidl = __commonJS({ + "lib/fetch/webidl.js"(exports2, module2) { + "use strict"; + var { toUSVString, types } = require("util"); + var webidl = {}; + webidl.converters = {}; + webidl.util = {}; + webidl.errors = {}; + webidl.errors.exception = function(message) { + throw new TypeError(`${message.header}: ${message.message}`); + }; + webidl.errors.conversionFailed = function(context) { + const plural = context.types.length === 1 ? "" : " one of"; + const message = `${context.argument} could not be converted to${plural}: ${context.types.join(", ")}.`; + return webidl.errors.exception({ + header: context.prefix, + message + }); + }; + webidl.errors.invalidArgument = function(context) { + return webidl.errors.exception({ + header: context.prefix, + message: `"${context.value}" is an invalid ${context.type}.` + }); + }; + webidl.util.Type = function(V) { + switch (typeof V) { + case "undefined": + return "Undefined"; + case "boolean": + return "Boolean"; + case "string": + return "String"; + case "symbol": + return "Symbol"; + case "number": + return "Number"; + case "bigint": + return "BigInt"; + case "function": + case "object": { + if (V === null) { + return "Null"; + } + return "Object"; + } + } + }; + webidl.util.ConvertToInt = function(V, bitLength, signedness, opts = {}) { + let upperBound; + let lowerBound; + if (bitLength === 64) { + upperBound = Math.pow(2, 53) - 1; + if (signedness === "unsigned") { + lowerBound = 0; + } else { + lowerBound = Math.pow(-2, 53) + 1; + } + } else if (signedness === "unsigned") { + lowerBound = 0; + upperBound = Math.pow(2, bitLength) - 1; + } else { + lowerBound = Math.pow(-2, bitLength) - 1; + upperBound = Math.pow(2, bitLength - 1) - 1; + } + let x = Number(V); + if (Object.is(-0, x)) { + x = 0; + } + if (opts.enforceRange === true) { + if (Number.isNaN(x) || x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY) { + webidl.errors.exception({ + header: "Integer conversion", + message: `Could not convert ${V} to an integer.` + }); + } + x = webidl.util.IntegerPart(x); + if (x < lowerBound || x > upperBound) { + webidl.errors.exception({ + header: "Integer conversion", + message: `Value must be between ${lowerBound}-${upperBound}, got ${x}.` + }); + } + return x; + } + if (!Number.isNaN(x) && opts.clamp === true) { + x = Math.min(Math.max(x, lowerBound), upperBound); + if (Math.floor(x) % 2 === 0) { + x = Math.floor(x); + } else { + x = Math.ceil(x); + } + return x; + } + if (Number.isNaN(x) || Object.is(0, x) || x === Number.POSITIVE_INFINITY || x === Number.NEGATIVE_INFINITY) { + return 0; + } + x = webidl.util.IntegerPart(x); + x = x % Math.pow(2, bitLength); + if (signedness === "signed" && x >= Math.pow(2, bitLength) - 1) { + return x - Math.pow(2, bitLength); + } + return x; + }; + webidl.util.IntegerPart = function(n) { + const r = Math.floor(Math.abs(n)); + if (n < 0) { + return -1 * r; + } + return r; + }; + webidl.sequenceConverter = function(converter) { + return (V) => { + if (webidl.util.Type(V) !== "Object") { + webidl.errors.exception({ + header: "Sequence", + message: `Value of type ${webidl.util.Type(V)} is not an Object.` + }); + } + const method = V?.[Symbol.iterator]?.(); + const seq = []; + if (method === void 0 || typeof method.next !== "function") { + webidl.errors.exception({ + header: "Sequence", + message: "Object is not an iterator." + }); + } + while (true) { + const { done, value } = method.next(); + if (done) { + break; + } + seq.push(converter(value)); + } + return seq; + }; + }; + webidl.recordConverter = function(keyConverter, valueConverter) { + return (V) => { + const record = {}; + const type = webidl.util.Type(V); + if (type === "Undefined" || type === "Null") { + return record; + } + if (type !== "Object") { + webidl.errors.exception({ + header: "Record", + message: `Expected ${V} to be an Object type.` + }); + } + for (let [key, value] of Object.entries(V)) { + key = keyConverter(key); + value = valueConverter(value); + record[key] = value; + } + return record; + }; + }; + webidl.interfaceConverter = function(i) { + return (V, opts = {}) => { + if (opts.strict !== false && !(V instanceof i)) { + webidl.errors.exception({ + header: i.name, + message: `Expected ${V} to be an instance of ${i.name}.` + }); + } + return V; + }; + }; + webidl.dictionaryConverter = function(converters) { + return (dictionary) => { + const type = webidl.util.Type(dictionary); + const dict = {}; + if (type !== "Null" && type !== "Undefined" && type !== "Object") { + webidl.errors.exception({ + header: "Dictionary", + message: `Expected ${dictionary} to be one of: Null, Undefined, Object.` + }); + } + for (const options of converters) { + const { key, defaultValue, required, converter } = options; + if (required === true) { + if (!Object.hasOwn(dictionary, key)) { + webidl.errors.exception({ + header: "Dictionary", + message: `Missing required key "${key}".` + }); + } + } + let value = dictionary[key]; + const hasDefault = Object.hasOwn(options, "defaultValue"); + if (hasDefault && value !== null) { + value = value ?? defaultValue; + } + if (required || hasDefault || value !== void 0) { + value = converter(value); + if (options.allowedValues && !options.allowedValues.includes(value)) { + webidl.errors.exception({ + header: "Dictionary", + message: `${value} is not an accepted type. Expected one of ${options.allowedValues.join(", ")}.` + }); + } + dict[key] = value; + } + } + return dict; + }; + }; + webidl.nullableConverter = function(converter) { + return (V) => { + if (V === null) { + return V; + } + return converter(V); + }; + }; + webidl.converters.DOMString = function(V, opts = {}) { + if (V === null && opts.legacyNullToEmptyString) { + return ""; + } + if (typeof V === "symbol") { + throw new TypeError("Could not convert argument of type symbol to string."); + } + return String(V); + }; + var isNotLatin1 = /[^\u0000-\u00ff]/; + webidl.converters.ByteString = function(V) { + const x = webidl.converters.DOMString(V); + if (isNotLatin1.test(x)) { + throw new TypeError("Argument is not a ByteString"); + } + return x; + }; + webidl.converters.USVString = toUSVString; + webidl.converters.boolean = function(V) { + const x = Boolean(V); + return x; + }; + webidl.converters.any = function(V) { + return V; + }; + webidl.converters["long long"] = function(V, opts) { + const x = webidl.util.ConvertToInt(V, 64, "signed", opts); + return x; + }; + webidl.converters["unsigned short"] = function(V) { + const x = webidl.util.ConvertToInt(V, 16, "unsigned"); + return x; + }; + webidl.converters.ArrayBuffer = function(V, opts = {}) { + if (webidl.util.Type(V) !== "Object" || !types.isAnyArrayBuffer(V)) { + webidl.errors.conversionFailed({ + prefix: `${V}`, + argument: `${V}`, + types: ["ArrayBuffer"] + }); + } + if (opts.allowShared === false && types.isSharedArrayBuffer(V)) { + webidl.errors.exception({ + header: "ArrayBuffer", + message: "SharedArrayBuffer is not allowed." + }); + } + return V; + }; + webidl.converters.TypedArray = function(V, T, opts = {}) { + if (webidl.util.Type(V) !== "Object" || !types.isTypedArray(V) || V.constructor.name !== T.name) { + webidl.errors.conversionFailed({ + prefix: `${T.name}`, + argument: `${V}`, + types: [T.name] + }); + } + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + webidl.errors.exception({ + header: "ArrayBuffer", + message: "SharedArrayBuffer is not allowed." + }); + } + return V; + }; + webidl.converters.DataView = function(V, opts = {}) { + if (webidl.util.Type(V) !== "Object" || !types.isDataView(V)) { + webidl.errors.exception({ + header: "DataView", + message: "Object is not a DataView." + }); + } + if (opts.allowShared === false && types.isSharedArrayBuffer(V.buffer)) { + webidl.errors.exception({ + header: "ArrayBuffer", + message: "SharedArrayBuffer is not allowed." + }); + } + return V; + }; + webidl.converters.BufferSource = function(V, opts = {}) { + if (types.isAnyArrayBuffer(V)) { + return webidl.converters.ArrayBuffer(V, opts); + } + if (types.isTypedArray(V)) { + return webidl.converters.TypedArray(V, V.constructor); + } + if (types.isDataView(V)) { + return webidl.converters.DataView(V, opts); + } + throw new TypeError(`Could not convert ${V} to a BufferSource.`); + }; + webidl.converters["sequence"] = webidl.sequenceConverter(webidl.converters.ByteString); + webidl.converters["sequence>"] = webidl.sequenceConverter(webidl.converters["sequence"]); + webidl.converters["record"] = webidl.recordConverter(webidl.converters.ByteString, webidl.converters.ByteString); + module2.exports = { + webidl + }; + } +}); + // lib/fetch/file.js var require_file = __commonJS({ "lib/fetch/file.js"(exports2, module2) { "use strict"; var { Blob } = require("buffer"); + var { types } = require("util"); var { kState } = require_symbols2(); + var { isBlobLike } = require_util2(); + var { webidl } = require_webidl(); var File = class extends Blob { constructor(fileBits, fileName, options = {}) { + if (arguments.length < 2) { + throw new TypeError("2 arguments required"); + } + fileBits = webidl.converters["sequence"](fileBits); + fileName = webidl.converters.USVString(fileName); + options = webidl.converters.FilePropertyBag(options); const n = fileName; - const t = options.type; - const d = options.lastModified ?? Date.now(); - super(fileBits, { type: t }); + const d = options.lastModified; + super(processBlobParts(fileBits, options), { type: options.type }); this[kState] = { name: n, lastModified: d @@ -1150,7 +1477,73 @@ var require_file = __commonJS({ return "File"; } }; - module2.exports = { File: globalThis.File ?? File, FileLike }; + webidl.converters.Blob = webidl.interfaceConverter(Blob); + webidl.converters.BlobPart = function(V, opts) { + if (webidl.util.Type(V) === "Object") { + if (isBlobLike(V)) { + return webidl.converters.Blob(V, { strict: false }); + } + return webidl.converters.BufferSource(V, opts); + } else { + return webidl.converters.USVString(V, opts); + } + }; + webidl.converters["sequence"] = webidl.sequenceConverter(webidl.converters.BlobPart); + webidl.converters.FilePropertyBag = webidl.dictionaryConverter([ + { + key: "lastModified", + converter: webidl.converters["long long"], + get defaultValue() { + return Date.now(); + } + }, + { + key: "type", + converter: webidl.converters.DOMString, + defaultValue: "" + }, + { + key: "endings", + converter: (value) => { + value = webidl.converters.DOMString(value); + value = value.toLowerCase(); + if (value !== "native") { + value = "transparent"; + } + return value; + }, + defaultValue: "transparent" + } + ]); + function processBlobParts(parts, options) { + const bytes = []; + for (const element of parts) { + if (typeof element === "string") { + let s = element; + if (options.endings === "native") { + s = convertLineEndingsNative(s); + } + bytes.push(new TextEncoder().encode(s)); + } else if (types.isAnyArrayBuffer(element) || types.isTypedArray(element)) { + if (!element.buffer) { + bytes.push(new Uint8Array(element)); + } else { + bytes.push(element.buffer); + } + } else if (isBlobLike(element)) { + bytes.push(element); + } + } + return bytes; + } + function convertLineEndingsNative(s) { + let nativeLineEnding = "\n"; + if (process.platform === "win32") { + nativeLineEnding = "\r\n"; + } + return s.replace(/\r?\n/g, nativeLineEnding); + } + module2.exports = { File, FileLike }; } }); @@ -1159,7 +1552,7 @@ var require_util2 = __commonJS({ "lib/fetch/util.js"(exports2, module2) { "use strict"; var { redirectStatus } = require_constants(); - var { performance } = require("perf_hooks"); + var { performance: performance2 } = require("perf_hooks"); var { isBlobLike, toUSVString, ReadableStreamFrom } = require_util(); var assert = require("assert"); var File; @@ -1301,6 +1694,26 @@ var require_util2 = __commonJS({ } return true; } + function isValidHeaderName(potentialValue) { + if (potentialValue.length === 0) { + return false; + } + for (const char of potentialValue) { + if (!isValidHTTPToken(char)) { + return false; + } + } + return true; + } + function isValidHeaderValue(potentialValue) { + if (potentialValue.startsWith(" ") || potentialValue.startsWith(" ") || potentialValue.endsWith(" ") || potentialValue.endsWith(" ")) { + return false; + } + if (potentialValue.includes("\0") || potentialValue.includes("\r") || potentialValue.includes("\n")) { + return false; + } + return true; + } function setRequestReferrerPolicyOnRedirect(request, actualResponse) { const policy = ""; if (policy !== "") { @@ -1352,7 +1765,7 @@ var require_util2 = __commonJS({ } } function coarsenedSharedCurrentTime(crossOriginIsolatedCapability) { - return performance.now(); + return performance2.now(); } function createOpaqueTimingInfo(timingInfo) { return { @@ -1459,7 +1872,9 @@ var require_util2 = __commonJS({ sameOrigin, normalizeMethod, serializeJavascriptValueToJSONString, - makeIterator + makeIterator, + isValidHeaderName, + isValidHeaderValue }; } }); @@ -1471,38 +1886,43 @@ var require_formdata = __commonJS({ var { isBlobLike, isFileLike, toUSVString, makeIterator } = require_util2(); var { kState } = require_symbols2(); var { File, FileLike } = require_file(); + var { webidl } = require_webidl(); var { Blob } = require("buffer"); var _FormData = class { - constructor(...args) { - if (args.length > 0 && !(args[0]?.constructor?.name === "HTMLFormElement")) { - throw new TypeError("Failed to construct 'FormData': parameter 1 is not of type 'HTMLFormElement'"); + constructor(form) { + if (arguments.length > 0 && form != null) { + webidl.errors.conversionFailed({ + prefix: "FormData constructor", + argument: "Argument 1", + types: ["null"] + }); } this[kState] = []; } - append(...args) { + append(name, value, filename = void 0) { if (!(this instanceof _FormData)) { throw new TypeError("Illegal invocation"); } - if (args.length < 2) { - throw new TypeError(`Failed to execute 'append' on 'FormData': 2 arguments required, but only ${args.length} present.`); + if (arguments.length < 2) { + throw new TypeError(`Failed to execute 'append' on 'FormData': 2 arguments required, but only ${arguments.length} present.`); } - if (args.length === 3 && !isBlobLike(args[1])) { + if (arguments.length === 3 && !isBlobLike(value)) { throw new TypeError("Failed to execute 'append' on 'FormData': parameter 2 is not of type 'Blob'"); } - const name = toUSVString(args[0]); - const filename = args.length === 3 ? toUSVString(args[2]) : void 0; - const value = isBlobLike(args[1]) ? args[1] : toUSVString(args[1]); + name = webidl.converters.USVString(name); + value = isBlobLike(value) ? webidl.converters.Blob(value, { strict: false }) : webidl.converters.USVString(value); + filename = arguments.length === 3 ? webidl.converters.USVString(filename) : void 0; const entry = makeEntry(name, value, filename); this[kState].push(entry); } - delete(...args) { + delete(name) { if (!(this instanceof _FormData)) { throw new TypeError("Illegal invocation"); } - if (args.length < 1) { - throw new TypeError(`Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${args.length} present.`); + if (arguments.length < 1) { + throw new TypeError(`Failed to execute 'delete' on 'FormData': 1 arguments required, but only ${arguments.length} present.`); } - const name = toUSVString(args[0]); + name = webidl.converters.USVString(name); const next = []; for (const entry of this[kState]) { if (entry.name !== name) { @@ -1511,53 +1931,53 @@ var require_formdata = __commonJS({ } this[kState] = next; } - get(...args) { + get(name) { if (!(this instanceof _FormData)) { throw new TypeError("Illegal invocation"); } - if (args.length < 1) { - throw new TypeError(`Failed to execute 'get' on 'FormData': 1 arguments required, but only ${args.length} present.`); + if (arguments.length < 1) { + throw new TypeError(`Failed to execute 'get' on 'FormData': 1 arguments required, but only ${arguments.length} present.`); } - const name = toUSVString(args[0]); + name = webidl.converters.USVString(name); const idx = this[kState].findIndex((entry) => entry.name === name); if (idx === -1) { return null; } return this[kState][idx].value; } - getAll(...args) { + getAll(name) { if (!(this instanceof _FormData)) { throw new TypeError("Illegal invocation"); } - if (args.length < 1) { - throw new TypeError(`Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${args.length} present.`); + if (arguments.length < 1) { + throw new TypeError(`Failed to execute 'getAll' on 'FormData': 1 arguments required, but only ${arguments.length} present.`); } - const name = toUSVString(args[0]); + name = webidl.converters.USVString(name); return this[kState].filter((entry) => entry.name === name).map((entry) => entry.value); } - has(...args) { + has(name) { if (!(this instanceof _FormData)) { throw new TypeError("Illegal invocation"); } - if (args.length < 1) { - throw new TypeError(`Failed to execute 'has' on 'FormData': 1 arguments required, but only ${args.length} present.`); + if (arguments.length < 1) { + throw new TypeError(`Failed to execute 'has' on 'FormData': 1 arguments required, but only ${arguments.length} present.`); } - const name = toUSVString(args[0]); + name = webidl.converters.USVString(name); return this[kState].findIndex((entry) => entry.name === name) !== -1; } - set(...args) { + set(name, value, filename = void 0) { if (!(this instanceof _FormData)) { throw new TypeError("Illegal invocation"); } - if (args.length < 2) { - throw new TypeError(`Failed to execute 'set' on 'FormData': 2 arguments required, but only ${args.length} present.`); + if (arguments.length < 2) { + throw new TypeError(`Failed to execute 'set' on 'FormData': 2 arguments required, but only ${arguments.length} present.`); } - if (args.length === 3 && !isBlobLike(args[1])) { + if (arguments.length === 3 && !isBlobLike(value)) { throw new TypeError("Failed to execute 'set' on 'FormData': parameter 2 is not of type 'Blob'"); } - const name = toUSVString(args[0]); - const filename = args.length === 3 ? toUSVString(args[2]) : void 0; - const value = isBlobLike(args[1]) ? args[1] : toUSVString(args[1]); + name = webidl.converters.USVString(name); + value = isBlobLike(value) ? webidl.converters.Blob(value, { strict: false }) : webidl.converters.USVString(value); + filename = arguments.length === 3 ? toUSVString(filename) : void 0; const entry = makeEntry(name, value, filename); const idx = this[kState].findIndex((entry2) => entry2.name === name); if (idx !== -1) { @@ -1610,19 +2030,18 @@ var require_formdata = __commonJS({ __publicField(FormData, "name", "FormData"); FormData.prototype[Symbol.iterator] = FormData.prototype.entries; function makeEntry(name, value, filename) { - const entry = { - name: null, - value: null - }; - entry.name = name; - if (isBlobLike(value) && !isFileLike(value)) { - value = value instanceof Blob ? new File([value], "blob", value) : new FileLike(value, "blob", value); - } - if (isFileLike(value) && filename != null) { - value = value instanceof File ? new File([value], filename, value) : new FileLike(value, filename, value); + name = Buffer.from(name).toString("utf8"); + if (typeof value === "string") { + value = Buffer.from(value).toString("utf8"); + } else { + if (!isFileLike(value)) { + value = value instanceof Blob ? new File([value], "blob", { type: value.type }) : new FileLike(value, "blob", { type: value.type }); + } + if (filename !== void 0) { + value = value instanceof File ? new File([value], filename, { type: value.type }) : new FileLike(value, filename, { type: value.type }); + } } - entry.value = value; - return entry; + return { name, value }; } function* makeIterable(entries, type) { for (const { name, value } of entries) { @@ -1647,6 +2066,7 @@ var require_body = __commonJS({ var { ReadableStreamFrom, toUSVString, isBlobLike } = require_util2(); var { FormData } = require_formdata(); var { kState } = require_symbols2(); + var { webidl } = require_webidl(); var { Blob } = require("buffer"); var { kBodyUsed } = require_symbols(); var assert = require("assert"); @@ -1783,77 +2203,104 @@ Content-Type: ${value.type || "application/octet-stream"}\r source: body.source }; } - var methods = { - async blob() { - const chunks = []; - if (this[kState].body) { - if (isUint8Array(this[kState].body)) { - chunks.push(this[kState].body); - } else { - const stream = this[kState].body.stream; - if (util.isDisturbed(stream)) { - throw new TypeError("disturbed"); - } - if (stream.locked) { - throw new TypeError("locked"); - } - stream[kBodyUsed] = true; - for await (const chunk of stream) { - chunks.push(chunk); + function bodyMixinMethods(instance) { + const methods = { + async blob() { + if (!(this instanceof instance)) { + throw new TypeError("Illegal invocation"); + } + const chunks = []; + if (this[kState].body) { + if (isUint8Array(this[kState].body)) { + chunks.push(this[kState].body); + } else { + const stream = this[kState].body.stream; + if (util.isDisturbed(stream)) { + throw new TypeError("disturbed"); + } + if (stream.locked) { + throw new TypeError("locked"); + } + stream[kBodyUsed] = true; + for await (const chunk of stream) { + chunks.push(chunk); + } } } - } - return new Blob(chunks, { type: this.headers.get("Content-Type") || "" }); - }, - async arrayBuffer() { - const blob = await this.blob(); - return await blob.arrayBuffer(); - }, - async text() { - const blob = await this.blob(); - return toUSVString(await blob.text()); - }, - async json() { - return JSON.parse(await this.text()); - }, - async formData() { - const contentType = this.headers.get("Content-Type"); - if (/multipart\/form-data/.test(contentType)) { - throw new NotSupportedError("multipart/form-data not supported"); - } else if (/application\/x-www-form-urlencoded/.test(contentType)) { - let entries; - try { - entries = new URLSearchParams(await this.text()); - } catch (err) { - throw Object.assign(new TypeError(), { cause: err }); + return new Blob(chunks, { type: this.headers.get("Content-Type") || "" }); + }, + async arrayBuffer() { + if (!(this instanceof instance)) { + throw new TypeError("Illegal invocation"); } - const formData = new FormData(); - for (const [name, value] of entries) { - formData.append(name, value); + const blob = await this.blob(); + return await blob.arrayBuffer(); + }, + async text() { + if (!(this instanceof instance)) { + throw new TypeError("Illegal invocation"); + } + const blob = await this.blob(); + return toUSVString(await blob.text()); + }, + async json() { + if (!(this instanceof instance)) { + throw new TypeError("Illegal invocation"); + } + return JSON.parse(await this.text()); + }, + async formData() { + if (!(this instanceof instance)) { + throw new TypeError("Illegal invocation"); + } + const contentType = this.headers.get("Content-Type"); + if (/multipart\/form-data/.test(contentType)) { + throw new NotSupportedError("multipart/form-data not supported"); + } else if (/application\/x-www-form-urlencoded/.test(contentType)) { + let entries; + try { + entries = new URLSearchParams(await this.text()); + } catch (err) { + throw Object.assign(new TypeError(), { cause: err }); + } + const formData = new FormData(); + for (const [name, value] of entries) { + formData.append(name, value); + } + return formData; + } else { + webidl.errors.exception({ + header: `${instance.name}.formData`, + value: "Could not parse content as FormData." + }); } - return formData; - } else { - throw new TypeError(); } - } - }; + }; + return methods; + } var properties = { body: { enumerable: true, get() { + if (!this || !this[kState]) { + throw new TypeError("Illegal invocation"); + } return this[kState].body ? this[kState].body.stream : null; } }, bodyUsed: { enumerable: true, get() { + if (!this || !this[kState]) { + throw new TypeError("Illegal invocation"); + } return !!this[kState].body && util.isDisturbed(this[kState].body.stream); } } }; function mixinBody(prototype) { - Object.assign(prototype, methods); - Object.defineProperties(prototype, properties); + Object.assign(prototype.prototype, bodyMixinMethods(prototype)); + Object.defineProperties(prototype.prototype, properties); } module2.exports = { extractBody, @@ -2166,7 +2613,7 @@ var require_redirect = __commonJS({ if (!this.location) { return this.handler.onHeaders(statusCode, headers, resume, statusText); } - const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin)); + const { origin, pathname, search } = util.parseURL(new URL(this.location, this.opts.origin && new URL(this.opts.path, this.opts.origin))); const path = search ? `${pathname}${search}` : pathname; this.opts.headers = cleanRequestHeaders(this.opts.headers, statusCode === 303, this.opts.origin !== origin); this.opts.path = path; @@ -3237,7 +3684,7 @@ var require_client = __commonJS({ this.timeout.refresh(); } } - if (request.method === "CONNECT" && statusCode >= 200 && statusCode < 300) { + if (request.method === "CONNECT") { assert(client[kRunning] === 1); this.upgrade = true; return 2; @@ -3361,10 +3808,8 @@ var require_client = __commonJS({ function onParserTimeout(parser) { const { socket, timeoutType, client } = parser; if (timeoutType === TIMEOUT_HEADERS) { - if (!socket[kWriting]) { - assert(!parser.paused, "cannot be paused while waiting for headers"); - util.destroy(socket, new HeadersTimeoutError()); - } + assert(!parser.paused, "cannot be paused while waiting for headers"); + util.destroy(socket, new HeadersTimeoutError()); } else if (timeoutType === TIMEOUT_BODY) { if (!parser.paused) { util.destroy(socket, new BodyTimeoutError()); @@ -4233,52 +4678,41 @@ var require_global = __commonJS({ var require_headers = __commonJS({ "lib/fetch/headers.js"(exports2, module2) { "use strict"; - var { validateHeaderName, validateHeaderValue } = require("http"); var { kHeadersList } = require_symbols(); var { kGuard } = require_symbols2(); var { kEnumerableProperty } = require_util(); - var { makeIterator } = require_util2(); + var { + makeIterator, + isValidHeaderName, + isValidHeaderValue + } = require_util2(); + var { webidl } = require_webidl(); var kHeadersMap = Symbol("headers map"); var kHeadersSortedMap = Symbol("headers map sorted"); - function normalizeAndValidateHeaderName(name) { - if (name === void 0) { - throw new TypeError(`Header name ${name}`); - } - const normalizedHeaderName = name.toLocaleLowerCase(); - validateHeaderName(normalizedHeaderName); - return normalizedHeaderName; - } - function normalizeAndValidateHeaderValue(name, value) { - if (value === void 0) { - throw new TypeError(value, name); - } - const normalizedHeaderValue = `${value}`.replace(/^[\n\t\r\x20]+|[\n\t\r\x20]+$/g, ""); - validateHeaderValue(name, normalizedHeaderValue); - return normalizedHeaderValue; + function headerValueNormalize(potentialValue) { + return potentialValue.replace(/^[\r\n\t ]+|[\r\n\t ]+$/g, ""); } function fill(headers, object) { - if (object[Symbol.iterator]) { - for (let header of object) { - if (!header[Symbol.iterator]) { - throw new TypeError(); - } - if (typeof header === "string") { - throw new TypeError(); - } - if (!Array.isArray(header)) { - header = [...header]; - } + if (Array.isArray(object)) { + for (const header of object) { if (header.length !== 2) { - throw new TypeError(); + webidl.errors.exception({ + header: "Headers constructor", + message: `expected name/value pair to be length 2, found ${header.length}.` + }); } headers.append(header[0], header[1]); } - } else if (object && typeof object === "object") { - for (const header of Object.entries(object)) { - headers.append(header[0], header[1]); + } else if (typeof object === "object" && object !== null) { + for (const [key, value] of Object.entries(object)) { + headers.append(key, value); } } else { - throw TypeError(); + webidl.errors.conversionFailed({ + prefix: "Headers constructor", + argument: "Argument 1", + types: ["sequence>", "record"] + }); } } var HeadersList = class { @@ -4291,38 +4725,43 @@ var require_headers = __commonJS({ this[kHeadersSortedMap] = null; } } + contains(name) { + name = name.toLowerCase(); + return this[kHeadersMap].has(name); + } clear() { this[kHeadersMap].clear(); this[kHeadersSortedMap] = null; } append(name, value) { this[kHeadersSortedMap] = null; - const normalizedName = normalizeAndValidateHeaderName(name); - const normalizedValue = normalizeAndValidateHeaderValue(name, value); - const exists = this[kHeadersMap].get(normalizedName); + name = name.toLowerCase(); + const exists = this[kHeadersMap].get(name); if (exists) { - this[kHeadersMap].set(normalizedName, `${exists}, ${normalizedValue}`); + this[kHeadersMap].set(name, `${exists}, ${value}`); } else { - this[kHeadersMap].set(normalizedName, `${normalizedValue}`); + this[kHeadersMap].set(name, `${value}`); } } set(name, value) { this[kHeadersSortedMap] = null; - const normalizedName = normalizeAndValidateHeaderName(name); - return this[kHeadersMap].set(normalizedName, value); + return this[kHeadersMap].set(name, value); } delete(name) { this[kHeadersSortedMap] = null; - const normalizedName = normalizeAndValidateHeaderName(name); - return this[kHeadersMap].delete(normalizedName); + name = name.toLowerCase(); + return this[kHeadersMap].delete(name); } get(name) { - const normalizedName = normalizeAndValidateHeaderName(name); - return this[kHeadersMap].get(normalizedName) ?? null; + name = name.toLowerCase(); + if (!this.contains(name)) { + return null; + } + return this[kHeadersMap].get(name) ?? null; } has(name) { - const normalizedName = normalizeAndValidateHeaderName(name); - return this[kHeadersMap].has(normalizedName); + name = name.toLowerCase(); + return this[kHeadersMap].has(name); } keys() { return this[kHeadersMap].keys(); @@ -4338,14 +4777,13 @@ var require_headers = __commonJS({ } }; var Headers = class { - constructor(...args) { - if (args[0] !== void 0 && !(typeof args[0] === "object" && args[0] != null) && !Array.isArray(args[0])) { - throw new TypeError("Failed to construct 'Headers': The provided value is not of type '(record or sequence>"); - } - const init = args.length >= 1 ? args[0] ?? {} : {}; + constructor(init = void 0) { this[kHeadersList] = new HeadersList(); this[kGuard] = "none"; - fill(this, init); + if (init !== void 0) { + init = webidl.converters.HeadersInit(init); + fill(this, init); + } } get [Symbol.toStringTag]() { return this.constructor.name; @@ -4357,11 +4795,27 @@ var require_headers = __commonJS({ if (arguments.length < 2) { throw new TypeError(`Failed to execute 'append' on 'Headers': 2 arguments required, but only ${arguments.length} present.`); } + name = webidl.converters.ByteString(name); + value = webidl.converters.ByteString(value); + value = headerValueNormalize(value); + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: "Headers.append", + value: name, + type: "header name" + }); + } else if (!isValidHeaderValue(value)) { + webidl.errors.invalidArgument({ + prefix: "Headers.append", + value, + type: "header value" + }); + } if (this[kGuard] === "immutable") { throw new TypeError("immutable"); } else if (this[kGuard] === "request-no-cors") { } - return this[kHeadersList].append(String(name), String(value)); + return this[kHeadersList].append(name, value); } delete(name) { if (!(this instanceof Headers)) { @@ -4370,11 +4824,22 @@ var require_headers = __commonJS({ if (arguments.length < 1) { throw new TypeError(`Failed to execute 'delete' on 'Headers': 1 argument required, but only ${arguments.length} present.`); } + name = webidl.converters.ByteString(name); + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: "Headers.delete", + value: name, + type: "header name" + }); + } if (this[kGuard] === "immutable") { throw new TypeError("immutable"); } else if (this[kGuard] === "request-no-cors") { } - return this[kHeadersList].delete(String(name)); + if (!this[kHeadersList].contains(name)) { + return; + } + return this[kHeadersList].delete(name); } get(name) { if (!(this instanceof Headers)) { @@ -4383,7 +4848,15 @@ var require_headers = __commonJS({ if (arguments.length < 1) { throw new TypeError(`Failed to execute 'get' on 'Headers': 1 argument required, but only ${arguments.length} present.`); } - return this[kHeadersList].get(String(name)); + name = webidl.converters.ByteString(name); + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: "Headers.get", + value: name, + type: "header name" + }); + } + return this[kHeadersList].get(name); } has(name) { if (!(this instanceof Headers)) { @@ -4392,7 +4865,15 @@ var require_headers = __commonJS({ if (arguments.length < 1) { throw new TypeError(`Failed to execute 'has' on 'Headers': 1 argument required, but only ${arguments.length} present.`); } - return this[kHeadersList].has(String(name)); + name = webidl.converters.ByteString(name); + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: "Headers.has", + value: name, + type: "header name" + }); + } + return this[kHeadersList].contains(name); } set(name, value) { if (!(this instanceof Headers)) { @@ -4401,11 +4882,27 @@ var require_headers = __commonJS({ if (arguments.length < 2) { throw new TypeError(`Failed to execute 'set' on 'Headers': 2 arguments required, but only ${arguments.length} present.`); } + name = webidl.converters.ByteString(name); + value = webidl.converters.ByteString(value); + value = headerValueNormalize(value); + if (!isValidHeaderName(name)) { + webidl.errors.invalidArgument({ + prefix: "Headers.set", + value: name, + type: "header name" + }); + } else if (!isValidHeaderValue(value)) { + webidl.errors.invalidArgument({ + prefix: "Headers.set", + value, + type: "header value" + }); + } if (this[kGuard] === "immutable") { throw new TypeError("immutable"); } else if (this[kGuard] === "request-no-cors") { } - return this[kHeadersList].set(String(name), String(value)); + return this[kHeadersList].set(name, value); } get [kHeadersSortedMap]() { this[kHeadersList][kHeadersSortedMap] ??= new Map([...this[kHeadersList]].sort((a, b) => a[0] < b[0] ? -1 : 1)); @@ -4462,12 +4959,23 @@ var require_headers = __commonJS({ entries: kEnumerableProperty, forEach: kEnumerableProperty }); + webidl.converters.HeadersInit = function(V) { + if (webidl.util.Type(V) === "Object") { + if (V[Symbol.iterator]) { + return webidl.converters["sequence>"](V); + } + return webidl.converters["record"](V); + } + webidl.errors.conversionFailed({ + prefix: "Headers constructor", + argument: "Argument 1", + types: ["sequence>", "record"] + }); + }; module2.exports = { fill, Headers, - HeadersList, - normalizeAndValidateHeaderName, - normalizeAndValidateHeaderValue + HeadersList }; } }); @@ -4477,18 +4985,29 @@ var require_response = __commonJS({ "lib/fetch/response.js"(exports2, module2) { "use strict"; var { Headers, HeadersList, fill } = require_headers(); - var { AbortError } = require_errors(); var { extractBody, cloneBody, mixinBody } = require_body(); var util = require_util(); var { kEnumerableProperty } = util; - var { responseURL, isValidReasonPhrase, toUSVString, isCancelled, isAborted, serializeJavascriptValueToJSONString } = require_util2(); + var { + responseURL, + isValidReasonPhrase, + isCancelled, + isAborted, + isBlobLike, + serializeJavascriptValueToJSONString + } = require_util2(); var { redirectStatus, - nullBodyStatus + nullBodyStatus, + DOMException } = require_constants(); var { kState, kHeaders, kGuard, kRealm } = require_symbols2(); + var { webidl } = require_webidl(); + var { FormData } = require_formdata(); var { kHeadersList } = require_symbols(); var assert = require("assert"); + var { types } = require("util"); + var ReadableStream = globalThis.ReadableStream || require("stream/web").ReadableStream; var Response = class { static error() { const relevantRealm = { settingsObject: {} }; @@ -4504,15 +5023,9 @@ var require_response = __commonJS({ if (arguments.length === 0) { throw new TypeError("Failed to execute 'json' on 'Response': 1 argument required, but 0 present."); } - if (init === null || typeof init !== "object") { - throw new TypeError(`Failed to execute 'json' on 'Response': init must be a RequestInit, found ${typeof init}.`); + if (init !== null) { + init = webidl.converters.ResponseInit(init); } - init = { - status: 200, - statusText: "", - headers: new HeadersList(), - ...init - }; const bytes = new TextEncoder("utf-8").encode(serializeJavascriptValueToJSONString(data)); const body = extractBody(bytes); const relevantRealm = { settingsObject: {} }; @@ -4523,13 +5036,13 @@ var require_response = __commonJS({ initializeResponse(responseObject, init, { body: body[0], type: "application/json" }); return responseObject; } - static redirect(...args) { + static redirect(url, status = 302) { const relevantRealm = { settingsObject: {} }; - if (args.length < 1) { - throw new TypeError(`Failed to execute 'redirect' on 'Response': 1 argument required, but only ${args.length} present.`); + if (arguments.length < 1) { + throw new TypeError(`Failed to execute 'redirect' on 'Response': 1 argument required, but only ${arguments.length} present.`); } - const status = args.length >= 2 ? args[1] : 302; - const url = toUSVString(args[0]); + url = webidl.converters.USVString(url); + status = webidl.converters["unsigned short"](status); let parsedURL; try { parsedURL = new URL(url); @@ -4550,12 +5063,11 @@ var require_response = __commonJS({ responseObject[kState].headersList.append("location", value); return responseObject; } - constructor(...args) { - if (args.length >= 1 && typeof args[1] !== "object" && args[1] !== void 0) { - throw new TypeError("Failed to construct 'Request': cannot convert to dictionary."); + constructor(body = null, init = {}) { + if (body !== null) { + body = webidl.converters.BodyInit(body); } - const body = args.length >= 1 ? args[0] : null; - const init = args.length >= 2 ? args[1] ?? {} : {}; + init = webidl.converters.ResponseInit(init); this[kRealm] = { settingsObject: {} }; this[kState] = makeResponse({}); this[kHeaders] = new Headers(); @@ -4627,7 +5139,10 @@ var require_response = __commonJS({ throw new TypeError("Illegal invocation"); } if (this.bodyUsed || this.body && this.body.locked) { - throw new TypeError(); + webidl.errors.exception({ + header: "Response.clone", + message: "Body has already been consumed." + }); } const clonedResponse = cloneResponse(this[kState]); const clonedResponseObject = new Response(); @@ -4639,7 +5154,7 @@ var require_response = __commonJS({ return clonedResponseObject; } }; - mixinBody(Response.prototype); + mixinBody(Response); Object.defineProperties(Response.prototype, { type: kEnumerableProperty, url: kEnumerableProperty, @@ -4735,10 +5250,10 @@ var require_response = __commonJS({ } function makeAppropriateNetworkError(fetchParams) { assert(isCancelled(fetchParams)); - return isAborted(fetchParams) ? makeNetworkError(new AbortError()) : makeNetworkError(fetchParams.controller.terminated.reason); + return isAborted(fetchParams) ? makeNetworkError(new DOMException("The operation was aborted.", "AbortError")) : makeNetworkError(fetchParams.controller.terminated.reason); } function initializeResponse(response, init, body) { - if (init.status != null && (init.status < 200 || init.status > 599)) { + if (init.status !== null && (init.status < 200 || init.status > 599)) { throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.'); } if ("statusText" in init && init.statusText != null) { @@ -4757,7 +5272,10 @@ var require_response = __commonJS({ } if (body) { if (nullBodyStatus.includes(response.status)) { - throw new TypeError(); + webidl.errors.exception({ + header: "Response constructor", + message: "Invalid response status code." + }); } response[kState].body = body.body; if (body.type != null && !response[kState].headersList.has("Content-Type")) { @@ -4765,6 +5283,52 @@ var require_response = __commonJS({ } } } + webidl.converters.ReadableStream = webidl.interfaceConverter(ReadableStream); + webidl.converters.FormData = webidl.interfaceConverter(FormData); + webidl.converters.URLSearchParams = webidl.interfaceConverter(URLSearchParams); + webidl.converters.XMLHttpRequestBodyInit = function(V) { + if (typeof V === "string") { + return webidl.converters.USVString(V); + } + if (isBlobLike(V)) { + return webidl.converters.Blob(V); + } + if (types.isAnyArrayBuffer(V) || types.isTypedArray(V) || types.isDataView(V)) { + return webidl.converters.BufferSource(V); + } + if (V instanceof FormData) { + return webidl.converters.FormData(V); + } + if (V instanceof URLSearchParams) { + return webidl.converters.URLSearchParams(V); + } + return webidl.converters.DOMString(V); + }; + webidl.converters.BodyInit = function(V) { + if (V instanceof ReadableStream) { + return webidl.converters.ReadableStream(V); + } + if (V?.[Symbol.asyncIterator]) { + return V; + } + return webidl.converters.XMLHttpRequestBodyInit(V); + }; + webidl.converters.ResponseInit = webidl.dictionaryConverter([ + { + key: "status", + converter: webidl.converters["unsigned short"], + defaultValue: 200 + }, + { + key: "statusText", + converter: webidl.converters.ByteString, + defaultValue: "" + }, + { + key: "headers", + converter: webidl.converters.HeadersInit + } + ]); module2.exports = { makeNetworkError, makeResponse, @@ -4785,7 +5349,6 @@ var require_request2 = __commonJS({ var { isValidHTTPToken, sameOrigin, - toUSVString, normalizeMethod } = require_util2(); var { @@ -4799,6 +5362,7 @@ var require_request2 = __commonJS({ } = require_constants(); var { kEnumerableProperty } = util; var { kHeaders, kSignal, kState, kGuard, kRealm } = require_symbols2(); + var { webidl } = require_webidl(); var { kHeadersList } = require_symbols(); var assert = require("assert"); var TransformStream; @@ -4807,18 +5371,15 @@ var require_request2 = __commonJS({ signal.removeEventListener("abort", abort); }); var Request = class { - constructor(...args) { - if (args[0] === kInit) { + constructor(input, init = {}) { + if (input === kInit) { return; } - if (args.length < 1) { - throw new TypeError(`Failed to construct 'Request': 1 argument required, but only ${args.length} present.`); - } - if (args.length >= 1 && typeof args[1] !== "object" && args[1] !== void 0) { - throw new TypeError("Failed to construct 'Request': cannot convert to dictionary."); + if (arguments.length < 1) { + throw new TypeError(`Failed to construct 'Request': 1 argument required, but only ${arguments.length} present.`); } - const input = args[0] instanceof Request ? args[0] : toUSVString(args[0]); - const init = args.length >= 1 ? args[1] ?? {} : {}; + input = webidl.converters.RequestInfo(input); + init = webidl.converters.RequestInit(init); this[kRealm] = { settingsObject: {} }; let request = null; let fallbackMode = null; @@ -4914,7 +5475,10 @@ var require_request2 = __commonJS({ mode = fallbackMode; } if (mode === "navigate") { - throw new TypeError(); + webidl.errors.exception({ + header: "Request constructor", + message: "invalid request mode navigate." + }); } if (mode != null) { request.mode = mode; @@ -5160,7 +5724,7 @@ var require_request2 = __commonJS({ return clonedRequestObject; } }; - mixinBody(Request.prototype); + mixinBody(Request); function makeRequest(init) { const request = { method: "GET", @@ -5219,6 +5783,107 @@ var require_request2 = __commonJS({ clone: kEnumerableProperty, signal: kEnumerableProperty }); + webidl.converters.Request = webidl.interfaceConverter(Request); + webidl.converters.RequestInfo = function(V) { + if (typeof V === "string") { + return webidl.converters.USVString(V); + } + if (V instanceof Request) { + return webidl.converters.Request(V); + } + return webidl.converters.USVString(V); + }; + webidl.converters.AbortSignal = webidl.interfaceConverter(AbortSignal); + webidl.converters.RequestInit = webidl.dictionaryConverter([ + { + key: "method", + converter: webidl.converters.ByteString + }, + { + key: "headers", + converter: webidl.converters.HeadersInit + }, + { + key: "body", + converter: webidl.nullableConverter(webidl.converters.BodyInit) + }, + { + key: "referrer", + converter: webidl.converters.USVString + }, + { + key: "referrerPolicy", + converter: webidl.converters.DOMString, + allowedValues: [ + "", + "no-referrer", + "no-referrer-when-downgrade", + "same-origin", + "origin", + "strict-origin", + "origin-when-cross-origin", + "strict-origin-when-cross-origin", + "unsafe-url" + ] + }, + { + key: "mode", + converter: webidl.converters.DOMString, + allowedValues: [ + "same-origin", + "cors", + "no-cors", + "navigate", + "websocket" + ] + }, + { + key: "credentials", + converter: webidl.converters.DOMString, + allowedValues: [ + "omit", + "same-origin", + "include" + ] + }, + { + key: "cache", + converter: webidl.converters.DOMString, + allowedValues: [ + "default", + "no-store", + "reload", + "no-cache", + "force-cache", + "only-if-cached" + ] + }, + { + key: "redirect", + converter: webidl.converters.DOMString, + allowedValues: [ + "follow", + "error", + "manual" + ] + }, + { + key: "integrity", + converter: webidl.converters.DOMString + }, + { + key: "keepalive", + converter: webidl.converters.boolean + }, + { + key: "signal", + converter: webidl.nullableConverter(webidl.converters.AbortSignal) + }, + { + key: "window", + converter: webidl.converters.any + } + ]); module2.exports = { Request, makeRequest }; } }); @@ -5227,7 +5892,7 @@ var require_request2 = __commonJS({ var require_dataURL = __commonJS({ "lib/fetch/dataURL.js"(exports2, module2) { var assert = require("assert"); - var { atob } = require("buffer"); + var { atob: atob2 } = require("buffer"); var encoder = new TextEncoder(); function dataURLProcessor(dataURL) { assert(dataURL.protocol === "data:"); @@ -5382,7 +6047,7 @@ var require_dataURL = __commonJS({ if (/[^+/0-9A-Za-z]/.test(data)) { return "failure"; } - const binary = atob(data); + const binary = atob2(data); const bytes = new Uint8Array(binary.length); for (let byte = 0; byte < binary.length; byte++) { bytes[byte] = binary.charCodeAt(byte); @@ -5467,7 +6132,6 @@ var require_fetch = __commonJS({ isAborted } = require_util2(); var { kState, kHeaders, kGuard, kRealm } = require_symbols2(); - var { AbortError } = require_errors(); var assert = require("assert"); var { safelyExtractBody, extractBody } = require_body(); var { @@ -5475,7 +6139,8 @@ var require_fetch = __commonJS({ nullBodyStatus, safeMethods, requestBodyHeader, - subresource + subresource, + DOMException } = require_constants(); var { kHeadersList } = require_symbols(); var EE = require("events"); @@ -5485,6 +6150,9 @@ var require_fetch = __commonJS({ var { TransformStream } = require("stream/web"); var resolveObjectURL; var ReadableStream; + var nodeVersion = process.versions.node.split("."); + var nodeMajor = Number(nodeVersion[0]); + var nodeMinor = Number(nodeVersion[1]); var Fetch = class extends EE { constructor(dispatcher) { super(); @@ -5505,23 +6173,24 @@ var require_fetch = __commonJS({ if (this.state !== "ongoing") { return; } - const reason = new AbortError(); + const reason = new DOMException("The operation was aborted.", "AbortError"); this.state = "aborted"; this.connection?.destroy(reason); this.emit("terminated", reason); } }; - async function fetch2(...args) { - if (args.length < 1) { - throw new TypeError(`Failed to execute 'fetch' on 'Window': 1 argument required, but only ${args.length} present.`); + async function fetch2(input, init = {}) { + if (arguments.length < 1) { + throw new TypeError(`Failed to execute 'fetch' on 'Window': 1 argument required, but only ${arguments.length} present.`); } - if (args.length >= 1 && typeof args[1] !== "object" && args[1] !== void 0) { - throw new TypeError("Failed to execute 'fetch' on 'Window': cannot convert to dictionary."); - } - const resource = args[0]; - const init = args.length >= 1 ? args[1] ?? {} : {}; const p = createDeferredPromise(); - const requestObject = new Request(resource, init); + let requestObject; + try { + requestObject = new Request(input, init); + } catch (e) { + p.reject(e); + return p.promise; + } const request = requestObject[kState]; if (requestObject.signal.aborted) { abortFetch(p, request, null); @@ -5597,10 +6266,13 @@ var require_fetch = __commonJS({ response.timingInfo = timingInfo; markResourceTiming(timingInfo, originalURL, initiatorType, globalThis, cacheState); } - function markResourceTiming() { + function markResourceTiming(timingInfo, originalURL, initiatorType, globalThis2, cacheState) { + if (nodeMajor >= 18 && nodeMinor >= 2) { + performance.markResourceTiming(timingInfo, originalURL, initiatorType, globalThis2, cacheState); + } } function abortFetch(p, request, responseObject) { - const error = new AbortError(); + const error = new DOMException("The operation was aborted.", "AbortError"); p.reject(error); if (request.body != null && isReadable(request.body?.stream)) { request.body.stream.cancel(error).catch((err) => { @@ -5929,7 +6601,7 @@ var require_fetch = __commonJS({ if (redirectStatus.includes(actualResponse.status)) { fetchParams.controller.connection.destroy(); if (request.redirect === "error") { - response = makeNetworkError(); + response = makeNetworkError("unexpected redirect"); } else if (request.redirect === "manual") { response = actualResponse; } else if (request.redirect === "follow") { @@ -6103,7 +6775,7 @@ var require_fetch = __commonJS({ destroy(err) { if (!this.destroyed) { this.destroyed = true; - this.abort?.(err ?? new AbortError()); + this.abort?.(err ?? new DOMException("The operation was aborted.", "AbortError")); } } }; @@ -6238,7 +6910,7 @@ var require_fetch = __commonJS({ if (isAborted(fetchParams)) { response.aborted = true; if (isReadable(stream)) { - fetchParams.controller.controller.error(new AbortError()); + fetchParams.controller.controller.error(new DOMException("The operation was aborted.", "AbortError")); } } else { if (isReadable(stream)) { @@ -6267,7 +6939,7 @@ var require_fetch = __commonJS({ onConnect(abort) { const { connection } = fetchParams.controller; if (connection.destroyed) { - abort(new AbortError()); + abort(new DOMException("The operation was aborted.", "AbortError")); } else { fetchParams.controller.on("terminated", abort); this.abort = connection.abort = abort;