aboutsummaryrefslogtreecommitdiffstats
path: root/vanilla/node_modules/undici/lib/web/fetch
diff options
context:
space:
mode:
Diffstat (limited to 'vanilla/node_modules/undici/lib/web/fetch')
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/LICENSE21
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/body.js509
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/constants.js131
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/data-url.js596
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js575
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/formdata.js259
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/global.js40
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/headers.js719
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/index.js2372
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/request.js1115
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/response.js641
-rw-r--r--vanilla/node_modules/undici/lib/web/fetch/util.js1520
12 files changed, 8498 insertions, 0 deletions
diff --git a/vanilla/node_modules/undici/lib/web/fetch/LICENSE b/vanilla/node_modules/undici/lib/web/fetch/LICENSE
new file mode 100644
index 0000000..2943500
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Ethan Arrowood
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vanilla/node_modules/undici/lib/web/fetch/body.js b/vanilla/node_modules/undici/lib/web/fetch/body.js
new file mode 100644
index 0000000..5d17249
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/body.js
@@ -0,0 +1,509 @@
+'use strict'
+
+const util = require('../../core/util')
+const {
+ ReadableStreamFrom,
+ readableStreamClose,
+ fullyReadBody,
+ extractMimeType
+} = require('./util')
+const { FormData, setFormDataState } = require('./formdata')
+const { webidl } = require('../webidl')
+const assert = require('node:assert')
+const { isErrored, isDisturbed } = require('node:stream')
+const { isUint8Array } = require('node:util/types')
+const { serializeAMimeType } = require('./data-url')
+const { multipartFormDataParser } = require('./formdata-parser')
+const { createDeferredPromise } = require('../../util/promise')
+const { parseJSONFromBytes } = require('../infra')
+const { utf8DecodeBytes } = require('../../encoding')
+const { runtimeFeatures } = require('../../util/runtime-features.js')
+
+const random = runtimeFeatures.has('crypto')
+ ? require('node:crypto').randomInt
+ : (max) => Math.floor(Math.random() * max)
+
+const textEncoder = new TextEncoder()
+function noop () {}
+
+const streamRegistry = new FinalizationRegistry((weakRef) => {
+ const stream = weakRef.deref()
+ if (stream && !stream.locked && !isDisturbed(stream) && !isErrored(stream)) {
+ stream.cancel('Response object has been garbage collected').catch(noop)
+ }
+})
+
+/**
+ * Extract a body with type from a byte sequence or BodyInit object
+ *
+ * @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
+ * @param {boolean} [keepalive=false] - If true, indicates that the body
+ * @returns {[{stream: ReadableStream, source: any, length: number | null}, string | null]} - Returns a tuple containing the body and its type
+ *
+ * @see https://fetch.spec.whatwg.org/#concept-bodyinit-extract
+ */
+function extractBody (object, keepalive = false) {
+ // 1. Let stream be null.
+ let stream = null
+ let controller = null
+
+ // 2. If object is a ReadableStream object, then set stream to object.
+ if (webidl.is.ReadableStream(object)) {
+ stream = object
+ } else if (webidl.is.Blob(object)) {
+ // 3. Otherwise, if object is a Blob object, set stream to the
+ // result of running object’s get stream.
+ stream = object.stream()
+ } else {
+ // 4. Otherwise, set stream to a new ReadableStream object, and set
+ // up stream with byte reading support.
+ stream = new ReadableStream({
+ pull () {},
+ start (c) {
+ controller = c
+ },
+ cancel () {},
+ type: 'bytes'
+ })
+ }
+
+ // 5. Assert: stream is a ReadableStream object.
+ assert(webidl.is.ReadableStream(stream))
+
+ // 6. Let action be null.
+ let action = null
+
+ // 7. Let source be null.
+ let source = null
+
+ // 8. Let length be null.
+ let length = null
+
+ // 9. Let type be null.
+ let type = null
+
+ // 10. Switch on object:
+ if (typeof object === 'string') {
+ // Set source to the UTF-8 encoding of object.
+ // Note: setting source to a Uint8Array here breaks some mocking assumptions.
+ source = object
+
+ // Set type to `text/plain;charset=UTF-8`.
+ type = 'text/plain;charset=UTF-8'
+ } else if (webidl.is.URLSearchParams(object)) {
+ // URLSearchParams
+
+ // spec says to run application/x-www-form-urlencoded on body.list
+ // this is implemented in Node.js as apart of an URLSearchParams instance toString method
+ // See: https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L490
+ // and https://github.com/nodejs/node/blob/e46c680bf2b211bbd52cf959ca17ee98c7f657f5/lib/internal/url.js#L1100
+
+ // Set source to the result of running the application/x-www-form-urlencoded serializer with object’s list.
+ source = object.toString()
+
+ // Set type to `application/x-www-form-urlencoded;charset=UTF-8`.
+ type = 'application/x-www-form-urlencoded;charset=UTF-8'
+ } else if (webidl.is.BufferSource(object)) {
+ // Set source to a copy of the bytes held by object.
+ source = webidl.util.getCopyOfBytesHeldByBufferSource(object)
+ } else if (webidl.is.FormData(object)) {
+ const boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
+ const prefix = `--${boundary}\r\nContent-Disposition: form-data`
+
+ /*! formdata-polyfill. MIT License. Jimmy Wärting <https://jimmy.warting.se/opensource> */
+ const formdataEscape = (str) =>
+ str.replace(/\n/g, '%0A').replace(/\r/g, '%0D').replace(/"/g, '%22')
+ const normalizeLinefeeds = (value) => value.replace(/\r?\n|\r/g, '\r\n')
+
+ // Set action to this step: run the multipart/form-data
+ // encoding algorithm, with object’s entry list and UTF-8.
+ // - This ensures that the body is immutable and can't be changed afterwords
+ // - That the content-length is calculated in advance.
+ // - And that all parts are pre-encoded and ready to be sent.
+
+ const blobParts = []
+ const rn = new Uint8Array([13, 10]) // '\r\n'
+ length = 0
+ let hasUnknownSizeValue = false
+
+ for (const [name, value] of object) {
+ if (typeof value === 'string') {
+ const chunk = textEncoder.encode(prefix +
+ `; name="${formdataEscape(normalizeLinefeeds(name))}"` +
+ `\r\n\r\n${normalizeLinefeeds(value)}\r\n`)
+ blobParts.push(chunk)
+ length += chunk.byteLength
+ } else {
+ const chunk = textEncoder.encode(`${prefix}; name="${formdataEscape(normalizeLinefeeds(name))}"` +
+ (value.name ? `; filename="${formdataEscape(value.name)}"` : '') + '\r\n' +
+ `Content-Type: ${
+ value.type || 'application/octet-stream'
+ }\r\n\r\n`)
+ blobParts.push(chunk, value, rn)
+ if (typeof value.size === 'number') {
+ length += chunk.byteLength + value.size + rn.byteLength
+ } else {
+ hasUnknownSizeValue = true
+ }
+ }
+ }
+
+ // CRLF is appended to the body to function with legacy servers and match other implementations.
+ // https://github.com/curl/curl/blob/3434c6b46e682452973972e8313613dfa58cd690/lib/mime.c#L1029-L1030
+ // https://github.com/form-data/form-data/issues/63
+ const chunk = textEncoder.encode(`--${boundary}--\r\n`)
+ blobParts.push(chunk)
+ length += chunk.byteLength
+ if (hasUnknownSizeValue) {
+ length = null
+ }
+
+ // Set source to object.
+ source = object
+
+ action = async function * () {
+ for (const part of blobParts) {
+ if (part.stream) {
+ yield * part.stream()
+ } else {
+ yield part
+ }
+ }
+ }
+
+ // Set type to `multipart/form-data; boundary=`,
+ // followed by the multipart/form-data boundary string generated
+ // by the multipart/form-data encoding algorithm.
+ type = `multipart/form-data; boundary=${boundary}`
+ } else if (webidl.is.Blob(object)) {
+ // Blob
+
+ // Set source to object.
+ source = object
+
+ // Set length to object’s size.
+ length = object.size
+
+ // If object’s type attribute is not the empty byte sequence, set
+ // type to its value.
+ if (object.type) {
+ type = object.type
+ }
+ } else if (typeof object[Symbol.asyncIterator] === 'function') {
+ // If keepalive is true, then throw a TypeError.
+ if (keepalive) {
+ throw new TypeError('keepalive')
+ }
+
+ // If object is disturbed or locked, then throw a TypeError.
+ if (util.isDisturbed(object) || object.locked) {
+ throw new TypeError(
+ 'Response body object should not be disturbed or locked'
+ )
+ }
+
+ stream =
+ webidl.is.ReadableStream(object) ? object : ReadableStreamFrom(object)
+ }
+
+ // 11. If source is a byte sequence, then set action to a
+ // step that returns source and length to source’s length.
+ if (typeof source === 'string' || isUint8Array(source)) {
+ action = () => {
+ length = typeof source === 'string' ? Buffer.byteLength(source) : source.length
+ return source
+ }
+ }
+
+ // 12. If action is non-null, then run these steps in parallel:
+ if (action != null) {
+ ;(async () => {
+ // 1. Run action.
+ const result = action()
+
+ // 2. Whenever one or more bytes are available and stream is not errored,
+ // enqueue the result of creating a Uint8Array from the available bytes into stream.
+ const iterator = result?.[Symbol.asyncIterator]?.()
+ if (iterator) {
+ for await (const bytes of iterator) {
+ if (isErrored(stream)) break
+ if (bytes.length) {
+ controller.enqueue(new Uint8Array(bytes))
+ }
+ }
+ } else if (result?.length && !isErrored(stream)) {
+ controller.enqueue(typeof result === 'string' ? textEncoder.encode(result) : new Uint8Array(result))
+ }
+
+ // 3. When running action is done, close stream.
+ queueMicrotask(() => readableStreamClose(controller))
+ })()
+ }
+
+ // 13. Let body be a body whose stream is stream, source is source,
+ // and length is length.
+ const body = { stream, source, length }
+
+ // 14. Return (body, type).
+ return [body, type]
+}
+
+/**
+ * @typedef {object} ExtractBodyResult
+ * @property {ReadableStream<Uint8Array<ArrayBuffer>>} stream - The ReadableStream containing the body data
+ * @property {any} source - The original source of the body data
+ * @property {number | null} length - The length of the body data, or null
+ */
+
+/**
+ * Safely extract a body with type from a byte sequence or BodyInit object.
+ *
+ * @param {import('../../../types').BodyInit} object - The BodyInit object to extract from
+ * @param {boolean} [keepalive=false] - If true, indicates that the body
+ * @returns {[ExtractBodyResult, string | null]} - Returns a tuple containing the body and its type
+ *
+ * @see https://fetch.spec.whatwg.org/#bodyinit-safely-extract
+ */
+function safelyExtractBody (object, keepalive = false) {
+ // To safely extract a body and a `Content-Type` value from
+ // a byte sequence or BodyInit object object, run these steps:
+
+ // 1. If object is a ReadableStream object, then:
+ if (webidl.is.ReadableStream(object)) {
+ // Assert: object is neither disturbed nor locked.
+ assert(!util.isDisturbed(object), 'The body has already been consumed.')
+ assert(!object.locked, 'The stream is locked.')
+ }
+
+ // 2. Return the results of extracting object.
+ return extractBody(object, keepalive)
+}
+
+function cloneBody (body) {
+ // To clone a body body, run these steps:
+
+ // https://fetch.spec.whatwg.org/#concept-body-clone
+
+ // 1. Let « out1, out2 » be the result of teeing body’s stream.
+ const { 0: out1, 1: out2 } = body.stream.tee()
+
+ // 2. Set body’s stream to out1.
+ body.stream = out1
+
+ // 3. Return a body whose stream is out2 and other members are copied from body.
+ return {
+ stream: out2,
+ length: body.length,
+ source: body.source
+ }
+}
+
+function bodyMixinMethods (instance, getInternalState) {
+ const methods = {
+ blob () {
+ // The blob() method steps are to return the result of
+ // running consume body with this and the following step
+ // given a byte sequence bytes: return a Blob whose
+ // contents are bytes and whose type attribute is this’s
+ // MIME type.
+ return consumeBody(this, (bytes) => {
+ let mimeType = bodyMimeType(getInternalState(this))
+
+ if (mimeType === null) {
+ mimeType = ''
+ } else if (mimeType) {
+ mimeType = serializeAMimeType(mimeType)
+ }
+
+ // Return a Blob whose contents are bytes and type attribute
+ // is mimeType.
+ return new Blob([bytes], { type: mimeType })
+ }, instance, getInternalState)
+ },
+
+ arrayBuffer () {
+ // The arrayBuffer() method steps are to return the result
+ // of running consume body with this and the following step
+ // given a byte sequence bytes: return a new ArrayBuffer
+ // whose contents are bytes.
+ return consumeBody(this, (bytes) => {
+ return new Uint8Array(bytes).buffer
+ }, instance, getInternalState)
+ },
+
+ text () {
+ // The text() method steps are to return the result of running
+ // consume body with this and UTF-8 decode.
+ return consumeBody(this, utf8DecodeBytes, instance, getInternalState)
+ },
+
+ json () {
+ // The json() method steps are to return the result of running
+ // consume body with this and parse JSON from bytes.
+ return consumeBody(this, parseJSONFromBytes, instance, getInternalState)
+ },
+
+ formData () {
+ // The formData() method steps are to return the result of running
+ // consume body with this and the following step given a byte sequence bytes:
+ return consumeBody(this, (value) => {
+ // 1. Let mimeType be the result of get the MIME type with this.
+ const mimeType = bodyMimeType(getInternalState(this))
+
+ // 2. If mimeType is non-null, then switch on mimeType’s essence and run
+ // the corresponding steps:
+ if (mimeType !== null) {
+ switch (mimeType.essence) {
+ case 'multipart/form-data': {
+ // 1. ... [long step]
+ // 2. If that fails for some reason, then throw a TypeError.
+ const parsed = multipartFormDataParser(value, mimeType)
+
+ // 3. Return a new FormData object, appending each entry,
+ // resulting from the parsing operation, to its entry list.
+ const fd = new FormData()
+ setFormDataState(fd, parsed)
+
+ return fd
+ }
+ case 'application/x-www-form-urlencoded': {
+ // 1. Let entries be the result of parsing bytes.
+ const entries = new URLSearchParams(value.toString())
+
+ // 2. If entries is failure, then throw a TypeError.
+
+ // 3. Return a new FormData object whose entry list is entries.
+ const fd = new FormData()
+
+ for (const [name, value] of entries) {
+ fd.append(name, value)
+ }
+
+ return fd
+ }
+ }
+ }
+
+ // 3. Throw a TypeError.
+ throw new TypeError(
+ 'Content-Type was not one of "multipart/form-data" or "application/x-www-form-urlencoded".'
+ )
+ }, instance, getInternalState)
+ },
+
+ bytes () {
+ // The bytes() method steps are to return the result of running consume body
+ // with this and the following step given a byte sequence bytes: return the
+ // result of creating a Uint8Array from bytes in this’s relevant realm.
+ return consumeBody(this, (bytes) => {
+ return new Uint8Array(bytes)
+ }, instance, getInternalState)
+ }
+ }
+
+ return methods
+}
+
+function mixinBody (prototype, getInternalState) {
+ Object.assign(prototype.prototype, bodyMixinMethods(prototype, getInternalState))
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-body-consume-body
+ * @param {any} object internal state
+ * @param {(value: unknown) => unknown} convertBytesToJSValue
+ * @param {any} instance
+ * @param {(target: any) => any} getInternalState
+ */
+function consumeBody (object, convertBytesToJSValue, instance, getInternalState) {
+ try {
+ webidl.brandCheck(object, instance)
+ } catch (e) {
+ return Promise.reject(e)
+ }
+
+ object = getInternalState(object)
+
+ // 1. If object is unusable, then return a promise rejected
+ // with a TypeError.
+ if (bodyUnusable(object)) {
+ return Promise.reject(new TypeError('Body is unusable: Body has already been read'))
+ }
+
+ // 2. Let promise be a new promise.
+ const promise = createDeferredPromise()
+
+ // 3. Let errorSteps given error be to reject promise with error.
+ const errorSteps = promise.reject
+
+ // 4. Let successSteps given a byte sequence data be to resolve
+ // promise with the result of running convertBytesToJSValue
+ // with data. If that threw an exception, then run errorSteps
+ // with that exception.
+ const successSteps = (data) => {
+ try {
+ promise.resolve(convertBytesToJSValue(data))
+ } catch (e) {
+ errorSteps(e)
+ }
+ }
+
+ // 5. If object’s body is null, then run successSteps with an
+ // empty byte sequence.
+ if (object.body == null) {
+ successSteps(Buffer.allocUnsafe(0))
+ return promise.promise
+ }
+
+ // 6. Otherwise, fully read object’s body given successSteps,
+ // errorSteps, and object’s relevant global object.
+ fullyReadBody(object.body, successSteps, errorSteps)
+
+ // 7. Return promise.
+ return promise.promise
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#body-unusable
+ * @param {any} object internal state
+ */
+function bodyUnusable (object) {
+ const body = object.body
+
+ // An object including the Body interface mixin is
+ // said to be unusable if its body is non-null and
+ // its body’s stream is disturbed or locked.
+ return body != null && (body.stream.locked || util.isDisturbed(body.stream))
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-body-mime-type
+ * @param {any} requestOrResponse internal state
+ */
+function bodyMimeType (requestOrResponse) {
+ // 1. Let headers be null.
+ // 2. If requestOrResponse is a Request object, then set headers to requestOrResponse’s request’s header list.
+ // 3. Otherwise, set headers to requestOrResponse’s response’s header list.
+ /** @type {import('./headers').HeadersList} */
+ const headers = requestOrResponse.headersList
+
+ // 4. Let mimeType be the result of extracting a MIME type from headers.
+ const mimeType = extractMimeType(headers)
+
+ // 5. If mimeType is failure, then return null.
+ if (mimeType === 'failure') {
+ return null
+ }
+
+ // 6. Return mimeType.
+ return mimeType
+}
+
+module.exports = {
+ extractBody,
+ safelyExtractBody,
+ cloneBody,
+ mixinBody,
+ streamRegistry,
+ bodyUnusable
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/constants.js b/vanilla/node_modules/undici/lib/web/fetch/constants.js
new file mode 100644
index 0000000..ef63b0c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/constants.js
@@ -0,0 +1,131 @@
+'use strict'
+
+const corsSafeListedMethods = /** @type {const} */ (['GET', 'HEAD', 'POST'])
+const corsSafeListedMethodsSet = new Set(corsSafeListedMethods)
+
+const nullBodyStatus = /** @type {const} */ ([101, 204, 205, 304])
+
+const redirectStatus = /** @type {const} */ ([301, 302, 303, 307, 308])
+const redirectStatusSet = new Set(redirectStatus)
+
+/**
+ * @see https://fetch.spec.whatwg.org/#block-bad-port
+ */
+const badPorts = /** @type {const} */ ([
+ '1', '7', '9', '11', '13', '15', '17', '19', '20', '21', '22', '23', '25', '37', '42', '43', '53', '69', '77', '79',
+ '87', '95', '101', '102', '103', '104', '109', '110', '111', '113', '115', '117', '119', '123', '135', '137',
+ '139', '143', '161', '179', '389', '427', '465', '512', '513', '514', '515', '526', '530', '531', '532',
+ '540', '548', '554', '556', '563', '587', '601', '636', '989', '990', '993', '995', '1719', '1720', '1723',
+ '2049', '3659', '4045', '4190', '5060', '5061', '6000', '6566', '6665', '6666', '6667', '6668', '6669', '6679',
+ '6697', '10080'
+])
+const badPortsSet = new Set(badPorts)
+
+/**
+ * @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policy-header
+ */
+const referrerPolicyTokens = /** @type {const} */ ([
+ 'no-referrer',
+ 'no-referrer-when-downgrade',
+ 'same-origin',
+ 'origin',
+ 'strict-origin',
+ 'origin-when-cross-origin',
+ 'strict-origin-when-cross-origin',
+ 'unsafe-url'
+])
+
+/**
+ * @see https://w3c.github.io/webappsec-referrer-policy/#referrer-policies
+ */
+const referrerPolicy = /** @type {const} */ ([
+ '',
+ ...referrerPolicyTokens
+])
+const referrerPolicyTokensSet = new Set(referrerPolicyTokens)
+
+const requestRedirect = /** @type {const} */ (['follow', 'manual', 'error'])
+
+const safeMethods = /** @type {const} */ (['GET', 'HEAD', 'OPTIONS', 'TRACE'])
+const safeMethodsSet = new Set(safeMethods)
+
+const requestMode = /** @type {const} */ (['navigate', 'same-origin', 'no-cors', 'cors'])
+
+const requestCredentials = /** @type {const} */ (['omit', 'same-origin', 'include'])
+
+const requestCache = /** @type {const} */ ([
+ 'default',
+ 'no-store',
+ 'reload',
+ 'no-cache',
+ 'force-cache',
+ 'only-if-cached'
+])
+
+/**
+ * @see https://fetch.spec.whatwg.org/#request-body-header-name
+ */
+const requestBodyHeader = /** @type {const} */ ([
+ 'content-encoding',
+ 'content-language',
+ 'content-location',
+ 'content-type',
+ // See https://github.com/nodejs/undici/issues/2021
+ // 'Content-Length' is a forbidden header name, which is typically
+ // removed in the Headers implementation. However, undici doesn't
+ // filter out headers, so we add it here.
+ 'content-length'
+])
+
+/**
+ * @see https://fetch.spec.whatwg.org/#enumdef-requestduplex
+ */
+const requestDuplex = /** @type {const} */ ([
+ 'half'
+])
+
+/**
+ * @see http://fetch.spec.whatwg.org/#forbidden-method
+ */
+const forbiddenMethods = /** @type {const} */ (['CONNECT', 'TRACE', 'TRACK'])
+const forbiddenMethodsSet = new Set(forbiddenMethods)
+
+const subresource = /** @type {const} */ ([
+ 'audio',
+ 'audioworklet',
+ 'font',
+ 'image',
+ 'manifest',
+ 'paintworklet',
+ 'script',
+ 'style',
+ 'track',
+ 'video',
+ 'xslt',
+ ''
+])
+const subresourceSet = new Set(subresource)
+
+module.exports = {
+ subresource,
+ forbiddenMethods,
+ requestBodyHeader,
+ referrerPolicy,
+ requestRedirect,
+ requestMode,
+ requestCredentials,
+ requestCache,
+ redirectStatus,
+ corsSafeListedMethods,
+ nullBodyStatus,
+ safeMethods,
+ badPorts,
+ requestDuplex,
+ subresourceSet,
+ badPortsSet,
+ redirectStatusSet,
+ corsSafeListedMethodsSet,
+ safeMethodsSet,
+ forbiddenMethodsSet,
+ referrerPolicyTokens: referrerPolicyTokensSet
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/data-url.js b/vanilla/node_modules/undici/lib/web/fetch/data-url.js
new file mode 100644
index 0000000..27ad7ae
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/data-url.js
@@ -0,0 +1,596 @@
+'use strict'
+
+const assert = require('node:assert')
+const { forgivingBase64, collectASequenceOfCodePoints, collectASequenceOfCodePointsFast, isomorphicDecode, removeASCIIWhitespace, removeChars } = require('../infra')
+
+const encoder = new TextEncoder()
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#http-token-code-point
+ */
+const HTTP_TOKEN_CODEPOINTS = /^[-!#$%&'*+.^_|~A-Za-z0-9]+$/u
+const HTTP_WHITESPACE_REGEX = /[\u000A\u000D\u0009\u0020]/u // eslint-disable-line
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#http-quoted-string-token-code-point
+ */
+const HTTP_QUOTED_STRING_TOKENS = /^[\u0009\u0020-\u007E\u0080-\u00FF]+$/u // eslint-disable-line
+
+// https://fetch.spec.whatwg.org/#data-url-processor
+/** @param {URL} dataURL */
+function dataURLProcessor (dataURL) {
+ // 1. Assert: dataURL’s scheme is "data".
+ assert(dataURL.protocol === 'data:')
+
+ // 2. Let input be the result of running the URL
+ // serializer on dataURL with exclude fragment
+ // set to true.
+ let input = URLSerializer(dataURL, true)
+
+ // 3. Remove the leading "data:" string from input.
+ input = input.slice(5)
+
+ // 4. Let position point at the start of input.
+ const position = { position: 0 }
+
+ // 5. Let mimeType be the result of collecting a
+ // sequence of code points that are not equal
+ // to U+002C (,), given position.
+ let mimeType = collectASequenceOfCodePointsFast(
+ ',',
+ input,
+ position
+ )
+
+ // 6. Strip leading and trailing ASCII whitespace
+ // from mimeType.
+ // Undici implementation note: we need to store the
+ // length because if the mimetype has spaces removed,
+ // the wrong amount will be sliced from the input in
+ // step #9
+ const mimeTypeLength = mimeType.length
+ mimeType = removeASCIIWhitespace(mimeType, true, true)
+
+ // 7. If position is past the end of input, then
+ // return failure
+ if (position.position >= input.length) {
+ return 'failure'
+ }
+
+ // 8. Advance position by 1.
+ position.position++
+
+ // 9. Let encodedBody be the remainder of input.
+ const encodedBody = input.slice(mimeTypeLength + 1)
+
+ // 10. Let body be the percent-decoding of encodedBody.
+ let body = stringPercentDecode(encodedBody)
+
+ // 11. If mimeType ends with U+003B (;), followed by
+ // zero or more U+0020 SPACE, followed by an ASCII
+ // case-insensitive match for "base64", then:
+ if (/;(?:\u0020*)base64$/ui.test(mimeType)) {
+ // 1. Let stringBody be the isomorphic decode of body.
+ const stringBody = isomorphicDecode(body)
+
+ // 2. Set body to the forgiving-base64 decode of
+ // stringBody.
+ body = forgivingBase64(stringBody)
+
+ // 3. If body is failure, then return failure.
+ if (body === 'failure') {
+ return 'failure'
+ }
+
+ // 4. Remove the last 6 code points from mimeType.
+ mimeType = mimeType.slice(0, -6)
+
+ // 5. Remove trailing U+0020 SPACE code points from mimeType,
+ // if any.
+ mimeType = mimeType.replace(/(\u0020+)$/u, '')
+
+ // 6. Remove the last U+003B (;) code point from mimeType.
+ mimeType = mimeType.slice(0, -1)
+ }
+
+ // 12. If mimeType starts with U+003B (;), then prepend
+ // "text/plain" to mimeType.
+ if (mimeType.startsWith(';')) {
+ mimeType = 'text/plain' + mimeType
+ }
+
+ // 13. Let mimeTypeRecord be the result of parsing
+ // mimeType.
+ let mimeTypeRecord = parseMIMEType(mimeType)
+
+ // 14. If mimeTypeRecord is failure, then set
+ // mimeTypeRecord to text/plain;charset=US-ASCII.
+ if (mimeTypeRecord === 'failure') {
+ mimeTypeRecord = parseMIMEType('text/plain;charset=US-ASCII')
+ }
+
+ // 15. Return a new data: URL struct whose MIME
+ // type is mimeTypeRecord and body is body.
+ // https://fetch.spec.whatwg.org/#data-url-struct
+ return { mimeType: mimeTypeRecord, body }
+}
+
+// https://url.spec.whatwg.org/#concept-url-serializer
+/**
+ * @param {URL} url
+ * @param {boolean} excludeFragment
+ */
+function URLSerializer (url, excludeFragment = false) {
+ if (!excludeFragment) {
+ return url.href
+ }
+
+ const href = url.href
+ const hashLength = url.hash.length
+
+ const serialized = hashLength === 0 ? href : href.substring(0, href.length - hashLength)
+
+ if (!hashLength && href.endsWith('#')) {
+ return serialized.slice(0, -1)
+ }
+
+ return serialized
+}
+
+// https://url.spec.whatwg.org/#string-percent-decode
+/** @param {string} input */
+function stringPercentDecode (input) {
+ // 1. Let bytes be the UTF-8 encoding of input.
+ const bytes = encoder.encode(input)
+
+ // 2. Return the percent-decoding of bytes.
+ return percentDecode(bytes)
+}
+
+/**
+ * @param {number} byte
+ */
+function isHexCharByte (byte) {
+ // 0-9 A-F a-f
+ return (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x46) || (byte >= 0x61 && byte <= 0x66)
+}
+
+/**
+ * @param {number} byte
+ */
+function hexByteToNumber (byte) {
+ return (
+ // 0-9
+ byte >= 0x30 && byte <= 0x39
+ ? (byte - 48)
+ // Convert to uppercase
+ // ((byte & 0xDF) - 65) + 10
+ : ((byte & 0xDF) - 55)
+ )
+}
+
+// https://url.spec.whatwg.org/#percent-decode
+/** @param {Uint8Array} input */
+function percentDecode (input) {
+ const length = input.length
+ // 1. Let output be an empty byte sequence.
+ /** @type {Uint8Array} */
+ const output = new Uint8Array(length)
+ let j = 0
+ let i = 0
+ // 2. For each byte byte in input:
+ while (i < length) {
+ const byte = input[i]
+
+ // 1. If byte is not 0x25 (%), then append byte to output.
+ if (byte !== 0x25) {
+ output[j++] = byte
+
+ // 2. Otherwise, if byte is 0x25 (%) and the next two bytes
+ // after byte in input are not in the ranges
+ // 0x30 (0) to 0x39 (9), 0x41 (A) to 0x46 (F),
+ // and 0x61 (a) to 0x66 (f), all inclusive, append byte
+ // to output.
+ } else if (
+ byte === 0x25 &&
+ !(isHexCharByte(input[i + 1]) && isHexCharByte(input[i + 2]))
+ ) {
+ output[j++] = 0x25
+
+ // 3. Otherwise:
+ } else {
+ // 1. Let bytePoint be the two bytes after byte in input,
+ // decoded, and then interpreted as hexadecimal number.
+ // 2. Append a byte whose value is bytePoint to output.
+ output[j++] = (hexByteToNumber(input[i + 1]) << 4) | hexByteToNumber(input[i + 2])
+
+ // 3. Skip the next two bytes in input.
+ i += 2
+ }
+ ++i
+ }
+
+ // 3. Return output.
+ return length === j ? output : output.subarray(0, j)
+}
+
+// https://mimesniff.spec.whatwg.org/#parse-a-mime-type
+/** @param {string} input */
+function parseMIMEType (input) {
+ // 1. Remove any leading and trailing HTTP whitespace
+ // from input.
+ input = removeHTTPWhitespace(input, true, true)
+
+ // 2. Let position be a position variable for input,
+ // initially pointing at the start of input.
+ const position = { position: 0 }
+
+ // 3. Let type be the result of collecting a sequence
+ // of code points that are not U+002F (/) from
+ // input, given position.
+ const type = collectASequenceOfCodePointsFast(
+ '/',
+ input,
+ position
+ )
+
+ // 4. If type is the empty string or does not solely
+ // contain HTTP token code points, then return failure.
+ // https://mimesniff.spec.whatwg.org/#http-token-code-point
+ if (type.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(type)) {
+ return 'failure'
+ }
+
+ // 5. If position is past the end of input, then return
+ // failure
+ if (position.position >= input.length) {
+ return 'failure'
+ }
+
+ // 6. Advance position by 1. (This skips past U+002F (/).)
+ position.position++
+
+ // 7. Let subtype be the result of collecting a sequence of
+ // code points that are not U+003B (;) from input, given
+ // position.
+ let subtype = collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 8. Remove any trailing HTTP whitespace from subtype.
+ subtype = removeHTTPWhitespace(subtype, false, true)
+
+ // 9. If subtype is the empty string or does not solely
+ // contain HTTP token code points, then return failure.
+ if (subtype.length === 0 || !HTTP_TOKEN_CODEPOINTS.test(subtype)) {
+ return 'failure'
+ }
+
+ const typeLowercase = type.toLowerCase()
+ const subtypeLowercase = subtype.toLowerCase()
+
+ // 10. Let mimeType be a new MIME type record whose type
+ // is type, in ASCII lowercase, and subtype is subtype,
+ // in ASCII lowercase.
+ // https://mimesniff.spec.whatwg.org/#mime-type
+ const mimeType = {
+ type: typeLowercase,
+ subtype: subtypeLowercase,
+ /** @type {Map<string, string>} */
+ parameters: new Map(),
+ // https://mimesniff.spec.whatwg.org/#mime-type-essence
+ essence: `${typeLowercase}/${subtypeLowercase}`
+ }
+
+ // 11. While position is not past the end of input:
+ while (position.position < input.length) {
+ // 1. Advance position by 1. (This skips past U+003B (;).)
+ position.position++
+
+ // 2. Collect a sequence of code points that are HTTP
+ // whitespace from input given position.
+ collectASequenceOfCodePoints(
+ // https://fetch.spec.whatwg.org/#http-whitespace
+ char => HTTP_WHITESPACE_REGEX.test(char),
+ input,
+ position
+ )
+
+ // 3. Let parameterName be the result of collecting a
+ // sequence of code points that are not U+003B (;)
+ // or U+003D (=) from input, given position.
+ let parameterName = collectASequenceOfCodePoints(
+ (char) => char !== ';' && char !== '=',
+ input,
+ position
+ )
+
+ // 4. Set parameterName to parameterName, in ASCII
+ // lowercase.
+ parameterName = parameterName.toLowerCase()
+
+ // 5. If position is not past the end of input, then:
+ if (position.position < input.length) {
+ // 1. If the code point at position within input is
+ // U+003B (;), then continue.
+ if (input[position.position] === ';') {
+ continue
+ }
+
+ // 2. Advance position by 1. (This skips past U+003D (=).)
+ position.position++
+ }
+
+ // 6. If position is past the end of input, then break.
+ if (position.position >= input.length) {
+ break
+ }
+
+ // 7. Let parameterValue be null.
+ let parameterValue = null
+
+ // 8. If the code point at position within input is
+ // U+0022 ("), then:
+ if (input[position.position] === '"') {
+ // 1. Set parameterValue to the result of collecting
+ // an HTTP quoted string from input, given position
+ // and the extract-value flag.
+ parameterValue = collectAnHTTPQuotedString(input, position, true)
+
+ // 2. Collect a sequence of code points that are not
+ // U+003B (;) from input, given position.
+ collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 9. Otherwise:
+ } else {
+ // 1. Set parameterValue to the result of collecting
+ // a sequence of code points that are not U+003B (;)
+ // from input, given position.
+ parameterValue = collectASequenceOfCodePointsFast(
+ ';',
+ input,
+ position
+ )
+
+ // 2. Remove any trailing HTTP whitespace from parameterValue.
+ parameterValue = removeHTTPWhitespace(parameterValue, false, true)
+
+ // 3. If parameterValue is the empty string, then continue.
+ if (parameterValue.length === 0) {
+ continue
+ }
+ }
+
+ // 10. If all of the following are true
+ // - parameterName is not the empty string
+ // - parameterName solely contains HTTP token code points
+ // - parameterValue solely contains HTTP quoted-string token code points
+ // - mimeType’s parameters[parameterName] does not exist
+ // then set mimeType’s parameters[parameterName] to parameterValue.
+ if (
+ parameterName.length !== 0 &&
+ HTTP_TOKEN_CODEPOINTS.test(parameterName) &&
+ (parameterValue.length === 0 || HTTP_QUOTED_STRING_TOKENS.test(parameterValue)) &&
+ !mimeType.parameters.has(parameterName)
+ ) {
+ mimeType.parameters.set(parameterName, parameterValue)
+ }
+ }
+
+ // 12. Return mimeType.
+ return mimeType
+}
+
+// https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
+// tests: https://fetch.spec.whatwg.org/#example-http-quoted-string
+/**
+ * @param {string} input
+ * @param {{ position: number }} position
+ * @param {boolean} [extractValue=false]
+ */
+function collectAnHTTPQuotedString (input, position, extractValue = false) {
+ // 1. Let positionStart be position.
+ const positionStart = position.position
+
+ // 2. Let value be the empty string.
+ let value = ''
+
+ // 3. Assert: the code point at position within input
+ // is U+0022 (").
+ assert(input[position.position] === '"')
+
+ // 4. Advance position by 1.
+ position.position++
+
+ // 5. While true:
+ while (true) {
+ // 1. Append the result of collecting a sequence of code points
+ // that are not U+0022 (") or U+005C (\) from input, given
+ // position, to value.
+ value += collectASequenceOfCodePoints(
+ (char) => char !== '"' && char !== '\\',
+ input,
+ position
+ )
+
+ // 2. If position is past the end of input, then break.
+ if (position.position >= input.length) {
+ break
+ }
+
+ // 3. Let quoteOrBackslash be the code point at position within
+ // input.
+ const quoteOrBackslash = input[position.position]
+
+ // 4. Advance position by 1.
+ position.position++
+
+ // 5. If quoteOrBackslash is U+005C (\), then:
+ if (quoteOrBackslash === '\\') {
+ // 1. If position is past the end of input, then append
+ // U+005C (\) to value and break.
+ if (position.position >= input.length) {
+ value += '\\'
+ break
+ }
+
+ // 2. Append the code point at position within input to value.
+ value += input[position.position]
+
+ // 3. Advance position by 1.
+ position.position++
+
+ // 6. Otherwise:
+ } else {
+ // 1. Assert: quoteOrBackslash is U+0022 (").
+ assert(quoteOrBackslash === '"')
+
+ // 2. Break.
+ break
+ }
+ }
+
+ // 6. If the extract-value flag is set, then return value.
+ if (extractValue) {
+ return value
+ }
+
+ // 7. Return the code points from positionStart to position,
+ // inclusive, within input.
+ return input.slice(positionStart, position.position)
+}
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#serialize-a-mime-type
+ */
+function serializeAMimeType (mimeType) {
+ assert(mimeType !== 'failure')
+ const { parameters, essence } = mimeType
+
+ // 1. Let serialization be the concatenation of mimeType’s
+ // type, U+002F (/), and mimeType’s subtype.
+ let serialization = essence
+
+ // 2. For each name → value of mimeType’s parameters:
+ for (let [name, value] of parameters.entries()) {
+ // 1. Append U+003B (;) to serialization.
+ serialization += ';'
+
+ // 2. Append name to serialization.
+ serialization += name
+
+ // 3. Append U+003D (=) to serialization.
+ serialization += '='
+
+ // 4. If value does not solely contain HTTP token code
+ // points or value is the empty string, then:
+ if (!HTTP_TOKEN_CODEPOINTS.test(value)) {
+ // 1. Precede each occurrence of U+0022 (") or
+ // U+005C (\) in value with U+005C (\).
+ value = value.replace(/[\\"]/ug, '\\$&')
+
+ // 2. Prepend U+0022 (") to value.
+ value = '"' + value
+
+ // 3. Append U+0022 (") to value.
+ value += '"'
+ }
+
+ // 5. Append value to serialization.
+ serialization += value
+ }
+
+ // 3. Return serialization.
+ return serialization
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-whitespace
+ * @param {number} char
+ */
+function isHTTPWhiteSpace (char) {
+ // "\r\n\t "
+ return char === 0x00d || char === 0x00a || char === 0x009 || char === 0x020
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-whitespace
+ * @param {string} str
+ * @param {boolean} [leading=true]
+ * @param {boolean} [trailing=true]
+ */
+function removeHTTPWhitespace (str, leading = true, trailing = true) {
+ return removeChars(str, leading, trailing, isHTTPWhiteSpace)
+}
+
+/**
+ * @see https://mimesniff.spec.whatwg.org/#minimize-a-supported-mime-type
+ * @param {Exclude<ReturnType<typeof parseMIMEType>, 'failure'>} mimeType
+ */
+function minimizeSupportedMimeType (mimeType) {
+ switch (mimeType.essence) {
+ case 'application/ecmascript':
+ case 'application/javascript':
+ case 'application/x-ecmascript':
+ case 'application/x-javascript':
+ case 'text/ecmascript':
+ case 'text/javascript':
+ case 'text/javascript1.0':
+ case 'text/javascript1.1':
+ case 'text/javascript1.2':
+ case 'text/javascript1.3':
+ case 'text/javascript1.4':
+ case 'text/javascript1.5':
+ case 'text/jscript':
+ case 'text/livescript':
+ case 'text/x-ecmascript':
+ case 'text/x-javascript':
+ // 1. If mimeType is a JavaScript MIME type, then return "text/javascript".
+ return 'text/javascript'
+ case 'application/json':
+ case 'text/json':
+ // 2. If mimeType is a JSON MIME type, then return "application/json".
+ return 'application/json'
+ case 'image/svg+xml':
+ // 3. If mimeType’s essence is "image/svg+xml", then return "image/svg+xml".
+ return 'image/svg+xml'
+ case 'text/xml':
+ case 'application/xml':
+ // 4. If mimeType is an XML MIME type, then return "application/xml".
+ return 'application/xml'
+ }
+
+ // 2. If mimeType is a JSON MIME type, then return "application/json".
+ if (mimeType.subtype.endsWith('+json')) {
+ return 'application/json'
+ }
+
+ // 4. If mimeType is an XML MIME type, then return "application/xml".
+ if (mimeType.subtype.endsWith('+xml')) {
+ return 'application/xml'
+ }
+
+ // 5. If mimeType is supported by the user agent, then return mimeType’s essence.
+ // Technically, node doesn't support any mimetypes.
+
+ // 6. Return the empty string.
+ return ''
+}
+
+module.exports = {
+ dataURLProcessor,
+ URLSerializer,
+ stringPercentDecode,
+ parseMIMEType,
+ collectAnHTTPQuotedString,
+ serializeAMimeType,
+ removeHTTPWhitespace,
+ minimizeSupportedMimeType,
+ HTTP_TOKEN_CODEPOINTS
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js b/vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js
new file mode 100644
index 0000000..4ba204c
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/formdata-parser.js
@@ -0,0 +1,575 @@
+'use strict'
+
+const { bufferToLowerCasedHeaderName } = require('../../core/util')
+const { HTTP_TOKEN_CODEPOINTS } = require('./data-url')
+const { makeEntry } = require('./formdata')
+const { webidl } = require('../webidl')
+const assert = require('node:assert')
+const { isomorphicDecode } = require('../infra')
+const { utf8DecodeBytes } = require('../../encoding')
+
+const dd = Buffer.from('--')
+const decoder = new TextDecoder()
+
+/**
+ * @param {string} chars
+ */
+function isAsciiString (chars) {
+ for (let i = 0; i < chars.length; ++i) {
+ if ((chars.charCodeAt(i) & ~0x7F) !== 0) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-boundary
+ * @param {string} boundary
+ */
+function validateBoundary (boundary) {
+ const length = boundary.length
+
+ // - its length is greater or equal to 27 and lesser or equal to 70, and
+ if (length < 27 || length > 70) {
+ return false
+ }
+
+ // - it is composed by bytes in the ranges 0x30 to 0x39, 0x41 to 0x5A, or
+ // 0x61 to 0x7A, inclusive (ASCII alphanumeric), or which are 0x27 ('),
+ // 0x2D (-) or 0x5F (_).
+ for (let i = 0; i < length; ++i) {
+ const cp = boundary.charCodeAt(i)
+
+ if (!(
+ (cp >= 0x30 && cp <= 0x39) ||
+ (cp >= 0x41 && cp <= 0x5a) ||
+ (cp >= 0x61 && cp <= 0x7a) ||
+ cp === 0x27 ||
+ cp === 0x2d ||
+ cp === 0x5f
+ )) {
+ return false
+ }
+ }
+
+ return true
+}
+
+/**
+ * @see https://andreubotella.github.io/multipart-form-data/#multipart-form-data-parser
+ * @param {Buffer} input
+ * @param {ReturnType<import('./data-url')['parseMIMEType']>} mimeType
+ */
+function multipartFormDataParser (input, mimeType) {
+ // 1. Assert: mimeType’s essence is "multipart/form-data".
+ assert(mimeType !== 'failure' && mimeType.essence === 'multipart/form-data')
+
+ const boundaryString = mimeType.parameters.get('boundary')
+
+ // 2. If mimeType’s parameters["boundary"] does not exist, return failure.
+ // Otherwise, let boundary be the result of UTF-8 decoding mimeType’s
+ // parameters["boundary"].
+ if (boundaryString === undefined) {
+ throw parsingError('missing boundary in content-type header')
+ }
+
+ const boundary = Buffer.from(`--${boundaryString}`, 'utf8')
+
+ // 3. Let entry list be an empty entry list.
+ const entryList = []
+
+ // 4. Let position be a pointer to a byte in input, initially pointing at
+ // the first byte.
+ const position = { position: 0 }
+
+ // Note: Per RFC 2046 Section 5.1.1, we must ignore anything before the
+ // first boundary delimiter line (preamble). Search for the first boundary.
+ const firstBoundaryIndex = input.indexOf(boundary)
+
+ if (firstBoundaryIndex === -1) {
+ throw parsingError('no boundary found in multipart body')
+ }
+
+ // Start parsing from the first boundary, ignoring any preamble
+ position.position = firstBoundaryIndex
+
+ // 5. While true:
+ while (true) {
+ // 5.1. If position points to a sequence of bytes starting with 0x2D 0x2D
+ // (`--`) followed by boundary, advance position by 2 + the length of
+ // boundary. Otherwise, return failure.
+ // Note: boundary is padded with 2 dashes already, no need to add 2.
+ if (input.subarray(position.position, position.position + boundary.length).equals(boundary)) {
+ position.position += boundary.length
+ } else {
+ throw parsingError('expected a value starting with -- and the boundary')
+ }
+
+ // 5.2. If position points to the sequence of bytes 0x2D 0x2D 0x0D 0x0A
+ // (`--` followed by CR LF) followed by the end of input, return entry list.
+ // Note: Per RFC 2046 Section 5.1.1, we must ignore anything after the
+ // final boundary delimiter (epilogue). Check for -- or --CRLF and return
+ // regardless of what follows.
+ if (bufferStartsWith(input, dd, position)) {
+ // Found closing boundary delimiter (--), ignore any epilogue
+ return entryList
+ }
+
+ // 5.3. If position does not point to a sequence of bytes starting with 0x0D
+ // 0x0A (CR LF), return failure.
+ if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
+ throw parsingError('expected CRLF')
+ }
+
+ // 5.4. Advance position by 2. (This skips past the newline.)
+ position.position += 2
+
+ // 5.5. Let name, filename and contentType be the result of parsing
+ // multipart/form-data headers on input and position, if the result
+ // is not failure. Otherwise, return failure.
+ const result = parseMultipartFormDataHeaders(input, position)
+
+ let { name, filename, contentType, encoding } = result
+
+ // 5.6. Advance position by 2. (This skips past the empty line that marks
+ // the end of the headers.)
+ position.position += 2
+
+ // 5.7. Let body be the empty byte sequence.
+ let body
+
+ // 5.8. Body loop: While position is not past the end of input:
+ // TODO: the steps here are completely wrong
+ {
+ const boundaryIndex = input.indexOf(boundary.subarray(2), position.position)
+
+ if (boundaryIndex === -1) {
+ throw parsingError('expected boundary after body')
+ }
+
+ body = input.subarray(position.position, boundaryIndex - 4)
+
+ position.position += body.length
+
+ // Note: position must be advanced by the body's length before being
+ // decoded, otherwise the parsing will fail.
+ if (encoding === 'base64') {
+ body = Buffer.from(body.toString(), 'base64')
+ }
+ }
+
+ // 5.9. If position does not point to a sequence of bytes starting with
+ // 0x0D 0x0A (CR LF), return failure. Otherwise, advance position by 2.
+ if (input[position.position] !== 0x0d || input[position.position + 1] !== 0x0a) {
+ throw parsingError('expected CRLF')
+ } else {
+ position.position += 2
+ }
+
+ // 5.10. If filename is not null:
+ let value
+
+ if (filename !== null) {
+ // 5.10.1. If contentType is null, set contentType to "text/plain".
+ contentType ??= 'text/plain'
+
+ // 5.10.2. If contentType is not an ASCII string, set contentType to the empty string.
+
+ // Note: `buffer.isAscii` can be used at zero-cost, but converting a string to a buffer is a high overhead.
+ // Content-Type is a relatively small string, so it is faster to use `String#charCodeAt`.
+ if (!isAsciiString(contentType)) {
+ contentType = ''
+ }
+
+ // 5.10.3. Let value be a new File object with name filename, type contentType, and body body.
+ value = new File([body], filename, { type: contentType })
+ } else {
+ // 5.11. Otherwise:
+
+ // 5.11.1. Let value be the UTF-8 decoding without BOM of body.
+ value = utf8DecodeBytes(Buffer.from(body))
+ }
+
+ // 5.12. Assert: name is a scalar value string and value is either a scalar value string or a File object.
+ assert(webidl.is.USVString(name))
+ assert((typeof value === 'string' && webidl.is.USVString(value)) || webidl.is.File(value))
+
+ // 5.13. Create an entry with name and value, and append it to entry list.
+ entryList.push(makeEntry(name, value, filename))
+ }
+}
+
+/**
+ * Parses content-disposition attributes (e.g., name="value" or filename*=utf-8''encoded)
+ * @param {Buffer} input
+ * @param {{ position: number }} position
+ * @returns {{ name: string, value: string }}
+ */
+function parseContentDispositionAttribute (input, position) {
+ // Skip leading semicolon and whitespace
+ if (input[position.position] === 0x3b /* ; */) {
+ position.position++
+ }
+
+ // Skip whitespace
+ collectASequenceOfBytes(
+ (char) => char === 0x20 || char === 0x09,
+ input,
+ position
+ )
+
+ // Collect attribute name (token characters)
+ const attributeName = collectASequenceOfBytes(
+ (char) => isToken(char) && char !== 0x3d && char !== 0x2a, // not = or *
+ input,
+ position
+ )
+
+ if (attributeName.length === 0) {
+ return null
+ }
+
+ const attrNameStr = attributeName.toString('ascii').toLowerCase()
+
+ // Check for extended notation (attribute*)
+ const isExtended = input[position.position] === 0x2a /* * */
+ if (isExtended) {
+ position.position++ // skip *
+ }
+
+ // Expect = sign
+ if (input[position.position] !== 0x3d /* = */) {
+ return null
+ }
+ position.position++ // skip =
+
+ // Skip whitespace
+ collectASequenceOfBytes(
+ (char) => char === 0x20 || char === 0x09,
+ input,
+ position
+ )
+
+ let value
+
+ if (isExtended) {
+ // Extended attribute format: charset'language'encoded-value
+ const headerValue = collectASequenceOfBytes(
+ (char) => char !== 0x20 && char !== 0x0d && char !== 0x0a && char !== 0x3b, // not space, CRLF, or ;
+ input,
+ position
+ )
+
+ // Check for utf-8'' prefix (case insensitive)
+ if (
+ (headerValue[0] !== 0x75 && headerValue[0] !== 0x55) || // u or U
+ (headerValue[1] !== 0x74 && headerValue[1] !== 0x54) || // t or T
+ (headerValue[2] !== 0x66 && headerValue[2] !== 0x46) || // f or F
+ headerValue[3] !== 0x2d || // -
+ headerValue[4] !== 0x38 // 8
+ ) {
+ throw parsingError('unknown encoding, expected utf-8\'\'')
+ }
+
+ // Skip utf-8'' and decode the rest
+ value = decodeURIComponent(decoder.decode(headerValue.subarray(7)))
+ } else if (input[position.position] === 0x22 /* " */) {
+ // Quoted string
+ position.position++ // skip opening quote
+
+ const quotedValue = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d && char !== 0x22, // not LF, CR, or "
+ input,
+ position
+ )
+
+ if (input[position.position] !== 0x22) {
+ throw parsingError('Closing quote not found')
+ }
+ position.position++ // skip closing quote
+
+ value = decoder.decode(quotedValue)
+ .replace(/%0A/ig, '\n')
+ .replace(/%0D/ig, '\r')
+ .replace(/%22/g, '"')
+ } else {
+ // Token value (no quotes)
+ const tokenValue = collectASequenceOfBytes(
+ (char) => isToken(char) && char !== 0x3b, // not ;
+ input,
+ position
+ )
+
+ value = decoder.decode(tokenValue)
+ }
+
+ return { name: attrNameStr, value }
+}
+
+/**
+ * @see https://andreubotella.github.io/multipart-form-data/#parse-multipart-form-data-headers
+ * @param {Buffer} input
+ * @param {{ position: number }} position
+ */
+function parseMultipartFormDataHeaders (input, position) {
+ // 1. Let name, filename and contentType be null.
+ let name = null
+ let filename = null
+ let contentType = null
+ let encoding = null
+
+ // 2. While true:
+ while (true) {
+ // 2.1. If position points to a sequence of bytes starting with 0x0D 0x0A (CR LF):
+ if (input[position.position] === 0x0d && input[position.position + 1] === 0x0a) {
+ // 2.1.1. If name is null, return failure.
+ if (name === null) {
+ throw parsingError('header name is null')
+ }
+
+ // 2.1.2. Return name, filename and contentType.
+ return { name, filename, contentType, encoding }
+ }
+
+ // 2.2. Let header name be the result of collecting a sequence of bytes that are
+ // not 0x0A (LF), 0x0D (CR) or 0x3A (:), given position.
+ let headerName = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d && char !== 0x3a,
+ input,
+ position
+ )
+
+ // 2.3. Remove any HTTP tab or space bytes from the start or end of header name.
+ headerName = removeChars(headerName, true, true, (char) => char === 0x9 || char === 0x20)
+
+ // 2.4. If header name does not match the field-name token production, return failure.
+ if (!HTTP_TOKEN_CODEPOINTS.test(headerName.toString())) {
+ throw parsingError('header name does not match the field-name token production')
+ }
+
+ // 2.5. If the byte at position is not 0x3A (:), return failure.
+ if (input[position.position] !== 0x3a) {
+ throw parsingError('expected :')
+ }
+
+ // 2.6. Advance position by 1.
+ position.position++
+
+ // 2.7. Collect a sequence of bytes that are HTTP tab or space bytes given position.
+ // (Do nothing with those bytes.)
+ collectASequenceOfBytes(
+ (char) => char === 0x20 || char === 0x09,
+ input,
+ position
+ )
+
+ // 2.8. Byte-lowercase header name and switch on the result:
+ switch (bufferToLowerCasedHeaderName(headerName)) {
+ case 'content-disposition': {
+ name = filename = null
+
+ // Collect the disposition type (should be "form-data")
+ const dispositionType = collectASequenceOfBytes(
+ (char) => isToken(char),
+ input,
+ position
+ )
+
+ if (dispositionType.toString('ascii').toLowerCase() !== 'form-data') {
+ throw parsingError('expected form-data for content-disposition header')
+ }
+
+ // Parse attributes recursively until CRLF
+ while (
+ position.position < input.length &&
+ input[position.position] !== 0x0d &&
+ input[position.position + 1] !== 0x0a
+ ) {
+ const attribute = parseContentDispositionAttribute(input, position)
+
+ if (!attribute) {
+ break
+ }
+
+ if (attribute.name === 'name') {
+ name = attribute.value
+ } else if (attribute.name === 'filename') {
+ filename = attribute.value
+ }
+ }
+
+ if (name === null) {
+ throw parsingError('name attribute is required in content-disposition header')
+ }
+
+ break
+ }
+ case 'content-type': {
+ // 1. Let header value be the result of collecting a sequence of bytes that are
+ // not 0x0A (LF) or 0x0D (CR), given position.
+ let headerValue = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d,
+ input,
+ position
+ )
+
+ // 2. Remove any HTTP tab or space bytes from the end of header value.
+ headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
+
+ // 3. Set contentType to the isomorphic decoding of header value.
+ contentType = isomorphicDecode(headerValue)
+
+ break
+ }
+ case 'content-transfer-encoding': {
+ let headerValue = collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d,
+ input,
+ position
+ )
+
+ headerValue = removeChars(headerValue, false, true, (char) => char === 0x9 || char === 0x20)
+
+ encoding = isomorphicDecode(headerValue)
+
+ break
+ }
+ default: {
+ // Collect a sequence of bytes that are not 0x0A (LF) or 0x0D (CR), given position.
+ // (Do nothing with those bytes.)
+ collectASequenceOfBytes(
+ (char) => char !== 0x0a && char !== 0x0d,
+ input,
+ position
+ )
+ }
+ }
+
+ // 2.9. If position does not point to a sequence of bytes starting with 0x0D 0x0A
+ // (CR LF), return failure. Otherwise, advance position by 2 (past the newline).
+ if (input[position.position] !== 0x0d && input[position.position + 1] !== 0x0a) {
+ throw parsingError('expected CRLF')
+ } else {
+ position.position += 2
+ }
+ }
+}
+
+/**
+ * @param {(char: number) => boolean} condition
+ * @param {Buffer} input
+ * @param {{ position: number }} position
+ */
+function collectASequenceOfBytes (condition, input, position) {
+ let start = position.position
+
+ while (start < input.length && condition(input[start])) {
+ ++start
+ }
+
+ return input.subarray(position.position, (position.position = start))
+}
+
+/**
+ * @param {Buffer} buf
+ * @param {boolean} leading
+ * @param {boolean} trailing
+ * @param {(charCode: number) => boolean} predicate
+ * @returns {Buffer}
+ */
+function removeChars (buf, leading, trailing, predicate) {
+ let lead = 0
+ let trail = buf.length - 1
+
+ if (leading) {
+ while (lead < buf.length && predicate(buf[lead])) lead++
+ }
+
+ if (trailing) {
+ while (trail > 0 && predicate(buf[trail])) trail--
+ }
+
+ return lead === 0 && trail === buf.length - 1 ? buf : buf.subarray(lead, trail + 1)
+}
+
+/**
+ * Checks if {@param buffer} starts with {@param start}
+ * @param {Buffer} buffer
+ * @param {Buffer} start
+ * @param {{ position: number }} position
+ */
+function bufferStartsWith (buffer, start, position) {
+ if (buffer.length < start.length) {
+ return false
+ }
+
+ for (let i = 0; i < start.length; i++) {
+ if (start[i] !== buffer[position.position + i]) {
+ return false
+ }
+ }
+
+ return true
+}
+
+function parsingError (cause) {
+ return new TypeError('Failed to parse body as FormData.', { cause: new TypeError(cause) })
+}
+
+/**
+ * CTL = <any US-ASCII control character
+ * (octets 0 - 31) and DEL (127)>
+ * @param {number} char
+ */
+function isCTL (char) {
+ return char <= 0x1f || char === 0x7f
+}
+
+/**
+ * tspecials := "(" / ")" / "<" / ">" / "@" /
+ * "," / ";" / ":" / "\" / <">
+ * "/" / "[" / "]" / "?" / "="
+ * ; Must be in quoted-string,
+ * ; to use within parameter values
+ * @param {number} char
+ */
+function isTSpecial (char) {
+ return (
+ char === 0x28 || // (
+ char === 0x29 || // )
+ char === 0x3c || // <
+ char === 0x3e || // >
+ char === 0x40 || // @
+ char === 0x2c || // ,
+ char === 0x3b || // ;
+ char === 0x3a || // :
+ char === 0x5c || // \
+ char === 0x22 || // "
+ char === 0x2f || // /
+ char === 0x5b || // [
+ char === 0x5d || // ]
+ char === 0x3f || // ?
+ char === 0x3d // +
+ )
+}
+
+/**
+ * token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
+ * or tspecials>
+ * @param {number} char
+ */
+function isToken (char) {
+ return (
+ char <= 0x7f && // ascii
+ char !== 0x20 && // space
+ char !== 0x09 &&
+ !isCTL(char) &&
+ !isTSpecial(char)
+ )
+}
+
+module.exports = {
+ multipartFormDataParser,
+ validateBoundary
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/formdata.js b/vanilla/node_modules/undici/lib/web/fetch/formdata.js
new file mode 100644
index 0000000..c21fb06
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/formdata.js
@@ -0,0 +1,259 @@
+'use strict'
+
+const { iteratorMixin } = require('./util')
+const { kEnumerableProperty } = require('../../core/util')
+const { webidl } = require('../webidl')
+const nodeUtil = require('node:util')
+
+// https://xhr.spec.whatwg.org/#formdata
+class FormData {
+ #state = []
+
+ constructor (form = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (form !== undefined) {
+ throw webidl.errors.conversionFailed({
+ prefix: 'FormData constructor',
+ argument: 'Argument 1',
+ types: ['undefined']
+ })
+ }
+ }
+
+ append (name, value, filename = undefined) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.append'
+ webidl.argumentLengthCheck(arguments, 2, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ if (arguments.length === 3 || webidl.is.Blob(value)) {
+ value = webidl.converters.Blob(value, prefix, 'value')
+
+ if (filename !== undefined) {
+ filename = webidl.converters.USVString(filename)
+ }
+ } else {
+ value = webidl.converters.USVString(value)
+ }
+
+ // 1. Let value be value if given; otherwise blobValue.
+
+ // 2. Let entry be the result of creating an entry with
+ // name, value, and filename if given.
+ const entry = makeEntry(name, value, filename)
+
+ // 3. Append entry to this’s entry list.
+ this.#state.push(entry)
+ }
+
+ delete (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.delete'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ // The delete(name) method steps are to remove all entries whose name
+ // is name from this’s entry list.
+ this.#state = this.#state.filter(entry => entry.name !== name)
+ }
+
+ get (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.get'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ // 1. If there is no entry whose name is name in this’s entry list,
+ // then return null.
+ const idx = this.#state.findIndex((entry) => entry.name === name)
+ if (idx === -1) {
+ return null
+ }
+
+ // 2. Return the value of the first entry whose name is name from
+ // this’s entry list.
+ return this.#state[idx].value
+ }
+
+ getAll (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.getAll'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ 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.
+ // 2. Return the values of all entries whose name is name, in order,
+ // from this’s entry list.
+ return this.#state
+ .filter((entry) => entry.name === name)
+ .map((entry) => entry.value)
+ }
+
+ has (name) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.has'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ 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.#state.findIndex((entry) => entry.name === name) !== -1
+ }
+
+ set (name, value, filename = undefined) {
+ webidl.brandCheck(this, FormData)
+
+ const prefix = 'FormData.set'
+ webidl.argumentLengthCheck(arguments, 2, prefix)
+
+ name = webidl.converters.USVString(name)
+
+ if (arguments.length === 3 || webidl.is.Blob(value)) {
+ value = webidl.converters.Blob(value, prefix, 'value')
+
+ if (filename !== undefined) {
+ filename = webidl.converters.USVString(filename)
+ }
+ } else {
+ value = webidl.converters.USVString(value)
+ }
+
+ // The set(name, value) and set(name, blobValue, filename) method steps
+ // are:
+
+ // 1. Let value be value if given; otherwise blobValue.
+
+ // 2. Let entry be the result of creating an entry with name, value, and
+ // filename if given.
+ const entry = makeEntry(name, value, filename)
+
+ // 3. If there are entries in this’s entry list whose name is name, then
+ // replace the first such entry with entry and remove the others.
+ const idx = this.#state.findIndex((entry) => entry.name === name)
+ if (idx !== -1) {
+ this.#state = [
+ ...this.#state.slice(0, idx),
+ entry,
+ ...this.#state.slice(idx + 1).filter((entry) => entry.name !== name)
+ ]
+ } else {
+ // 4. Otherwise, append entry to this’s entry list.
+ this.#state.push(entry)
+ }
+ }
+
+ [nodeUtil.inspect.custom] (depth, options) {
+ const state = this.#state.reduce((a, b) => {
+ if (a[b.name]) {
+ if (Array.isArray(a[b.name])) {
+ a[b.name].push(b.value)
+ } else {
+ a[b.name] = [a[b.name], b.value]
+ }
+ } else {
+ a[b.name] = b.value
+ }
+
+ return a
+ }, { __proto__: null })
+
+ options.depth ??= depth
+ options.colors ??= true
+
+ const output = nodeUtil.formatWithOptions(options, state)
+
+ // remove [Object null prototype]
+ return `FormData ${output.slice(output.indexOf(']') + 2)}`
+ }
+
+ /**
+ * @param {FormData} formData
+ */
+ static getFormDataState (formData) {
+ return formData.#state
+ }
+
+ /**
+ * @param {FormData} formData
+ * @param {any[]} newState
+ */
+ static setFormDataState (formData, newState) {
+ formData.#state = newState
+ }
+}
+
+const { getFormDataState, setFormDataState } = FormData
+Reflect.deleteProperty(FormData, 'getFormDataState')
+Reflect.deleteProperty(FormData, 'setFormDataState')
+
+iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')
+
+Object.defineProperties(FormData.prototype, {
+ append: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ get: kEnumerableProperty,
+ getAll: kEnumerableProperty,
+ has: kEnumerableProperty,
+ set: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'FormData',
+ configurable: true
+ }
+})
+
+/**
+ * @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) {
+ // 1. Set name to the result of converting name into a scalar value string.
+ // Note: This operation was done by the webidl converter USVString.
+
+ // 2. If value is a string, then set value to the result of converting
+ // value into a scalar value string.
+ if (typeof value === 'string') {
+ // Note: This operation was done by the webidl converter USVString.
+ } 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 (!webidl.is.File(value)) {
+ value = new File([value], 'blob', { type: value.type })
+ }
+
+ // 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) {
+ /** @type {FilePropertyBag} */
+ const options = {
+ type: value.type,
+ lastModified: value.lastModified
+ }
+
+ value = new File([value], filename, options)
+ }
+ }
+
+ // 4. Return an entry whose name is name and whose value is value.
+ return { name, value }
+}
+
+webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)
+
+module.exports = { FormData, makeEntry, setFormDataState }
diff --git a/vanilla/node_modules/undici/lib/web/fetch/global.js b/vanilla/node_modules/undici/lib/web/fetch/global.js
new file mode 100644
index 0000000..1df6f12
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/global.js
@@ -0,0 +1,40 @@
+'use strict'
+
+// In case of breaking changes, increase the version
+// number to avoid conflicts.
+const globalOrigin = Symbol.for('undici.globalOrigin.1')
+
+function getGlobalOrigin () {
+ return globalThis[globalOrigin]
+}
+
+function setGlobalOrigin (newOrigin) {
+ if (newOrigin === undefined) {
+ Object.defineProperty(globalThis, globalOrigin, {
+ value: undefined,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+
+ return
+ }
+
+ const parsedURL = new URL(newOrigin)
+
+ if (parsedURL.protocol !== 'http:' && parsedURL.protocol !== 'https:') {
+ throw new TypeError(`Only http & https urls are allowed, received ${parsedURL.protocol}`)
+ }
+
+ Object.defineProperty(globalThis, globalOrigin, {
+ value: parsedURL,
+ writable: true,
+ enumerable: false,
+ configurable: false
+ })
+}
+
+module.exports = {
+ getGlobalOrigin,
+ setGlobalOrigin
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/headers.js b/vanilla/node_modules/undici/lib/web/fetch/headers.js
new file mode 100644
index 0000000..024d198
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/headers.js
@@ -0,0 +1,719 @@
+// https://github.com/Ethan-Arrowood/undici-fetch
+
+'use strict'
+
+const { kConstruct } = require('../../core/symbols')
+const { kEnumerableProperty } = require('../../core/util')
+const {
+ iteratorMixin,
+ isValidHeaderName,
+ isValidHeaderValue
+} = require('./util')
+const { webidl } = require('../webidl')
+const assert = require('node:assert')
+const util = require('node:util')
+
+/**
+ * @param {number} code
+ * @returns {code is (0x0a | 0x0d | 0x09 | 0x20)}
+ */
+function isHTTPWhiteSpaceCharCode (code) {
+ return code === 0x0a || code === 0x0d || code === 0x09 || code === 0x20
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-header-value-normalize
+ * @param {string} potentialValue
+ * @returns {string}
+ */
+function headerValueNormalize (potentialValue) {
+ // To normalize a byte sequence potentialValue, remove
+ // any leading and trailing HTTP whitespace bytes from
+ // potentialValue.
+ let i = 0; let j = potentialValue.length
+
+ while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(j - 1))) --j
+ while (j > i && isHTTPWhiteSpaceCharCode(potentialValue.charCodeAt(i))) ++i
+
+ return i === 0 && j === potentialValue.length ? potentialValue : potentialValue.substring(i, j)
+}
+
+/**
+ * @param {Headers} headers
+ * @param {Array|Object} object
+ */
+function fill (headers, object) {
+ // To fill a Headers object headers with a given object object, run these steps:
+
+ // 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 (let i = 0; i < object.length; ++i) {
+ const header = object[i]
+ // 1. If header does not contain exactly two items, then throw a TypeError.
+ if (header.length !== 2) {
+ throw webidl.errors.exception({
+ 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.
+ appendHeader(headers, header[0], header[1])
+ }
+ } else if (typeof object === 'object' && object !== null) {
+ // Note: null should throw
+
+ // 2. Otherwise, object is a record, then for each key → value in object,
+ // append (key, value) to headers
+ const keys = Object.keys(object)
+ for (let i = 0; i < keys.length; ++i) {
+ appendHeader(headers, keys[i], object[keys[i]])
+ }
+ } else {
+ throw webidl.errors.conversionFailed({
+ prefix: 'Headers constructor',
+ argument: 'Argument 1',
+ types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
+ })
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-headers-append
+ * @param {Headers} headers
+ * @param {string} name
+ * @param {string} value
+ */
+function appendHeader (headers, name, value) {
+ // 1. Normalize value.
+ value = headerValueNormalize(value)
+
+ // 2. If name is not a header name or value is not a
+ // header value, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.append',
+ value: name,
+ type: 'header name'
+ })
+ } else if (!isValidHeaderValue(value)) {
+ throw webidl.errors.invalidArgument({
+ prefix: 'Headers.append',
+ value,
+ type: 'header value'
+ })
+ }
+
+ // 3. If headers’s guard is "immutable", then throw a TypeError.
+ // 4. Otherwise, if headers’s guard is "request" and name is a
+ // forbidden header name, return.
+ // 5. Otherwise, if headers’s guard is "request-no-cors":
+ // TODO
+ // Note: undici does not implement forbidden header names
+ if (getHeadersGuard(headers) === 'immutable') {
+ throw new TypeError('immutable')
+ }
+
+ // 6. Otherwise, if headers’s guard is "response" and name is a
+ // forbidden response-header name, return.
+
+ // 7. Append (name, value) to headers’s header list.
+ return getHeadersList(headers).append(name, value, false)
+
+ // 8. If headers’s guard is "request-no-cors", then remove
+ // privileged no-CORS request headers from headers
+}
+
+// https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+/**
+ * @param {Headers} target
+ */
+function headersListSortAndCombine (target) {
+ const headersList = getHeadersList(target)
+
+ if (!headersList) {
+ return []
+ }
+
+ if (headersList.sortedMap) {
+ return headersList.sortedMap
+ }
+
+ // 1. Let headers be an empty list of headers with the key being the name
+ // and value the value.
+ const headers = []
+
+ // 2. Let names be the result of convert header names to a sorted-lowercase
+ // set with all the names of the headers in list.
+ const names = headersList.toSortedArray()
+
+ const cookies = headersList.cookies
+
+ // fast-path
+ if (cookies === null || cookies.length === 1) {
+ // Note: The non-null assertion of value has already been done by `HeadersList#toSortedArray`
+ return (headersList.sortedMap = names)
+ }
+
+ // 3. For each name of names:
+ for (let i = 0; i < names.length; ++i) {
+ const { 0: name, 1: value } = names[i]
+ // 1. If name is `set-cookie`, then:
+ if (name === 'set-cookie') {
+ // 1. Let values be a list of all values of headers in list whose name
+ // is a byte-case-insensitive match for name, in order.
+
+ // 2. For each value of values:
+ // 1. Append (name, value) to headers.
+ for (let j = 0; j < cookies.length; ++j) {
+ headers.push([name, cookies[j]])
+ }
+ } else {
+ // 2. Otherwise:
+
+ // 1. Let value be the result of getting name from list.
+
+ // 2. Assert: value is non-null.
+ // Note: This operation was done by `HeadersList#toSortedArray`.
+
+ // 3. Append (name, value) to headers.
+ headers.push([name, value])
+ }
+ }
+
+ // 4. Return headers.
+ return (headersList.sortedMap = headers)
+}
+
+function compareHeaderName (a, b) {
+ return a[0] < b[0] ? -1 : 1
+}
+
+class HeadersList {
+ /** @type {[string, string][]|null} */
+ cookies = null
+
+ sortedMap
+ headersMap
+
+ constructor (init) {
+ if (init instanceof HeadersList) {
+ this.headersMap = new Map(init.headersMap)
+ this.sortedMap = init.sortedMap
+ this.cookies = init.cookies === null ? null : [...init.cookies]
+ } else {
+ this.headersMap = new Map(init)
+ this.sortedMap = null
+ }
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#header-list-contains
+ * @param {string} name
+ * @param {boolean} isLowerCase
+ */
+ contains (name, isLowerCase) {
+ // A header list list contains a header name name if list
+ // contains a header whose name is a byte-case-insensitive
+ // match for name.
+
+ return this.headersMap.has(isLowerCase ? name : name.toLowerCase())
+ }
+
+ clear () {
+ this.headersMap.clear()
+ this.sortedMap = null
+ this.cookies = null
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-append
+ * @param {string} name
+ * @param {string} value
+ * @param {boolean} isLowerCase
+ */
+ append (name, value, isLowerCase) {
+ this.sortedMap = null
+
+ // 1. If list contains name, then set name to the first such
+ // header’s name.
+ const lowercaseName = isLowerCase ? name : name.toLowerCase()
+ const exists = this.headersMap.get(lowercaseName)
+
+ // 2. Append (name, value) to list.
+ if (exists) {
+ const delimiter = lowercaseName === 'cookie' ? '; ' : ', '
+ this.headersMap.set(lowercaseName, {
+ name: exists.name,
+ value: `${exists.value}${delimiter}${value}`
+ })
+ } else {
+ this.headersMap.set(lowercaseName, { name, value })
+ }
+
+ if (lowercaseName === 'set-cookie') {
+ (this.cookies ??= []).push(value)
+ }
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-set
+ * @param {string} name
+ * @param {string} value
+ * @param {boolean} isLowerCase
+ */
+ set (name, value, isLowerCase) {
+ this.sortedMap = null
+ const lowercaseName = isLowerCase ? name : name.toLowerCase()
+
+ if (lowercaseName === 'set-cookie') {
+ this.cookies = [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.
+ this.headersMap.set(lowercaseName, { name, value })
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-delete
+ * @param {string} name
+ * @param {boolean} isLowerCase
+ */
+ delete (name, isLowerCase) {
+ this.sortedMap = null
+ if (!isLowerCase) name = name.toLowerCase()
+
+ if (name === 'set-cookie') {
+ this.cookies = null
+ }
+
+ this.headersMap.delete(name)
+ }
+
+ /**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-get
+ * @param {string} name
+ * @param {boolean} isLowerCase
+ * @returns {string | null}
+ */
+ get (name, isLowerCase) {
+ // 1. If list does not contain name, then return null.
+ // 2. Return the values of all headers in list whose name
+ // is a byte-case-insensitive match for name,
+ // separated from each other by 0x2C 0x20, in order.
+ return this.headersMap.get(isLowerCase ? name : name.toLowerCase())?.value ?? null
+ }
+
+ * [Symbol.iterator] () {
+ // use the lowercased name
+ for (const { 0: name, 1: { value } } of this.headersMap) {
+ yield [name, value]
+ }
+ }
+
+ get entries () {
+ const headers = {}
+
+ if (this.headersMap.size !== 0) {
+ for (const { name, value } of this.headersMap.values()) {
+ headers[name] = value
+ }
+ }
+
+ return headers
+ }
+
+ rawValues () {
+ return this.headersMap.values()
+ }
+
+ get entriesList () {
+ const headers = []
+
+ if (this.headersMap.size !== 0) {
+ for (const { 0: lowerName, 1: { name, value } } of this.headersMap) {
+ if (lowerName === 'set-cookie') {
+ for (const cookie of this.cookies) {
+ headers.push([name, cookie])
+ }
+ } else {
+ headers.push([name, value])
+ }
+ }
+ }
+
+ return headers
+ }
+
+ // https://fetch.spec.whatwg.org/#convert-header-names-to-a-sorted-lowercase-set
+ toSortedArray () {
+ const size = this.headersMap.size
+ const array = new Array(size)
+ // In most cases, you will use the fast-path.
+ // fast-path: Use binary insertion sort for small arrays.
+ if (size <= 32) {
+ if (size === 0) {
+ // If empty, it is an empty array. To avoid the first index assignment.
+ return array
+ }
+ // Improve performance by unrolling loop and avoiding double-loop.
+ // Double-loop-less version of the binary insertion sort.
+ const iterator = this.headersMap[Symbol.iterator]()
+ const firstValue = iterator.next().value
+ // set [name, value] to first index.
+ array[0] = [firstValue[0], firstValue[1].value]
+ // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+ // 3.2.2. Assert: value is non-null.
+ assert(firstValue[1].value !== null)
+ for (
+ let i = 1, j = 0, right = 0, left = 0, pivot = 0, x, value;
+ i < size;
+ ++i
+ ) {
+ // get next value
+ value = iterator.next().value
+ // set [name, value] to current index.
+ x = array[i] = [value[0], value[1].value]
+ // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+ // 3.2.2. Assert: value is non-null.
+ assert(x[1] !== null)
+ left = 0
+ right = i
+ // binary search
+ while (left < right) {
+ // middle index
+ pivot = left + ((right - left) >> 1)
+ // compare header name
+ if (array[pivot][0] <= x[0]) {
+ left = pivot + 1
+ } else {
+ right = pivot
+ }
+ }
+ if (i !== pivot) {
+ j = i
+ while (j > left) {
+ array[j] = array[--j]
+ }
+ array[left] = x
+ }
+ }
+ /* c8 ignore next 4 */
+ if (!iterator.next().done) {
+ // This is for debugging and will never be called.
+ throw new TypeError('Unreachable')
+ }
+ return array
+ } else {
+ // This case would be a rare occurrence.
+ // slow-path: fallback
+ let i = 0
+ for (const { 0: name, 1: { value } } of this.headersMap) {
+ array[i++] = [name, value]
+ // https://fetch.spec.whatwg.org/#concept-header-list-sort-and-combine
+ // 3.2.2. Assert: value is non-null.
+ assert(value !== null)
+ }
+ return array.sort(compareHeaderName)
+ }
+ }
+}
+
+// https://fetch.spec.whatwg.org/#headers-class
+class Headers {
+ #guard
+ /**
+ * @type {HeadersList}
+ */
+ #headersList
+
+ /**
+ * @param {HeadersInit|Symbol} [init]
+ * @returns
+ */
+ constructor (init = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (init === kConstruct) {
+ return
+ }
+
+ this.#headersList = new HeadersList()
+
+ // The new Headers(init) constructor steps are:
+
+ // 1. Set this’s guard to "none".
+ this.#guard = 'none'
+
+ // 2. If init is given, then fill this with init.
+ if (init !== undefined) {
+ init = webidl.converters.HeadersInit(init, 'Headers constructor', 'init')
+ fill(this, init)
+ }
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-append
+ append (name, value) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 2, 'Headers.append')
+
+ const prefix = 'Headers.append'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+ value = webidl.converters.ByteString(value, prefix, 'value')
+
+ return appendHeader(this, name, value)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-delete
+ delete (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, 'Headers.delete')
+
+ const prefix = 'Headers.delete'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw 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.#guard === 'immutable') {
+ throw new TypeError('immutable')
+ }
+
+ // 6. If this’s header list does not contain name, then
+ // return.
+ if (!this.#headersList.contains(name, false)) {
+ 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.
+ this.#headersList.delete(name, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-get
+ get (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, 'Headers.get')
+
+ const prefix = 'Headers.get'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. Return the result of getting name from this’s header
+ // list.
+ return this.#headersList.get(name, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-has
+ has (name) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 1, 'Headers.has')
+
+ const prefix = 'Headers.has'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+
+ // 1. If name is not a header name, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ value: name,
+ type: 'header name'
+ })
+ }
+
+ // 2. Return true if this’s header list contains name;
+ // otherwise false.
+ return this.#headersList.contains(name, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-set
+ set (name, value) {
+ webidl.brandCheck(this, Headers)
+
+ webidl.argumentLengthCheck(arguments, 2, 'Headers.set')
+
+ const prefix = 'Headers.set'
+ name = webidl.converters.ByteString(name, prefix, 'name')
+ value = webidl.converters.ByteString(value, prefix, 'value')
+
+ // 1. Normalize value.
+ value = headerValueNormalize(value)
+
+ // 2. If name is not a header name or value is not a
+ // header value, then throw a TypeError.
+ if (!isValidHeaderName(name)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ value: name,
+ type: 'header name'
+ })
+ } else if (!isValidHeaderValue(value)) {
+ throw webidl.errors.invalidArgument({
+ prefix,
+ 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.#guard === 'immutable') {
+ throw new TypeError('immutable')
+ }
+
+ // 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
+ this.#headersList.set(name, value, false)
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-headers-getsetcookie
+ getSetCookie () {
+ webidl.brandCheck(this, Headers)
+
+ // 1. If this’s header list does not contain `Set-Cookie`, then return « ».
+ // 2. Return the values of all headers in this’s header list whose name is
+ // a byte-case-insensitive match for `Set-Cookie`, in order.
+
+ const list = this.#headersList.cookies
+
+ if (list) {
+ return [...list]
+ }
+
+ return []
+ }
+
+ [util.inspect.custom] (depth, options) {
+ options.depth ??= depth
+
+ return `Headers ${util.formatWithOptions(options, this.#headersList.entries)}`
+ }
+
+ static getHeadersGuard (o) {
+ return o.#guard
+ }
+
+ static setHeadersGuard (o, guard) {
+ o.#guard = guard
+ }
+
+ /**
+ * @param {Headers} o
+ */
+ static getHeadersList (o) {
+ return o.#headersList
+ }
+
+ /**
+ * @param {Headers} target
+ * @param {HeadersList} list
+ */
+ static setHeadersList (target, list) {
+ target.#headersList = list
+ }
+}
+
+const { getHeadersGuard, setHeadersGuard, getHeadersList, setHeadersList } = Headers
+Reflect.deleteProperty(Headers, 'getHeadersGuard')
+Reflect.deleteProperty(Headers, 'setHeadersGuard')
+Reflect.deleteProperty(Headers, 'getHeadersList')
+Reflect.deleteProperty(Headers, 'setHeadersList')
+
+iteratorMixin('Headers', Headers, headersListSortAndCombine, 0, 1)
+
+Object.defineProperties(Headers.prototype, {
+ append: kEnumerableProperty,
+ delete: kEnumerableProperty,
+ get: kEnumerableProperty,
+ has: kEnumerableProperty,
+ set: kEnumerableProperty,
+ getSetCookie: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Headers',
+ configurable: true
+ },
+ [util.inspect.custom]: {
+ enumerable: false
+ }
+})
+
+webidl.converters.HeadersInit = function (V, prefix, argument) {
+ if (webidl.util.Type(V) === webidl.util.Types.OBJECT) {
+ const iterator = Reflect.get(V, Symbol.iterator)
+
+ // A work-around to ensure we send the properly-cased Headers when V is a Headers object.
+ // Read https://github.com/nodejs/undici/pull/3159#issuecomment-2075537226 before touching, please.
+ if (!util.types.isProxy(V) && iterator === Headers.prototype.entries) { // Headers object
+ try {
+ return getHeadersList(V).entriesList
+ } catch {
+ // fall-through
+ }
+ }
+
+ if (typeof iterator === 'function') {
+ return webidl.converters['sequence<sequence<ByteString>>'](V, prefix, argument, iterator.bind(V))
+ }
+
+ return webidl.converters['record<ByteString, ByteString>'](V, prefix, argument)
+ }
+
+ throw webidl.errors.conversionFailed({
+ prefix: 'Headers constructor',
+ argument: 'Argument 1',
+ types: ['sequence<sequence<ByteString>>', 'record<ByteString, ByteString>']
+ })
+}
+
+module.exports = {
+ fill,
+ // for test.
+ compareHeaderName,
+ Headers,
+ HeadersList,
+ getHeadersGuard,
+ setHeadersGuard,
+ setHeadersList,
+ getHeadersList
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/index.js b/vanilla/node_modules/undici/lib/web/fetch/index.js
new file mode 100644
index 0000000..f350035
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/index.js
@@ -0,0 +1,2372 @@
+// https://github.com/Ethan-Arrowood/undici-fetch
+
+'use strict'
+
+const {
+ makeNetworkError,
+ makeAppropriateNetworkError,
+ filterResponse,
+ makeResponse,
+ fromInnerResponse,
+ getResponseState
+} = require('./response')
+const { HeadersList } = require('./headers')
+const { Request, cloneRequest, getRequestDispatcher, getRequestState } = require('./request')
+const zlib = require('node:zlib')
+const {
+ makePolicyContainer,
+ clonePolicyContainer,
+ requestBadPort,
+ TAOCheck,
+ appendRequestOriginHeader,
+ responseLocationURL,
+ requestCurrentURL,
+ setRequestReferrerPolicyOnRedirect,
+ tryUpgradeRequestToAPotentiallyTrustworthyURL,
+ createOpaqueTimingInfo,
+ appendFetchMetadata,
+ corsCheck,
+ crossOriginResourcePolicyCheck,
+ determineRequestsReferrer,
+ coarsenedSharedCurrentTime,
+ sameOrigin,
+ isCancelled,
+ isAborted,
+ isErrorLike,
+ fullyReadBody,
+ readableStreamClose,
+ urlIsLocal,
+ urlIsHttpHttpsScheme,
+ urlHasHttpsScheme,
+ clampAndCoarsenConnectionTimingInfo,
+ simpleRangeHeaderValue,
+ buildContentRange,
+ createInflate,
+ extractMimeType,
+ hasAuthenticationEntry,
+ includesCredentials,
+ isTraversableNavigable
+} = require('./util')
+const assert = require('node:assert')
+const { safelyExtractBody, extractBody } = require('./body')
+const {
+ redirectStatusSet,
+ nullBodyStatus,
+ safeMethodsSet,
+ requestBodyHeader,
+ subresourceSet
+} = require('./constants')
+const EE = require('node:events')
+const { Readable, pipeline, finished, isErrored, isReadable } = require('node:stream')
+const { addAbortListener, bufferToLowerCasedHeaderName } = require('../../core/util')
+const { dataURLProcessor, serializeAMimeType, minimizeSupportedMimeType } = require('./data-url')
+const { getGlobalDispatcher } = require('../../global')
+const { webidl } = require('../webidl')
+const { STATUS_CODES } = require('node:http')
+const { bytesMatch } = require('../subresource-integrity/subresource-integrity')
+const { createDeferredPromise } = require('../../util/promise')
+const { isomorphicEncode } = require('../infra')
+const { runtimeFeatures } = require('../../util/runtime-features')
+
+// Node.js v23.8.0+ and v22.15.0+ supports Zstandard
+const hasZstd = runtimeFeatures.has('zstd')
+
+const GET_OR_HEAD = ['GET', 'HEAD']
+
+const defaultUserAgent = typeof __UNDICI_IS_NODE__ !== 'undefined' || typeof esbuildDetection !== 'undefined'
+ ? 'node'
+ : 'undici'
+
+/** @type {import('buffer').resolveObjectURL} */
+let resolveObjectURL
+
+class Fetch extends EE {
+ constructor (dispatcher) {
+ super()
+
+ this.dispatcher = dispatcher
+ this.connection = null
+ this.dump = false
+ this.state = 'ongoing'
+ }
+
+ terminate (reason) {
+ if (this.state !== 'ongoing') {
+ return
+ }
+
+ this.state = 'terminated'
+ this.connection?.destroy(reason)
+ this.emit('terminated', reason)
+ }
+
+ // https://fetch.spec.whatwg.org/#fetch-controller-abort
+ abort (error) {
+ if (this.state !== 'ongoing') {
+ return
+ }
+
+ // 1. Set controller’s state to "aborted".
+ this.state = 'aborted'
+
+ // 2. Let fallbackError be an "AbortError" DOMException.
+ // 3. Set error to fallbackError if it is not given.
+ if (!error) {
+ error = new DOMException('The operation was aborted.', 'AbortError')
+ }
+
+ // 4. Let serializedError be StructuredSerialize(error).
+ // If that threw an exception, catch it, and let
+ // serializedError be StructuredSerialize(fallbackError).
+
+ // 5. Set controller’s serialized abort reason to serializedError.
+ this.serializedAbortReason = error
+
+ this.connection?.destroy(error)
+ this.emit('terminated', error)
+ }
+}
+
+function handleFetchDone (response) {
+ finalizeAndReportTiming(response, 'fetch')
+}
+
+// https://fetch.spec.whatwg.org/#fetch-method
+function fetch (input, init = undefined) {
+ webidl.argumentLengthCheck(arguments, 1, 'globalThis.fetch')
+
+ // 1. Let p be a new promise.
+ let p = createDeferredPromise()
+
+ // 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.
+ 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 = getRequestState(requestObject)
+
+ // 4. If requestObject’s signal’s aborted flag is set, then:
+ if (requestObject.signal.aborted) {
+ // 1. Abort the fetch() call with p, request, null, and
+ // requestObject’s signal’s abort reason.
+ abortFetch(p, request, null, requestObject.signal.reason, null)
+
+ // 2. Return p.
+ return p.promise
+ }
+
+ // 5. Let globalObject be request’s client’s global object.
+ const globalObject = request.client.globalObject
+
+ // 6. If globalObject is a ServiceWorkerGlobalScope object, then set
+ // request’s service-workers mode to "none".
+ if (globalObject?.constructor?.name === 'ServiceWorkerGlobalScope') {
+ request.serviceWorkers = 'none'
+ }
+
+ // 7. Let responseObject be null.
+ let responseObject = null
+
+ // 8. Let relevantRealm be this’s relevant Realm.
+
+ // 9. Let locallyAborted be false.
+ let locallyAborted = false
+
+ // 10. Let controller be null.
+ let controller = null
+
+ // 11. Add the following abort steps to requestObject’s signal:
+ addAbortListener(
+ requestObject.signal,
+ () => {
+ // 1. Set locallyAborted to true.
+ locallyAborted = true
+
+ // 2. Assert: controller is non-null.
+ assert(controller != null)
+
+ // 3. Abort controller with requestObject’s signal’s abort reason.
+ controller.abort(requestObject.signal.reason)
+
+ const realResponse = responseObject?.deref()
+
+ // 4. Abort the fetch() call with p, request, responseObject,
+ // and requestObject’s signal’s abort reason.
+ abortFetch(p, request, realResponse, requestObject.signal.reason, controller.controller)
+ }
+ )
+
+ // 12. Let handleFetchDone given response response be to finalize and
+ // report timing with response, globalObject, and "fetch".
+ // see function handleFetchDone
+
+ // 13. Set controller to the result of calling fetch given request,
+ // with processResponseEndOfBody set to handleFetchDone, and processResponse
+ // given response being these substeps:
+
+ const processResponse = (response) => {
+ // 1. If locallyAborted is true, terminate these substeps.
+ if (locallyAborted) {
+ return
+ }
+
+ // 2. If response’s aborted flag is set, then:
+ if (response.aborted) {
+ // 1. Let deserializedError be the result of deserialize a serialized
+ // abort reason given controller’s serialized abort reason and
+ // relevantRealm.
+
+ // 2. Abort the fetch() call with p, request, responseObject, and
+ // deserializedError.
+
+ abortFetch(p, request, responseObject, controller.serializedAbortReason, controller.controller)
+ return
+ }
+
+ // 3. If response is a network error, then reject p with a TypeError
+ // and terminate these substeps.
+ if (response.type === 'error') {
+ p.reject(new TypeError('fetch failed', { cause: response.error }))
+ return
+ }
+
+ // 4. Set responseObject to the result of creating a Response object,
+ // given response, "immutable", and relevantRealm.
+ responseObject = new WeakRef(fromInnerResponse(response, 'immutable'))
+
+ // 5. Resolve p with responseObject.
+ p.resolve(responseObject.deref())
+ p = null
+ }
+
+ controller = fetching({
+ request,
+ processResponseEndOfBody: handleFetchDone,
+ processResponse,
+ dispatcher: getRequestDispatcher(requestObject) // undici
+ })
+
+ // 14. Return p.
+ return p.promise
+}
+
+// https://fetch.spec.whatwg.org/#finalize-and-report-timing
+function finalizeAndReportTiming (response, initiatorType = 'other') {
+ // 1. If response is an aborted network error, then return.
+ if (response.type === 'error' && response.aborted) {
+ return
+ }
+
+ // 2. If response’s URL list is null or empty, then return.
+ if (!response.urlList?.length) {
+ return
+ }
+
+ // 3. Let originalURL be response’s URL list[0].
+ const originalURL = response.urlList[0]
+
+ // 4. Let timingInfo be response’s timing info.
+ let timingInfo = response.timingInfo
+
+ // 5. Let cacheState be response’s cache state.
+ let cacheState = response.cacheState
+
+ // 6. If originalURL’s scheme is not an HTTP(S) scheme, then return.
+ if (!urlIsHttpHttpsScheme(originalURL)) {
+ return
+ }
+
+ // 7. If timingInfo is null, then return.
+ if (timingInfo === null) {
+ return
+ }
+
+ // 8. If response’s timing allow passed flag is not set, then:
+ if (!response.timingAllowPassed) {
+ // 1. Set timingInfo to a the result of creating an opaque timing info for timingInfo.
+ timingInfo = createOpaqueTimingInfo({
+ startTime: timingInfo.startTime
+ })
+
+ // 2. Set cacheState to the empty string.
+ cacheState = ''
+ }
+
+ // 9. Set timingInfo’s end time to the coarsened shared current time
+ // given global’s relevant settings object’s cross-origin isolated
+ // capability.
+ // TODO: given global’s relevant settings object’s cross-origin isolated
+ // capability?
+ timingInfo.endTime = coarsenedSharedCurrentTime()
+
+ // 10. Set response’s timing info to timingInfo.
+ response.timingInfo = timingInfo
+
+ // 11. Mark resource timing for timingInfo, originalURL, initiatorType,
+ // global, and cacheState.
+ markResourceTiming(
+ timingInfo,
+ originalURL.href,
+ initiatorType,
+ globalThis,
+ cacheState,
+ '', // bodyType
+ response.status
+ )
+}
+
+// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing
+const markResourceTiming = performance.markResourceTiming
+
+// https://fetch.spec.whatwg.org/#abort-fetch
+function abortFetch (p, request, responseObject, error, controller /* undici-specific */) {
+ // 1. Reject promise with error.
+ if (p) {
+ // We might have already resolved the promise at this stage
+ p.reject(error)
+ }
+
+ // 2. If request’s body is not null and is readable, then cancel request’s
+ // body with error.
+ if (request.body?.stream != null && isReadable(request.body.stream)) {
+ request.body.stream.cancel(error).catch((err) => {
+ if (err.code === 'ERR_INVALID_STATE') {
+ // Node bug?
+ return
+ }
+ throw err
+ })
+ }
+
+ // 3. If responseObject is null, then return.
+ if (responseObject == null) {
+ return
+ }
+
+ // 4. Let response be responseObject’s response.
+ const response = getResponseState(responseObject)
+
+ // 5. If response’s body is not null and is readable, then error response’s
+ // body with error.
+ if (response.body?.stream != null && isReadable(response.body.stream)) {
+ controller.error(error)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#fetching
+function fetching ({
+ request,
+ processRequestBodyChunkLength,
+ processRequestEndOfBody,
+ processResponse,
+ processResponseEndOfBody,
+ processResponseConsumeBody,
+ useParallelQueue = false,
+ dispatcher = getGlobalDispatcher() // undici
+}) {
+ // Ensure that the dispatcher is set accordingly
+ assert(dispatcher)
+
+ // 1. Let taskDestination be null.
+ let taskDestination = null
+
+ // 2. Let crossOriginIsolatedCapability be false.
+ let crossOriginIsolatedCapability = false
+
+ // 3. If request’s client is non-null, then:
+ if (request.client != null) {
+ // 1. Set taskDestination to request’s client’s global object.
+ taskDestination = request.client.globalObject
+
+ // 2. Set crossOriginIsolatedCapability to request’s client’s cross-origin
+ // isolated capability.
+ crossOriginIsolatedCapability =
+ request.client.crossOriginIsolatedCapability
+ }
+
+ // 4. If useParallelQueue is true, then set taskDestination to the result of
+ // starting a new parallel queue.
+ // TODO
+
+ // 5. Let timingInfo be a new fetch timing info whose start time and
+ // post-redirect start time are the coarsened shared current time given
+ // crossOriginIsolatedCapability.
+ const currentTime = coarsenedSharedCurrentTime(crossOriginIsolatedCapability)
+ const timingInfo = createOpaqueTimingInfo({
+ startTime: currentTime
+ })
+
+ // 6. Let fetchParams be a new fetch params whose
+ // request is request,
+ // timing info is timingInfo,
+ // process request body chunk length is processRequestBodyChunkLength,
+ // process request end-of-body is processRequestEndOfBody,
+ // process response is processResponse,
+ // process response consume body is processResponseConsumeBody,
+ // process response end-of-body is processResponseEndOfBody,
+ // task destination is taskDestination,
+ // and cross-origin isolated capability is crossOriginIsolatedCapability.
+ const fetchParams = {
+ controller: new Fetch(dispatcher),
+ request,
+ timingInfo,
+ processRequestBodyChunkLength,
+ processRequestEndOfBody,
+ processResponse,
+ processResponseConsumeBody,
+ processResponseEndOfBody,
+ taskDestination,
+ crossOriginIsolatedCapability
+ }
+
+ // 7. If request’s body is a byte sequence, then set request’s body to
+ // request’s body as a body.
+ // NOTE: Since fetching is only called from fetch, body should already be
+ // extracted.
+ assert(!request.body || request.body.stream)
+
+ // 8. If request’s window is "client", then set request’s window to request’s
+ // client, if request’s client’s global object is a Window object; otherwise
+ // "no-window".
+ if (request.window === 'client') {
+ // TODO: What if request.client is null?
+ request.window =
+ request.client?.globalObject?.constructor?.name === 'Window'
+ ? request.client
+ : 'no-window'
+ }
+
+ // 9. If request’s origin is "client", then set request’s origin to request’s
+ // client’s origin.
+ if (request.origin === 'client') {
+ request.origin = request.client.origin
+ }
+
+ // 10. If all of the following conditions are true:
+ // TODO
+
+ // 11. If request’s policy container is "client", then:
+ if (request.policyContainer === 'client') {
+ // 1. If request’s client is non-null, then set request’s policy
+ // container to a clone of request’s client’s policy container. [HTML]
+ if (request.client != null) {
+ request.policyContainer = clonePolicyContainer(
+ request.client.policyContainer
+ )
+ } else {
+ // 2. Otherwise, set request’s policy container to a new policy
+ // container.
+ request.policyContainer = makePolicyContainer()
+ }
+ }
+
+ // 12. If request’s header list does not contain `Accept`, then:
+ if (!request.headersList.contains('accept', true)) {
+ // 1. Let value be `*/*`.
+ const value = '*/*'
+
+ // 2. A user agent should set value to the first matching statement, if
+ // any, switching on request’s destination:
+ // "document"
+ // "frame"
+ // "iframe"
+ // `text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8`
+ // "image"
+ // `image/png,image/svg+xml,image/*;q=0.8,*/*;q=0.5`
+ // "style"
+ // `text/css,*/*;q=0.1`
+ // TODO
+
+ // 3. Append `Accept`/value to request’s header list.
+ request.headersList.append('accept', value, true)
+ }
+
+ // 13. If request’s header list does not contain `Accept-Language`, then
+ // user agents should append `Accept-Language`/an appropriate value to
+ // request’s header list.
+ if (!request.headersList.contains('accept-language', true)) {
+ request.headersList.append('accept-language', '*', true)
+ }
+
+ // 14. If request’s priority is null, then use request’s initiator and
+ // destination appropriately in setting request’s priority to a
+ // user-agent-defined object.
+ if (request.priority === null) {
+ // TODO
+ }
+
+ // 15. If request is a subresource request, then:
+ if (subresourceSet.has(request.destination)) {
+ // TODO
+ }
+
+ // 16. Run main fetch given fetchParams.
+ mainFetch(fetchParams, false)
+
+ // 17. Return fetchParam's controller
+ return fetchParams.controller
+}
+
+// https://fetch.spec.whatwg.org/#concept-main-fetch
+async function mainFetch (fetchParams, recursive) {
+ try {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. If request’s local-URLs-only flag is set and request’s current URL is
+ // not local, then set response to a network error.
+ if (request.localURLsOnly && !urlIsLocal(requestCurrentURL(request))) {
+ response = makeNetworkError('local URLs only')
+ }
+
+ // 4. Run report Content Security Policy violations for request.
+ // TODO
+
+ // 5. Upgrade request to a potentially trustworthy URL, if appropriate.
+ tryUpgradeRequestToAPotentiallyTrustworthyURL(request)
+
+ // 6. If should request be blocked due to a bad port, should fetching request
+ // be blocked as mixed content, or should request be blocked by Content
+ // Security Policy returns blocked, then set response to a network error.
+ if (requestBadPort(request) === 'blocked') {
+ response = makeNetworkError('bad port')
+ }
+ // TODO: should fetching request be blocked as mixed content?
+ // TODO: should request be blocked by Content Security Policy?
+
+ // 7. If request’s referrer policy is the empty string, then set request’s
+ // referrer policy to request’s policy container’s referrer policy.
+ if (request.referrerPolicy === '') {
+ request.referrerPolicy = request.policyContainer.referrerPolicy
+ }
+
+ // 8. If request’s referrer is not "no-referrer", then set request’s
+ // referrer to the result of invoking determine request’s referrer.
+ if (request.referrer !== 'no-referrer') {
+ request.referrer = determineRequestsReferrer(request)
+ }
+
+ // 9. Set request’s current URL’s scheme to "https" if all of the following
+ // conditions are true:
+ // - request’s current URL’s scheme is "http"
+ // - request’s current URL’s host is a domain
+ // - Matching request’s current URL’s host per Known HSTS Host Domain Name
+ // Matching results in either a superdomain match with an asserted
+ // includeSubDomains directive or a congruent match (with or without an
+ // asserted includeSubDomains directive). [HSTS]
+ // TODO
+
+ // 10. If recursive is false, then run the remaining steps in parallel.
+ // TODO
+
+ // 11. If response is null, then set response to the result of running
+ // the steps corresponding to the first matching statement:
+ if (response === null) {
+ const currentURL = requestCurrentURL(request)
+ if (
+ // - request’s current URL’s origin is same origin with request’s origin,
+ // and request’s response tainting is "basic"
+ (sameOrigin(currentURL, request.url) && request.responseTainting === 'basic') ||
+ // request’s current URL’s scheme is "data"
+ (currentURL.protocol === 'data:') ||
+ // - request’s mode is "navigate" or "websocket"
+ (request.mode === 'navigate' || request.mode === 'websocket')
+ ) {
+ // 1. Set request’s response tainting to "basic".
+ request.responseTainting = 'basic'
+
+ // 2. Return the result of running scheme fetch given fetchParams.
+ response = await schemeFetch(fetchParams)
+
+ // request’s mode is "same-origin"
+ } else if (request.mode === 'same-origin') {
+ // 1. Return a network error.
+ response = makeNetworkError('request mode cannot be "same-origin"')
+
+ // request’s mode is "no-cors"
+ } else if (request.mode === 'no-cors') {
+ // 1. If request’s redirect mode is not "follow", then return a network
+ // error.
+ if (request.redirect !== 'follow') {
+ response = makeNetworkError(
+ 'redirect mode cannot be "follow" for "no-cors" request'
+ )
+ } else {
+ // 2. Set request’s response tainting to "opaque".
+ request.responseTainting = 'opaque'
+
+ // 3. Return the result of running scheme fetch given fetchParams.
+ response = await schemeFetch(fetchParams)
+ }
+ // request’s current URL’s scheme is not an HTTP(S) scheme
+ } else if (!urlIsHttpHttpsScheme(requestCurrentURL(request))) {
+ // Return a network error.
+ response = makeNetworkError('URL scheme must be a HTTP(S) scheme')
+
+ // - request’s use-CORS-preflight flag is set
+ // - request’s unsafe-request flag is set and either request’s method is
+ // not a CORS-safelisted method or CORS-unsafe request-header names with
+ // request’s header list is not empty
+ // 1. Set request’s response tainting to "cors".
+ // 2. Let corsWithPreflightResponse be the result of running HTTP fetch
+ // given fetchParams and true.
+ // 3. If corsWithPreflightResponse is a network error, then clear cache
+ // entries using request.
+ // 4. Return corsWithPreflightResponse.
+ // TODO
+
+ // Otherwise
+ } else {
+ // 1. Set request’s response tainting to "cors".
+ request.responseTainting = 'cors'
+
+ // 2. Return the result of running HTTP fetch given fetchParams.
+ response = await httpFetch(fetchParams)
+ }
+ }
+
+ // 12. If recursive is true, then return response.
+ if (recursive) {
+ return response
+ }
+
+ // 13. If response is not a network error and response is not a filtered
+ // response, then:
+ if (response.status !== 0 && !response.internalResponse) {
+ // If request’s response tainting is "cors", then:
+ if (request.responseTainting === 'cors') {
+ // 1. Let headerNames be the result of extracting header list values
+ // given `Access-Control-Expose-Headers` and response’s header list.
+ // TODO
+ // 2. If request’s credentials mode is not "include" and headerNames
+ // contains `*`, then set response’s CORS-exposed header-name list to
+ // all unique header names in response’s header list.
+ // TODO
+ // 3. Otherwise, if headerNames is not null or failure, then set
+ // response’s CORS-exposed header-name list to headerNames.
+ // TODO
+ }
+
+ // Set response to the following filtered response with response as its
+ // internal response, depending on request’s response tainting:
+ if (request.responseTainting === 'basic') {
+ response = filterResponse(response, 'basic')
+ } else if (request.responseTainting === 'cors') {
+ response = filterResponse(response, 'cors')
+ } else if (request.responseTainting === 'opaque') {
+ response = filterResponse(response, 'opaque')
+ } else {
+ assert(false)
+ }
+ }
+
+ // 14. Let internalResponse be response, if response is a network error,
+ // and response’s internal response otherwise.
+ let internalResponse =
+ response.status === 0 ? response : response.internalResponse
+
+ // 15. If internalResponse’s URL list is empty, then set it to a clone of
+ // request’s URL list.
+ if (internalResponse.urlList.length === 0) {
+ internalResponse.urlList.push(...request.urlList)
+ }
+
+ // 16. If request’s timing allow failed flag is unset, then set
+ // internalResponse’s timing allow passed flag.
+ if (!request.timingAllowFailed) {
+ response.timingAllowPassed = true
+ }
+
+ // 17. If response is not a network error and any of the following returns
+ // blocked
+ // - should internalResponse to request be blocked as mixed content
+ // - should internalResponse to request be blocked by Content Security Policy
+ // - should internalResponse to request be blocked due to its MIME type
+ // - should internalResponse to request be blocked due to nosniff
+ // TODO
+
+ // 18. If response’s type is "opaque", internalResponse’s status is 206,
+ // internalResponse’s range-requested flag is set, and request’s header
+ // list does not contain `Range`, then set response and internalResponse
+ // to a network error.
+ if (
+ response.type === 'opaque' &&
+ internalResponse.status === 206 &&
+ internalResponse.rangeRequested &&
+ !request.headers.contains('range', true)
+ ) {
+ response = internalResponse = makeNetworkError()
+ }
+
+ // 19. If response is not a network error and either request’s method is
+ // `HEAD` or `CONNECT`, or internalResponse’s status is a null body status,
+ // set internalResponse’s body to null and disregard any enqueuing toward
+ // it (if any).
+ if (
+ response.status !== 0 &&
+ (request.method === 'HEAD' ||
+ request.method === 'CONNECT' ||
+ nullBodyStatus.includes(internalResponse.status))
+ ) {
+ internalResponse.body = null
+ fetchParams.controller.dump = true
+ }
+
+ // 20. If request’s integrity metadata is not the empty string, then:
+ if (request.integrity) {
+ // 1. Let processBodyError be this step: run fetch finale given fetchParams
+ // and a network error.
+ const processBodyError = (reason) =>
+ fetchFinale(fetchParams, makeNetworkError(reason))
+
+ // 2. If request’s response tainting is "opaque", or response’s body is null,
+ // then run processBodyError and abort these steps.
+ if (request.responseTainting === 'opaque' || response.body == null) {
+ processBodyError(response.error)
+ return
+ }
+
+ // 3. Let processBody given bytes be these steps:
+ const processBody = (bytes) => {
+ // 1. If bytes do not match request’s integrity metadata,
+ // then run processBodyError and abort these steps. [SRI]
+ if (!bytesMatch(bytes, request.integrity)) {
+ processBodyError('integrity mismatch')
+ return
+ }
+
+ // 2. Set response’s body to bytes as a body.
+ response.body = safelyExtractBody(bytes)[0]
+
+ // 3. Run fetch finale given fetchParams and response.
+ fetchFinale(fetchParams, response)
+ }
+
+ // 4. Fully read response’s body given processBody and processBodyError.
+ fullyReadBody(response.body, processBody, processBodyError)
+ } else {
+ // 21. Otherwise, run fetch finale given fetchParams and response.
+ fetchFinale(fetchParams, response)
+ }
+ } catch (err) {
+ fetchParams.controller.terminate(err)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#concept-scheme-fetch
+// given a fetch params fetchParams
+function schemeFetch (fetchParams) {
+ // Note: since the connection is destroyed on redirect, which sets fetchParams to a
+ // cancelled state, we do not want this condition to trigger *unless* there have been
+ // no redirects. See https://github.com/nodejs/undici/issues/1776
+ // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams) && fetchParams.request.redirectCount === 0) {
+ return Promise.resolve(makeAppropriateNetworkError(fetchParams))
+ }
+
+ // 2. Let request be fetchParams’s request.
+ const { request } = fetchParams
+
+ const { protocol: scheme } = requestCurrentURL(request)
+
+ // 3. Switch on request’s current URL’s scheme and run the associated steps:
+ switch (scheme) {
+ case 'about:': {
+ // If request’s current URL’s path is the string "blank", then return a new response
+ // whose status message is `OK`, header list is « (`Content-Type`, `text/html;charset=utf-8`) »,
+ // and body is the empty byte sequence as a body.
+
+ // Otherwise, return a network error.
+ return Promise.resolve(makeNetworkError('about scheme is not supported'))
+ }
+ case 'blob:': {
+ if (!resolveObjectURL) {
+ resolveObjectURL = require('node:buffer').resolveObjectURL
+ }
+
+ // 1. Let blobURLEntry be request’s current URL’s blob URL entry.
+ const blobURLEntry = requestCurrentURL(request)
+
+ // https://github.com/web-platform-tests/wpt/blob/7b0ebaccc62b566a1965396e5be7bb2bc06f841f/FileAPI/url/resources/fetch-tests.js#L52-L56
+ // Buffer.resolveObjectURL does not ignore URL queries.
+ if (blobURLEntry.search.length !== 0) {
+ return Promise.resolve(makeNetworkError('NetworkError when attempting to fetch resource.'))
+ }
+
+ const blob = resolveObjectURL(blobURLEntry.toString())
+
+ // 2. If request’s method is not `GET`, blobURLEntry is null, or blobURLEntry’s
+ // object is not a Blob object, then return a network error.
+ if (request.method !== 'GET' || !webidl.is.Blob(blob)) {
+ return Promise.resolve(makeNetworkError('invalid method'))
+ }
+
+ // 3. Let blob be blobURLEntry’s object.
+ // Note: done above
+
+ // 4. Let response be a new response.
+ const response = makeResponse()
+
+ // 5. Let fullLength be blob’s size.
+ const fullLength = blob.size
+
+ // 6. Let serializedFullLength be fullLength, serialized and isomorphic encoded.
+ const serializedFullLength = isomorphicEncode(`${fullLength}`)
+
+ // 7. Let type be blob’s type.
+ const type = blob.type
+
+ // 8. If request’s header list does not contain `Range`:
+ // 9. Otherwise:
+ if (!request.headersList.contains('range', true)) {
+ // 1. Let bodyWithType be the result of safely extracting blob.
+ // Note: in the FileAPI a blob "object" is a Blob *or* a MediaSource.
+ // In node, this can only ever be a Blob. Therefore we can safely
+ // use extractBody directly.
+ const bodyWithType = extractBody(blob)
+
+ // 2. Set response’s status message to `OK`.
+ response.statusText = 'OK'
+
+ // 3. Set response’s body to bodyWithType’s body.
+ response.body = bodyWithType[0]
+
+ // 4. Set response’s header list to « (`Content-Length`, serializedFullLength), (`Content-Type`, type) ».
+ response.headersList.set('content-length', serializedFullLength, true)
+ response.headersList.set('content-type', type, true)
+ } else {
+ // 1. Set response’s range-requested flag.
+ response.rangeRequested = true
+
+ // 2. Let rangeHeader be the result of getting `Range` from request’s header list.
+ const rangeHeader = request.headersList.get('range', true)
+
+ // 3. Let rangeValue be the result of parsing a single range header value given rangeHeader and true.
+ const rangeValue = simpleRangeHeaderValue(rangeHeader, true)
+
+ // 4. If rangeValue is failure, then return a network error.
+ if (rangeValue === 'failure') {
+ return Promise.resolve(makeNetworkError('failed to fetch the data URL'))
+ }
+
+ // 5. Let (rangeStart, rangeEnd) be rangeValue.
+ let { rangeStartValue: rangeStart, rangeEndValue: rangeEnd } = rangeValue
+
+ // 6. If rangeStart is null:
+ // 7. Otherwise:
+ if (rangeStart === null) {
+ // 1. Set rangeStart to fullLength − rangeEnd.
+ rangeStart = fullLength - rangeEnd
+
+ // 2. Set rangeEnd to rangeStart + rangeEnd − 1.
+ rangeEnd = rangeStart + rangeEnd - 1
+ } else {
+ // 1. If rangeStart is greater than or equal to fullLength, then return a network error.
+ if (rangeStart >= fullLength) {
+ return Promise.resolve(makeNetworkError('Range start is greater than the blob\'s size.'))
+ }
+
+ // 2. If rangeEnd is null or rangeEnd is greater than or equal to fullLength, then set
+ // rangeEnd to fullLength − 1.
+ if (rangeEnd === null || rangeEnd >= fullLength) {
+ rangeEnd = fullLength - 1
+ }
+ }
+
+ // 8. Let slicedBlob be the result of invoking slice blob given blob, rangeStart,
+ // rangeEnd + 1, and type.
+ const slicedBlob = blob.slice(rangeStart, rangeEnd + 1, type)
+
+ // 9. Let slicedBodyWithType be the result of safely extracting slicedBlob.
+ // Note: same reason as mentioned above as to why we use extractBody
+ const slicedBodyWithType = extractBody(slicedBlob)
+
+ // 10. Set response’s body to slicedBodyWithType’s body.
+ response.body = slicedBodyWithType[0]
+
+ // 11. Let serializedSlicedLength be slicedBlob’s size, serialized and isomorphic encoded.
+ const serializedSlicedLength = isomorphicEncode(`${slicedBlob.size}`)
+
+ // 12. Let contentRange be the result of invoking build a content range given rangeStart,
+ // rangeEnd, and fullLength.
+ const contentRange = buildContentRange(rangeStart, rangeEnd, fullLength)
+
+ // 13. Set response’s status to 206.
+ response.status = 206
+
+ // 14. Set response’s status message to `Partial Content`.
+ response.statusText = 'Partial Content'
+
+ // 15. Set response’s header list to « (`Content-Length`, serializedSlicedLength),
+ // (`Content-Type`, type), (`Content-Range`, contentRange) ».
+ response.headersList.set('content-length', serializedSlicedLength, true)
+ response.headersList.set('content-type', type, true)
+ response.headersList.set('content-range', contentRange, true)
+ }
+
+ // 10. Return response.
+ return Promise.resolve(response)
+ }
+ case 'data:': {
+ // 1. Let dataURLStruct be the result of running the
+ // data: URL processor on request’s current URL.
+ const currentURL = requestCurrentURL(request)
+ const dataURLStruct = dataURLProcessor(currentURL)
+
+ // 2. If dataURLStruct is failure, then return a
+ // network error.
+ if (dataURLStruct === 'failure') {
+ return Promise.resolve(makeNetworkError('failed to fetch the data URL'))
+ }
+
+ // 3. Let mimeType be dataURLStruct’s MIME type, serialized.
+ const mimeType = serializeAMimeType(dataURLStruct.mimeType)
+
+ // 4. Return a response whose status message is `OK`,
+ // header list is « (`Content-Type`, mimeType) »,
+ // and body is dataURLStruct’s body as a body.
+ return Promise.resolve(makeResponse({
+ statusText: 'OK',
+ headersList: [
+ ['content-type', { name: 'Content-Type', value: mimeType }]
+ ],
+ body: safelyExtractBody(dataURLStruct.body)[0]
+ }))
+ }
+ case 'file:': {
+ // For now, unfortunate as it is, file URLs are left as an exercise for the reader.
+ // When in doubt, return a network error.
+ return Promise.resolve(makeNetworkError('not implemented... yet...'))
+ }
+ case 'http:':
+ case 'https:': {
+ // Return the result of running HTTP fetch given fetchParams.
+
+ return httpFetch(fetchParams)
+ .catch((err) => makeNetworkError(err))
+ }
+ default: {
+ return Promise.resolve(makeNetworkError('unknown scheme'))
+ }
+ }
+}
+
+// https://fetch.spec.whatwg.org/#finalize-response
+function finalizeResponse (fetchParams, response) {
+ // 1. Set fetchParams’s request’s done flag.
+ fetchParams.request.done = true
+
+ // 2, If fetchParams’s process response done is not null, then queue a fetch
+ // task to run fetchParams’s process response done given response, with
+ // fetchParams’s task destination.
+ if (fetchParams.processResponseDone != null) {
+ queueMicrotask(() => fetchParams.processResponseDone(response))
+ }
+}
+
+// https://fetch.spec.whatwg.org/#fetch-finale
+function fetchFinale (fetchParams, response) {
+ // 1. Let timingInfo be fetchParams’s timing info.
+ let timingInfo = fetchParams.timingInfo
+
+ // 2. If response is not a network error and fetchParams’s request’s client is a secure context,
+ // then set timingInfo’s server-timing headers to the result of getting, decoding, and splitting
+ // `Server-Timing` from response’s internal response’s header list.
+ // TODO
+
+ // 3. Let processResponseEndOfBody be the following steps:
+ const processResponseEndOfBody = () => {
+ // 1. Let unsafeEndTime be the unsafe shared current time.
+ const unsafeEndTime = Date.now() // ?
+
+ // 2. If fetchParams’s request’s destination is "document", then set fetchParams’s controller’s
+ // full timing info to fetchParams’s timing info.
+ if (fetchParams.request.destination === 'document') {
+ fetchParams.controller.fullTimingInfo = timingInfo
+ }
+
+ // 3. Set fetchParams’s controller’s report timing steps to the following steps given a global object global:
+ fetchParams.controller.reportTimingSteps = () => {
+ // 1. If fetchParams’s request’s URL’s scheme is not an HTTP(S) scheme, then return.
+ if (!urlIsHttpHttpsScheme(fetchParams.request.url)) {
+ return
+ }
+
+ // 2. Set timingInfo’s end time to the relative high resolution time given unsafeEndTime and global.
+ timingInfo.endTime = unsafeEndTime
+
+ // 3. Let cacheState be response’s cache state.
+ let cacheState = response.cacheState
+
+ // 4. Let bodyInfo be response’s body info.
+ const bodyInfo = response.bodyInfo
+
+ // 5. If response’s timing allow passed flag is not set, then set timingInfo to the result of creating an
+ // opaque timing info for timingInfo and set cacheState to the empty string.
+ if (!response.timingAllowPassed) {
+ timingInfo = createOpaqueTimingInfo(timingInfo)
+
+ cacheState = ''
+ }
+
+ // 6. Let responseStatus be 0.
+ let responseStatus = 0
+
+ // 7. If fetchParams’s request’s mode is not "navigate" or response’s has-cross-origin-redirects is false:
+ if (fetchParams.request.mode !== 'navigator' || !response.hasCrossOriginRedirects) {
+ // 1. Set responseStatus to response’s status.
+ responseStatus = response.status
+
+ // 2. Let mimeType be the result of extracting a MIME type from response’s header list.
+ const mimeType = extractMimeType(response.headersList)
+
+ // 3. If mimeType is not failure, then set bodyInfo’s content type to the result of minimizing a supported MIME type given mimeType.
+ if (mimeType !== 'failure') {
+ bodyInfo.contentType = minimizeSupportedMimeType(mimeType)
+ }
+ }
+
+ // 8. If fetchParams’s request’s initiator type is non-null, then mark resource timing given timingInfo,
+ // fetchParams’s request’s URL, fetchParams’s request’s initiator type, global, cacheState, bodyInfo,
+ // and responseStatus.
+ if (fetchParams.request.initiatorType != null) {
+ markResourceTiming(timingInfo, fetchParams.request.url.href, fetchParams.request.initiatorType, globalThis, cacheState, bodyInfo, responseStatus)
+ }
+ }
+
+ // 4. Let processResponseEndOfBodyTask be the following steps:
+ const processResponseEndOfBodyTask = () => {
+ // 1. Set fetchParams’s request’s done flag.
+ fetchParams.request.done = true
+
+ // 2. If fetchParams’s process response end-of-body is non-null, then run fetchParams’s process
+ // response end-of-body given response.
+ if (fetchParams.processResponseEndOfBody != null) {
+ queueMicrotask(() => fetchParams.processResponseEndOfBody(response))
+ }
+
+ // 3. If fetchParams’s request’s initiator type is non-null and fetchParams’s request’s client’s
+ // global object is fetchParams’s task destination, then run fetchParams’s controller’s report
+ // timing steps given fetchParams’s request’s client’s global object.
+ if (fetchParams.request.initiatorType != null) {
+ fetchParams.controller.reportTimingSteps()
+ }
+ }
+
+ // 5. Queue a fetch task to run processResponseEndOfBodyTask with fetchParams’s task destination
+ queueMicrotask(() => processResponseEndOfBodyTask())
+ }
+
+ // 4. If fetchParams’s process response is non-null, then queue a fetch task to run fetchParams’s
+ // process response given response, with fetchParams’s task destination.
+ if (fetchParams.processResponse != null) {
+ queueMicrotask(() => {
+ fetchParams.processResponse(response)
+ fetchParams.processResponse = null
+ })
+ }
+
+ // 5. Let internalResponse be response, if response is a network error; otherwise response’s internal response.
+ const internalResponse = response.type === 'error' ? response : (response.internalResponse ?? response)
+
+ // 6. If internalResponse’s body is null, then run processResponseEndOfBody.
+ // 7. Otherwise:
+ if (internalResponse.body == null) {
+ processResponseEndOfBody()
+ } else {
+ // mcollina: all the following steps of the specs are skipped.
+ // The internal transform stream is not needed.
+ // See https://github.com/nodejs/undici/pull/3093#issuecomment-2050198541
+
+ // 1. Let transformStream be a new TransformStream.
+ // 2. Let identityTransformAlgorithm be an algorithm which, given chunk, enqueues chunk in transformStream.
+ // 3. Set up transformStream with transformAlgorithm set to identityTransformAlgorithm and flushAlgorithm
+ // set to processResponseEndOfBody.
+ // 4. Set internalResponse’s body’s stream to the result of internalResponse’s body’s stream piped through transformStream.
+
+ finished(internalResponse.body.stream, () => {
+ processResponseEndOfBody()
+ })
+ }
+}
+
+// https://fetch.spec.whatwg.org/#http-fetch
+async function httpFetch (fetchParams) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. Let actualResponse be null.
+ let actualResponse = null
+
+ // 4. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 5. If request’s service-workers mode is "all", then:
+ if (request.serviceWorkers === 'all') {
+ // TODO
+ }
+
+ // 6. If response is null, then:
+ if (response === null) {
+ // 1. If makeCORSPreflight is true and one of these conditions is true:
+ // TODO
+
+ // 2. If request’s redirect mode is "follow", then set request’s
+ // service-workers mode to "none".
+ if (request.redirect === 'follow') {
+ request.serviceWorkers = 'none'
+ }
+
+ // 3. Set response and actualResponse to the result of running
+ // HTTP-network-or-cache fetch given fetchParams.
+ actualResponse = response = await httpNetworkOrCacheFetch(fetchParams)
+
+ // 4. If request’s response tainting is "cors" and a CORS check
+ // for request and response returns failure, then return a network error.
+ if (
+ request.responseTainting === 'cors' &&
+ corsCheck(request, response) === 'failure'
+ ) {
+ return makeNetworkError('cors failure')
+ }
+
+ // 5. If the TAO check for request and response returns failure, then set
+ // request’s timing allow failed flag.
+ if (TAOCheck(request, response) === 'failure') {
+ request.timingAllowFailed = true
+ }
+ }
+
+ // 7. If either request’s response tainting or response’s type
+ // is "opaque", and the cross-origin resource policy check with
+ // request’s origin, request’s client, request’s destination,
+ // and actualResponse returns blocked, then return a network error.
+ if (
+ (request.responseTainting === 'opaque' || response.type === 'opaque') &&
+ crossOriginResourcePolicyCheck(
+ request.origin,
+ request.client,
+ request.destination,
+ actualResponse
+ ) === 'blocked'
+ ) {
+ return makeNetworkError('blocked')
+ }
+
+ // 8. If actualResponse’s status is a redirect status, then:
+ if (redirectStatusSet.has(actualResponse.status)) {
+ // 1. If actualResponse’s status is not 303, request’s body is not null,
+ // and the connection uses HTTP/2, then user agents may, and are even
+ // encouraged to, transmit an RST_STREAM frame.
+ // See, https://github.com/whatwg/fetch/issues/1288
+ if (request.redirect !== 'manual') {
+ fetchParams.controller.connection.destroy(undefined, false)
+ }
+
+ // 2. Switch on request’s redirect mode:
+ if (request.redirect === 'error') {
+ // Set response to a network error.
+ response = makeNetworkError('unexpected redirect')
+ } else if (request.redirect === 'manual') {
+ // Set response to an opaque-redirect filtered response whose internal
+ // response is actualResponse.
+ // NOTE(spec): On the web this would return an `opaqueredirect` response,
+ // but that doesn't make sense server side.
+ // See https://github.com/nodejs/undici/issues/1193.
+ response = actualResponse
+ } else if (request.redirect === 'follow') {
+ // Set response to the result of running HTTP-redirect fetch given
+ // fetchParams and response.
+ response = await httpRedirectFetch(fetchParams, response)
+ } else {
+ assert(false)
+ }
+ }
+
+ // 9. Set response’s timing info to timingInfo.
+ response.timingInfo = timingInfo
+
+ // 10. Return response.
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#http-redirect-fetch
+function httpRedirectFetch (fetchParams, response) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let actualResponse be response, if response is not a filtered response,
+ // and response’s internal response otherwise.
+ const actualResponse = response.internalResponse
+ ? response.internalResponse
+ : response
+
+ // 3. Let locationURL be actualResponse’s location URL given request’s current
+ // URL’s fragment.
+ let locationURL
+
+ try {
+ locationURL = responseLocationURL(
+ actualResponse,
+ requestCurrentURL(request).hash
+ )
+
+ // 4. If locationURL is null, then return response.
+ if (locationURL == null) {
+ return response
+ }
+ } catch (err) {
+ // 5. If locationURL is failure, then return a network error.
+ return Promise.resolve(makeNetworkError(err))
+ }
+
+ // 6. If locationURL’s scheme is not an HTTP(S) scheme, then return a network
+ // error.
+ if (!urlIsHttpHttpsScheme(locationURL)) {
+ return Promise.resolve(makeNetworkError('URL scheme must be a HTTP(S) scheme'))
+ }
+
+ // 7. If request’s redirect count is 20, then return a network error.
+ if (request.redirectCount === 20) {
+ return Promise.resolve(makeNetworkError('redirect count exceeded'))
+ }
+
+ // 8. Increase request’s redirect count by 1.
+ request.redirectCount += 1
+
+ // 9. If request’s mode is "cors", locationURL includes credentials, and
+ // request’s origin is not same origin with locationURL’s origin, then return
+ // a network error.
+ if (
+ request.mode === 'cors' &&
+ (locationURL.username || locationURL.password) &&
+ !sameOrigin(request, locationURL)
+ ) {
+ return Promise.resolve(makeNetworkError('cross origin not allowed for request mode "cors"'))
+ }
+
+ // 10. If request’s response tainting is "cors" and locationURL includes
+ // credentials, then return a network error.
+ if (
+ request.responseTainting === 'cors' &&
+ (locationURL.username || locationURL.password)
+ ) {
+ return Promise.resolve(makeNetworkError(
+ 'URL cannot contain credentials for request mode "cors"'
+ ))
+ }
+
+ // 11. If actualResponse’s status is not 303, request’s body is non-null,
+ // and request’s body’s source is null, then return a network error.
+ if (
+ actualResponse.status !== 303 &&
+ request.body != null &&
+ request.body.source == null
+ ) {
+ return Promise.resolve(makeNetworkError())
+ }
+
+ // 12. If one of the following is true
+ // - actualResponse’s status is 301 or 302 and request’s method is `POST`
+ // - actualResponse’s status is 303 and request’s method is not `GET` or `HEAD`
+ if (
+ ([301, 302].includes(actualResponse.status) && request.method === 'POST') ||
+ (actualResponse.status === 303 &&
+ !GET_OR_HEAD.includes(request.method))
+ ) {
+ // then:
+ // 1. Set request’s method to `GET` and request’s body to null.
+ request.method = 'GET'
+ request.body = null
+
+ // 2. For each headerName of request-body-header name, delete headerName from
+ // request’s header list.
+ for (const headerName of requestBodyHeader) {
+ request.headersList.delete(headerName)
+ }
+ }
+
+ // 13. If request’s current URL’s origin is not same origin with locationURL’s
+ // origin, then for each headerName of CORS non-wildcard request-header name,
+ // delete headerName from request’s header list.
+ if (!sameOrigin(requestCurrentURL(request), locationURL)) {
+ // https://fetch.spec.whatwg.org/#cors-non-wildcard-request-header-name
+ request.headersList.delete('authorization', true)
+
+ // https://fetch.spec.whatwg.org/#authentication-entries
+ request.headersList.delete('proxy-authorization', true)
+
+ // "Cookie" and "Host" are forbidden request-headers, which undici doesn't implement.
+ request.headersList.delete('cookie', true)
+ request.headersList.delete('host', true)
+ }
+
+ // 14. If request's body is non-null, then set request's body to the first return
+ // value of safely extracting request's body's source.
+ if (request.body != null) {
+ assert(request.body.source != null)
+ request.body = safelyExtractBody(request.body.source)[0]
+ }
+
+ // 15. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 16. Set timingInfo’s redirect end time and post-redirect start time to the
+ // coarsened shared current time given fetchParams’s cross-origin isolated
+ // capability.
+ timingInfo.redirectEndTime = timingInfo.postRedirectStartTime =
+ coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
+
+ // 17. If timingInfo’s redirect start time is 0, then set timingInfo’s
+ // redirect start time to timingInfo’s start time.
+ if (timingInfo.redirectStartTime === 0) {
+ timingInfo.redirectStartTime = timingInfo.startTime
+ }
+
+ // 18. Append locationURL to request’s URL list.
+ request.urlList.push(locationURL)
+
+ // 19. Invoke set request’s referrer policy on redirect on request and
+ // actualResponse.
+ setRequestReferrerPolicyOnRedirect(request, actualResponse)
+
+ // 20. Return the result of running main fetch given fetchParams and true.
+ return mainFetch(fetchParams, true)
+}
+
+// https://fetch.spec.whatwg.org/#http-network-or-cache-fetch
+async function httpNetworkOrCacheFetch (
+ fetchParams,
+ isAuthenticationFetch = false,
+ isNewConnectionFetch = false
+) {
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let httpFetchParams be null.
+ let httpFetchParams = null
+
+ // 3. Let httpRequest be null.
+ let httpRequest = null
+
+ // 4. Let response be null.
+ let response = null
+
+ // 5. Let storedResponse be null.
+ // TODO: cache
+
+ // 6. Let httpCache be null.
+ const httpCache = null
+
+ // 7. Let the revalidatingFlag be unset.
+ const revalidatingFlag = false
+
+ // 8. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. If request’s window is "no-window" and request’s redirect mode is
+ // "error", then set httpFetchParams to fetchParams and httpRequest to
+ // request.
+ if (request.window === 'no-window' && request.redirect === 'error') {
+ httpFetchParams = fetchParams
+ httpRequest = request
+ } else {
+ // Otherwise:
+
+ // 1. Set httpRequest to a clone of request.
+ httpRequest = cloneRequest(request)
+
+ // 2. Set httpFetchParams to a copy of fetchParams.
+ httpFetchParams = { ...fetchParams }
+
+ // 3. Set httpFetchParams’s request to httpRequest.
+ httpFetchParams.request = httpRequest
+ }
+
+ // 3. Let includeCredentials be true if one of
+ const includeCredentials =
+ request.credentials === 'include' ||
+ (request.credentials === 'same-origin' &&
+ request.responseTainting === 'basic')
+
+ // 4. Let contentLength be httpRequest’s body’s length, if httpRequest’s
+ // body is non-null; otherwise null.
+ const contentLength = httpRequest.body ? httpRequest.body.length : null
+
+ // 5. Let contentLengthHeaderValue be null.
+ let contentLengthHeaderValue = null
+
+ // 6. If httpRequest’s body is null and httpRequest’s method is `POST` or
+ // `PUT`, then set contentLengthHeaderValue to `0`.
+ if (
+ httpRequest.body == null &&
+ ['POST', 'PUT'].includes(httpRequest.method)
+ ) {
+ contentLengthHeaderValue = '0'
+ }
+
+ // 7. If contentLength is non-null, then set contentLengthHeaderValue to
+ // contentLength, serialized and isomorphic encoded.
+ if (contentLength != null) {
+ contentLengthHeaderValue = isomorphicEncode(`${contentLength}`)
+ }
+
+ // 8. If contentLengthHeaderValue is non-null, then append
+ // `Content-Length`/contentLengthHeaderValue to httpRequest’s header
+ // list.
+ if (contentLengthHeaderValue != null) {
+ httpRequest.headersList.append('content-length', contentLengthHeaderValue, true)
+ }
+
+ // 9. If contentLengthHeaderValue is non-null, then append (`Content-Length`,
+ // contentLengthHeaderValue) to httpRequest’s header list.
+
+ // 10. If contentLength is non-null and httpRequest’s keepalive is true,
+ // then:
+ if (contentLength != null && httpRequest.keepalive) {
+ // NOTE: keepalive is a noop outside of browser context.
+ }
+
+ // 11. If httpRequest’s referrer is a URL, then append
+ // `Referer`/httpRequest’s referrer, serialized and isomorphic encoded,
+ // to httpRequest’s header list.
+ if (webidl.is.URL(httpRequest.referrer)) {
+ httpRequest.headersList.append('referer', isomorphicEncode(httpRequest.referrer.href), true)
+ }
+
+ // 12. Append a request `Origin` header for httpRequest.
+ appendRequestOriginHeader(httpRequest)
+
+ // 13. Append the Fetch metadata headers for httpRequest. [FETCH-METADATA]
+ appendFetchMetadata(httpRequest)
+
+ // 14. If httpRequest’s header list does not contain `User-Agent`, then
+ // user agents should append `User-Agent`/default `User-Agent` value to
+ // httpRequest’s header list.
+ if (!httpRequest.headersList.contains('user-agent', true)) {
+ httpRequest.headersList.append('user-agent', defaultUserAgent, true)
+ }
+
+ // 15. If httpRequest’s cache mode is "default" and httpRequest’s header
+ // list contains `If-Modified-Since`, `If-None-Match`,
+ // `If-Unmodified-Since`, `If-Match`, or `If-Range`, then set
+ // httpRequest’s cache mode to "no-store".
+ if (
+ httpRequest.cache === 'default' &&
+ (httpRequest.headersList.contains('if-modified-since', true) ||
+ httpRequest.headersList.contains('if-none-match', true) ||
+ httpRequest.headersList.contains('if-unmodified-since', true) ||
+ httpRequest.headersList.contains('if-match', true) ||
+ httpRequest.headersList.contains('if-range', true))
+ ) {
+ httpRequest.cache = 'no-store'
+ }
+
+ // 16. If httpRequest’s cache mode is "no-cache", httpRequest’s prevent
+ // no-cache cache-control header modification flag is unset, and
+ // httpRequest’s header list does not contain `Cache-Control`, then append
+ // `Cache-Control`/`max-age=0` to httpRequest’s header list.
+ if (
+ httpRequest.cache === 'no-cache' &&
+ !httpRequest.preventNoCacheCacheControlHeaderModification &&
+ !httpRequest.headersList.contains('cache-control', true)
+ ) {
+ httpRequest.headersList.append('cache-control', 'max-age=0', true)
+ }
+
+ // 17. If httpRequest’s cache mode is "no-store" or "reload", then:
+ if (httpRequest.cache === 'no-store' || httpRequest.cache === 'reload') {
+ // 1. If httpRequest’s header list does not contain `Pragma`, then append
+ // `Pragma`/`no-cache` to httpRequest’s header list.
+ if (!httpRequest.headersList.contains('pragma', true)) {
+ httpRequest.headersList.append('pragma', 'no-cache', true)
+ }
+
+ // 2. If httpRequest’s header list does not contain `Cache-Control`,
+ // then append `Cache-Control`/`no-cache` to httpRequest’s header list.
+ if (!httpRequest.headersList.contains('cache-control', true)) {
+ httpRequest.headersList.append('cache-control', 'no-cache', true)
+ }
+ }
+
+ // 18. If httpRequest’s header list contains `Range`, then append
+ // `Accept-Encoding`/`identity` to httpRequest’s header list.
+ if (httpRequest.headersList.contains('range', true)) {
+ httpRequest.headersList.append('accept-encoding', 'identity', true)
+ }
+
+ // 19. Modify httpRequest’s header list per HTTP. Do not append a given
+ // header if httpRequest’s header list contains that header’s name.
+ // TODO: https://github.com/whatwg/fetch/issues/1285#issuecomment-896560129
+ if (!httpRequest.headersList.contains('accept-encoding', true)) {
+ if (urlHasHttpsScheme(requestCurrentURL(httpRequest))) {
+ httpRequest.headersList.append('accept-encoding', 'br, gzip, deflate', true)
+ } else {
+ httpRequest.headersList.append('accept-encoding', 'gzip, deflate', true)
+ }
+ }
+
+ httpRequest.headersList.delete('host', true)
+
+ // 21. If includeCredentials is true, then:
+ if (includeCredentials) {
+ // 1. If the user agent is not configured to block cookies for httpRequest
+ // (see section 7 of [COOKIES]), then:
+ // TODO: credentials
+
+ // 2. If httpRequest’s header list does not contain `Authorization`, then:
+ if (!httpRequest.headersList.contains('authorization', true)) {
+ // 1. Let authorizationValue be null.
+ let authorizationValue = null
+
+ // 2. If there’s an authentication entry for httpRequest and either
+ // httpRequest’s use-URL-credentials flag is unset or httpRequest’s
+ // current URL does not include credentials, then set
+ // authorizationValue to authentication entry.
+ if (hasAuthenticationEntry(httpRequest) && (
+ httpRequest.useURLCredentials === undefined || !includesCredentials(requestCurrentURL(httpRequest))
+ )) {
+ // TODO
+ } else if (includesCredentials(requestCurrentURL(httpRequest)) && isAuthenticationFetch) {
+ // 3. Otherwise, if httpRequest’s current URL does include credentials
+ // and isAuthenticationFetch is true, set authorizationValue to
+ // httpRequest’s current URL, converted to an `Authorization` value
+ const { username, password } = requestCurrentURL(httpRequest)
+ authorizationValue = `Basic ${Buffer.from(`${username}:${password}`).toString('base64')}`
+ }
+
+ // 4. If authorizationValue is non-null, then append (`Authorization`,
+ // authorizationValue) to httpRequest’s header list.
+ if (authorizationValue !== null) {
+ httpRequest.headersList.append('Authorization', authorizationValue, false)
+ }
+ }
+ }
+
+ // 21. If there’s a proxy-authentication entry, use it as appropriate.
+ // TODO: proxy-authentication
+
+ // 22. Set httpCache to the result of determining the HTTP cache
+ // partition, given httpRequest.
+ // TODO: cache
+
+ // 23. If httpCache is null, then set httpRequest’s cache mode to
+ // "no-store".
+ if (httpCache == null) {
+ httpRequest.cache = 'no-store'
+ }
+
+ // 24. If httpRequest’s cache mode is neither "no-store" nor "reload",
+ // then:
+ if (httpRequest.cache !== 'no-store' && httpRequest.cache !== 'reload') {
+ // TODO: cache
+ }
+
+ // 9. If aborted, then return the appropriate network error for fetchParams.
+ // TODO
+
+ // 10. If response is null, then:
+ if (response == null) {
+ // 1. If httpRequest’s cache mode is "only-if-cached", then return a
+ // network error.
+ if (httpRequest.cache === 'only-if-cached') {
+ return makeNetworkError('only if cached')
+ }
+
+ // 2. Let forwardResponse be the result of running HTTP-network fetch
+ // given httpFetchParams, includeCredentials, and isNewConnectionFetch.
+ const forwardResponse = await httpNetworkFetch(
+ httpFetchParams,
+ includeCredentials,
+ isNewConnectionFetch
+ )
+
+ // 3. If httpRequest’s method is unsafe and forwardResponse’s status is
+ // in the range 200 to 399, inclusive, invalidate appropriate stored
+ // responses in httpCache, as per the "Invalidation" chapter of HTTP
+ // Caching, and set storedResponse to null. [HTTP-CACHING]
+ if (
+ !safeMethodsSet.has(httpRequest.method) &&
+ forwardResponse.status >= 200 &&
+ forwardResponse.status <= 399
+ ) {
+ // TODO: cache
+ }
+
+ // 4. If the revalidatingFlag is set and forwardResponse’s status is 304,
+ // then:
+ if (revalidatingFlag && forwardResponse.status === 304) {
+ // TODO: cache
+ }
+
+ // 5. If response is null, then:
+ if (response == null) {
+ // 1. Set response to forwardResponse.
+ response = forwardResponse
+
+ // 2. Store httpRequest and forwardResponse in httpCache, as per the
+ // "Storing Responses in Caches" chapter of HTTP Caching. [HTTP-CACHING]
+ // TODO: cache
+ }
+ }
+
+ // 11. Set response’s URL list to a clone of httpRequest’s URL list.
+ response.urlList = [...httpRequest.urlList]
+
+ // 12. If httpRequest’s header list contains `Range`, then set response’s
+ // range-requested flag.
+ if (httpRequest.headersList.contains('range', true)) {
+ response.rangeRequested = true
+ }
+
+ // 13. Set response’s request-includes-credentials to includeCredentials.
+ response.requestIncludesCredentials = includeCredentials
+
+ // 14. If response’s status is 401, httpRequest’s response tainting is not "cors",
+ // includeCredentials is true, and request’s traversable for user prompts is
+ // a traversable navigable:
+ if (response.status === 401 && httpRequest.responseTainting !== 'cors' && includeCredentials && isTraversableNavigable(request.traversableForUserPrompts)) {
+ // 2. If request’s body is non-null, then:
+ if (request.body != null) {
+ // 1. If request’s body’s source is null, then return a network error.
+ if (request.body.source == null) {
+ return makeNetworkError('expected non-null body source')
+ }
+
+ // 2. Set request’s body to the body of the result of safely extracting
+ // request’s body’s source.
+ request.body = safelyExtractBody(request.body.source)[0]
+ }
+
+ // 3. If request’s use-URL-credentials flag is unset or isAuthenticationFetch is
+ // true, then:
+ if (request.useURLCredentials === undefined || isAuthenticationFetch) {
+ // 1. If fetchParams is canceled, then return the appropriate network error
+ // for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 2. Let username and password be the result of prompting the end user for a
+ // username and password, respectively, in request’s traversable for user prompts.
+ // TODO
+
+ // 3. Set the username given request’s current URL and username.
+ // requestCurrentURL(request).username = TODO
+
+ // 4. Set the password given request’s current URL and password.
+ // requestCurrentURL(request).password = TODO
+
+ // In browsers, the user will be prompted to enter a username/password before the request
+ // is re-sent. To prevent an infinite 401 loop, return the response for now.
+ // https://github.com/nodejs/undici/pull/4756
+ return response
+ }
+
+ // 4. Set response to the result of running HTTP-network-or-cache fetch given
+ // fetchParams and true.
+ fetchParams.controller.connection.destroy()
+
+ response = await httpNetworkOrCacheFetch(fetchParams, true)
+ }
+
+ // 15. If response’s status is 407, then:
+ if (response.status === 407) {
+ // 1. If request’s window is "no-window", then return a network error.
+ if (request.window === 'no-window') {
+ return makeNetworkError()
+ }
+
+ // 2. ???
+
+ // 3. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 4. Prompt the end user as appropriate in request’s window and store
+ // the result as a proxy-authentication entry. [HTTP-AUTH]
+ // TODO: Invoke some kind of callback?
+
+ // 5. Set response to the result of running HTTP-network-or-cache fetch given
+ // fetchParams.
+ // TODO
+ return makeNetworkError('proxy authentication required')
+ }
+
+ // 16. If all of the following are true
+ if (
+ // response’s status is 421
+ response.status === 421 &&
+ // isNewConnectionFetch is false
+ !isNewConnectionFetch &&
+ // request’s body is null, or request’s body is non-null and request’s body’s source is non-null
+ (request.body == null || request.body.source != null)
+ ) {
+ // then:
+
+ // 1. If fetchParams is canceled, then return the appropriate network error for fetchParams.
+ if (isCancelled(fetchParams)) {
+ return makeAppropriateNetworkError(fetchParams)
+ }
+
+ // 2. Set response to the result of running HTTP-network-or-cache
+ // fetch given fetchParams, isAuthenticationFetch, and true.
+
+ // TODO (spec): The spec doesn't specify this but we need to cancel
+ // the active response before we can start a new one.
+ // https://github.com/whatwg/fetch/issues/1293
+ fetchParams.controller.connection.destroy()
+
+ response = await httpNetworkOrCacheFetch(
+ fetchParams,
+ isAuthenticationFetch,
+ true
+ )
+ }
+
+ // 17. If isAuthenticationFetch is true, then create an authentication entry
+ if (isAuthenticationFetch) {
+ // TODO
+ }
+
+ // 18. Return response.
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#http-network-fetch
+async function httpNetworkFetch (
+ fetchParams,
+ includeCredentials = false,
+ forceNewConnection = false
+) {
+ assert(!fetchParams.controller.connection || fetchParams.controller.connection.destroyed)
+
+ fetchParams.controller.connection = {
+ abort: null,
+ destroyed: false,
+ destroy (err, abort = true) {
+ if (!this.destroyed) {
+ this.destroyed = true
+ if (abort) {
+ this.abort?.(err ?? new DOMException('The operation was aborted.', 'AbortError'))
+ }
+ }
+ }
+ }
+
+ // 1. Let request be fetchParams’s request.
+ const request = fetchParams.request
+
+ // 2. Let response be null.
+ let response = null
+
+ // 3. Let timingInfo be fetchParams’s timing info.
+ const timingInfo = fetchParams.timingInfo
+
+ // 4. Let httpCache be the result of determining the HTTP cache partition,
+ // given request.
+ // TODO: cache
+ const httpCache = null
+
+ // 5. If httpCache is null, then set request’s cache mode to "no-store".
+ if (httpCache == null) {
+ request.cache = 'no-store'
+ }
+
+ // 6. Let networkPartitionKey be the result of determining the network
+ // partition key given request.
+ // TODO
+
+ // 7. Let newConnection be "yes" if forceNewConnection is true; otherwise
+ // "no".
+ const newConnection = forceNewConnection ? 'yes' : 'no' // eslint-disable-line no-unused-vars
+
+ // 8. Switch on request’s mode:
+ if (request.mode === 'websocket') {
+ // Let connection be the result of obtaining a WebSocket connection,
+ // given request’s current URL.
+ // TODO
+ } else {
+ // Let connection be the result of obtaining a connection, given
+ // networkPartitionKey, request’s current URL’s origin,
+ // includeCredentials, and forceNewConnection.
+ // TODO
+ }
+
+ // 9. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. If connection is failure, then return a network error.
+
+ // 2. Set timingInfo’s final connection timing info to the result of
+ // calling clamp and coarsen connection timing info with connection’s
+ // timing info, timingInfo’s post-redirect start time, and fetchParams’s
+ // cross-origin isolated capability.
+
+ // 3. If connection is not an HTTP/2 connection, request’s body is non-null,
+ // and request’s body’s source is null, then append (`Transfer-Encoding`,
+ // `chunked`) to request’s header list.
+
+ // 4. Set timingInfo’s final network-request start time to the coarsened
+ // shared current time given fetchParams’s cross-origin isolated
+ // capability.
+
+ // 5. Set response to the result of making an HTTP request over connection
+ // using request with the following caveats:
+
+ // - Follow the relevant requirements from HTTP. [HTTP] [HTTP-SEMANTICS]
+ // [HTTP-COND] [HTTP-CACHING] [HTTP-AUTH]
+
+ // - If request’s body is non-null, and request’s body’s source is null,
+ // then the user agent may have a buffer of up to 64 kibibytes and store
+ // a part of request’s body in that buffer. If the user agent reads from
+ // request’s body beyond that buffer’s size and the user agent needs to
+ // resend request, then instead return a network error.
+
+ // - Set timingInfo’s final network-response start time to the coarsened
+ // shared current time given fetchParams’s cross-origin isolated capability,
+ // immediately after the user agent’s HTTP parser receives the first byte
+ // of the response (e.g., frame header bytes for HTTP/2 or response status
+ // line for HTTP/1.x).
+
+ // - Wait until all the headers are transmitted.
+
+ // - Any responses whose status is in the range 100 to 199, inclusive,
+ // and is not 101, are to be ignored, except for the purposes of setting
+ // timingInfo’s final network-response start time above.
+
+ // - If request’s header list contains `Transfer-Encoding`/`chunked` and
+ // response is transferred via HTTP/1.0 or older, then return a network
+ // error.
+
+ // - If the HTTP request results in a TLS client certificate dialog, then:
+
+ // 1. If request’s window is an environment settings object, make the
+ // dialog available in request’s window.
+
+ // 2. Otherwise, return a network error.
+
+ // To transmit request’s body body, run these steps:
+ let requestBody = null
+ // 1. If body is null and fetchParams’s process request end-of-body is
+ // non-null, then queue a fetch task given fetchParams’s process request
+ // end-of-body and fetchParams’s task destination.
+ if (request.body == null && fetchParams.processRequestEndOfBody) {
+ queueMicrotask(() => fetchParams.processRequestEndOfBody())
+ } else if (request.body != null) {
+ // 2. Otherwise, if body is non-null:
+
+ // 1. Let processBodyChunk given bytes be these steps:
+ const processBodyChunk = async function * (bytes) {
+ // 1. If the ongoing fetch is terminated, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. Run this step in parallel: transmit bytes.
+ yield bytes
+
+ // 3. If fetchParams’s process request body is non-null, then run
+ // fetchParams’s process request body given bytes’s length.
+ fetchParams.processRequestBodyChunkLength?.(bytes.byteLength)
+ }
+
+ // 2. Let processEndOfBody be these steps:
+ const processEndOfBody = () => {
+ // 1. If fetchParams is canceled, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. If fetchParams’s process request end-of-body is non-null,
+ // then run fetchParams’s process request end-of-body.
+ if (fetchParams.processRequestEndOfBody) {
+ fetchParams.processRequestEndOfBody()
+ }
+ }
+
+ // 3. Let processBodyError given e be these steps:
+ const processBodyError = (e) => {
+ // 1. If fetchParams is canceled, then abort these steps.
+ if (isCancelled(fetchParams)) {
+ return
+ }
+
+ // 2. If e is an "AbortError" DOMException, then abort fetchParams’s controller.
+ if (e.name === 'AbortError') {
+ fetchParams.controller.abort()
+ } else {
+ fetchParams.controller.terminate(e)
+ }
+ }
+
+ // 4. Incrementally read request’s body given processBodyChunk, processEndOfBody,
+ // processBodyError, and fetchParams’s task destination.
+ requestBody = (async function * () {
+ try {
+ for await (const bytes of request.body.stream) {
+ yield * processBodyChunk(bytes)
+ }
+ processEndOfBody()
+ } catch (err) {
+ processBodyError(err)
+ }
+ })()
+ }
+
+ try {
+ // socket is only provided for websockets
+ const { body, status, statusText, headersList, socket } = await dispatch({ body: requestBody })
+
+ if (socket) {
+ response = makeResponse({ status, statusText, headersList, socket })
+ } else {
+ const iterator = body[Symbol.asyncIterator]()
+ fetchParams.controller.next = () => iterator.next()
+
+ response = makeResponse({ status, statusText, headersList })
+ }
+ } catch (err) {
+ // 10. If aborted, then:
+ if (err.name === 'AbortError') {
+ // 1. If connection uses HTTP/2, then transmit an RST_STREAM frame.
+ fetchParams.controller.connection.destroy()
+
+ // 2. Return the appropriate network error for fetchParams.
+ return makeAppropriateNetworkError(fetchParams, err)
+ }
+
+ return makeNetworkError(err)
+ }
+
+ // 11. Let pullAlgorithm be an action that resumes the ongoing fetch
+ // if it is suspended.
+ const pullAlgorithm = () => {
+ return fetchParams.controller.resume()
+ }
+
+ // 12. Let cancelAlgorithm be an algorithm that aborts fetchParams’s
+ // controller with reason, given reason.
+ const cancelAlgorithm = (reason) => {
+ // If the aborted fetch was already terminated, then we do not
+ // need to do anything.
+ if (!isCancelled(fetchParams)) {
+ fetchParams.controller.abort(reason)
+ }
+ }
+
+ // 13. Let highWaterMark be a non-negative, non-NaN number, chosen by
+ // the user agent.
+ // TODO
+
+ // 14. Let sizeAlgorithm be an algorithm that accepts a chunk object
+ // and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent.
+ // TODO
+
+ // 15. Let stream be a new ReadableStream.
+ // 16. Set up stream with byte reading support with pullAlgorithm set to pullAlgorithm,
+ // cancelAlgorithm set to cancelAlgorithm.
+ const stream = new ReadableStream(
+ {
+ start (controller) {
+ fetchParams.controller.controller = controller
+ },
+ pull: pullAlgorithm,
+ cancel: cancelAlgorithm,
+ type: 'bytes'
+ }
+ )
+
+ // 17. Run these steps, but abort when the ongoing fetch is terminated:
+
+ // 1. Set response’s body to a new body whose stream is stream.
+ response.body = { stream, source: null, length: null }
+
+ // 2. If response is not a network error and request’s cache mode is
+ // not "no-store", then update response in httpCache for request.
+ // TODO
+
+ // 3. If includeCredentials is true and the user agent is not configured
+ // to block cookies for request (see section 7 of [COOKIES]), then run the
+ // "set-cookie-string" parsing algorithm (see section 5.2 of [COOKIES]) on
+ // the value of each header whose name is a byte-case-insensitive match for
+ // `Set-Cookie` in response’s header list, if any, and request’s current URL.
+ // TODO
+
+ // 18. If aborted, then:
+ // TODO
+
+ // 19. Run these steps in parallel:
+
+ // 1. Run these steps, but abort when fetchParams is canceled:
+ if (!fetchParams.controller.resume) {
+ fetchParams.controller.on('terminated', onAborted)
+ }
+
+ fetchParams.controller.resume = async () => {
+ // 1. While true
+ while (true) {
+ // 1-3. See onData...
+
+ // 4. Set bytes to the result of handling content codings given
+ // codings and bytes.
+ let bytes
+ let isFailure
+ try {
+ const { done, value } = await fetchParams.controller.next()
+
+ if (isAborted(fetchParams)) {
+ break
+ }
+
+ bytes = done ? undefined : value
+ } catch (err) {
+ if (fetchParams.controller.ended && !timingInfo.encodedBodySize) {
+ // zlib doesn't like empty streams.
+ bytes = undefined
+ } else {
+ bytes = err
+
+ // err may be propagated from the result of calling readablestream.cancel,
+ // which might not be an error. https://github.com/nodejs/undici/issues/2009
+ isFailure = true
+ }
+ }
+
+ if (bytes === undefined) {
+ // 2. Otherwise, if the bytes transmission for response’s message
+ // body is done normally and stream is readable, then close
+ // stream, finalize response for fetchParams and response, and
+ // abort these in-parallel steps.
+ readableStreamClose(fetchParams.controller.controller)
+
+ finalizeResponse(fetchParams, response)
+
+ return
+ }
+
+ // 5. Increase timingInfo’s decoded body size by bytes’s length.
+ timingInfo.decodedBodySize += bytes?.byteLength ?? 0
+
+ // 6. If bytes is failure, then terminate fetchParams’s controller.
+ if (isFailure) {
+ fetchParams.controller.terminate(bytes)
+ return
+ }
+
+ // 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes
+ // into stream.
+ const buffer = new Uint8Array(bytes)
+ if (buffer.byteLength) {
+ fetchParams.controller.controller.enqueue(buffer)
+ }
+
+ // 8. If stream is errored, then terminate the ongoing fetch.
+ if (isErrored(stream)) {
+ fetchParams.controller.terminate()
+ return
+ }
+
+ // 9. If stream doesn’t need more data ask the user agent to suspend
+ // the ongoing fetch.
+ if (fetchParams.controller.controller.desiredSize <= 0) {
+ return
+ }
+ }
+ }
+
+ // 2. If aborted, then:
+ function onAborted (reason) {
+ // 2. If fetchParams is aborted, then:
+ if (isAborted(fetchParams)) {
+ // 1. Set response’s aborted flag.
+ response.aborted = true
+
+ // 2. If stream is readable, then error stream with the result of
+ // deserialize a serialized abort reason given fetchParams’s
+ // controller’s serialized abort reason and an
+ // implementation-defined realm.
+ if (isReadable(stream)) {
+ fetchParams.controller.controller.error(
+ fetchParams.controller.serializedAbortReason
+ )
+ }
+ } else {
+ // 3. Otherwise, if stream is readable, error stream with a TypeError.
+ if (isReadable(stream)) {
+ fetchParams.controller.controller.error(new TypeError('terminated', {
+ cause: isErrorLike(reason) ? reason : undefined
+ }))
+ }
+ }
+
+ // 4. If connection uses HTTP/2, then transmit an RST_STREAM frame.
+ // 5. Otherwise, the user agent should close connection unless it would be bad for performance to do so.
+ fetchParams.controller.connection.destroy()
+ }
+
+ // 20. Return response.
+ return response
+
+ function dispatch ({ body }) {
+ const url = requestCurrentURL(request)
+ /** @type {import('../../..').Agent} */
+ const agent = fetchParams.controller.dispatcher
+
+ return new Promise((resolve, reject) => agent.dispatch(
+ {
+ path: url.pathname + url.search,
+ origin: url.origin,
+ method: request.method,
+ body: agent.isMockActive ? request.body && (request.body.source || request.body.stream) : body,
+ headers: request.headersList.entries,
+ maxRedirections: 0,
+ upgrade: request.mode === 'websocket' ? 'websocket' : undefined
+ },
+ {
+ body: null,
+ abort: null,
+
+ onConnect (abort) {
+ // TODO (fix): Do we need connection here?
+ const { connection } = fetchParams.controller
+
+ // Set timingInfo’s final connection timing info to the result of calling clamp and coarsen
+ // connection timing info with connection’s timing info, timingInfo’s post-redirect start
+ // time, and fetchParams’s cross-origin isolated capability.
+ // TODO: implement connection timing
+ timingInfo.finalConnectionTimingInfo = clampAndCoarsenConnectionTimingInfo(undefined, timingInfo.postRedirectStartTime, fetchParams.crossOriginIsolatedCapability)
+
+ if (connection.destroyed) {
+ abort(new DOMException('The operation was aborted.', 'AbortError'))
+ } else {
+ fetchParams.controller.on('terminated', abort)
+ this.abort = connection.abort = abort
+ }
+
+ // Set timingInfo’s final network-request start time to the coarsened shared current time given
+ // fetchParams’s cross-origin isolated capability.
+ timingInfo.finalNetworkRequestStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
+ },
+
+ onResponseStarted () {
+ // Set timingInfo’s final network-response start time to the coarsened shared current
+ // time given fetchParams’s cross-origin isolated capability, immediately after the
+ // user agent’s HTTP parser receives the first byte of the response (e.g., frame header
+ // bytes for HTTP/2 or response status line for HTTP/1.x).
+ timingInfo.finalNetworkResponseStartTime = coarsenedSharedCurrentTime(fetchParams.crossOriginIsolatedCapability)
+ },
+
+ onHeaders (status, rawHeaders, resume, statusText) {
+ if (status < 200) {
+ return false
+ }
+
+ const headersList = new HeadersList()
+
+ for (let i = 0; i < rawHeaders.length; i += 2) {
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
+ }
+ const location = headersList.get('location', true)
+
+ this.body = new Readable({ read: resume })
+
+ const willFollow = location && request.redirect === 'follow' &&
+ redirectStatusSet.has(status)
+
+ const decoders = []
+
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
+ if (request.method !== 'HEAD' && request.method !== 'CONNECT' && !nullBodyStatus.includes(status) && !willFollow) {
+ // https://www.rfc-editor.org/rfc/rfc7231#section-3.1.2.1
+ const contentEncoding = headersList.get('content-encoding', true)
+ // "All content-coding values are case-insensitive..."
+ /** @type {string[]} */
+ const codings = contentEncoding ? contentEncoding.toLowerCase().split(',') : []
+
+ // Limit the number of content-encodings to prevent resource exhaustion.
+ // CVE fix similar to urllib3 (GHSA-gm62-xv2j-4w53) and curl (CVE-2022-32206).
+ const maxContentEncodings = 5
+ if (codings.length > maxContentEncodings) {
+ reject(new Error(`too many content-encodings in response: ${codings.length}, maximum allowed is ${maxContentEncodings}`))
+ return true
+ }
+
+ for (let i = codings.length - 1; i >= 0; --i) {
+ const coding = codings[i].trim()
+ // https://www.rfc-editor.org/rfc/rfc9112.html#section-7.2
+ if (coding === 'x-gzip' || coding === 'gzip') {
+ decoders.push(zlib.createGunzip({
+ // Be less strict when decoding compressed responses, since sometimes
+ // servers send slightly invalid responses that are still accepted
+ // by common browsers.
+ // Always using Z_SYNC_FLUSH is what cURL does.
+ flush: zlib.constants.Z_SYNC_FLUSH,
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
+ }))
+ } else if (coding === 'deflate') {
+ decoders.push(createInflate({
+ flush: zlib.constants.Z_SYNC_FLUSH,
+ finishFlush: zlib.constants.Z_SYNC_FLUSH
+ }))
+ } else if (coding === 'br') {
+ decoders.push(zlib.createBrotliDecompress({
+ flush: zlib.constants.BROTLI_OPERATION_FLUSH,
+ finishFlush: zlib.constants.BROTLI_OPERATION_FLUSH
+ }))
+ } else if (coding === 'zstd' && hasZstd) {
+ decoders.push(zlib.createZstdDecompress({
+ flush: zlib.constants.ZSTD_e_continue,
+ finishFlush: zlib.constants.ZSTD_e_end
+ }))
+ } else {
+ decoders.length = 0
+ break
+ }
+ }
+ }
+
+ const onError = this.onError.bind(this)
+
+ resolve({
+ status,
+ statusText,
+ headersList,
+ body: decoders.length
+ ? pipeline(this.body, ...decoders, (err) => {
+ if (err) {
+ this.onError(err)
+ }
+ }).on('error', onError)
+ : this.body.on('error', onError)
+ })
+
+ return true
+ },
+
+ onData (chunk) {
+ if (fetchParams.controller.dump) {
+ return
+ }
+
+ // 1. If one or more bytes have been transmitted from response’s
+ // message body, then:
+
+ // 1. Let bytes be the transmitted bytes.
+ const bytes = chunk
+
+ // 2. Let codings be the result of extracting header list values
+ // given `Content-Encoding` and response’s header list.
+ // See pullAlgorithm.
+
+ // 3. Increase timingInfo’s encoded body size by bytes’s length.
+ timingInfo.encodedBodySize += bytes.byteLength
+
+ // 4. See pullAlgorithm...
+
+ return this.body.push(bytes)
+ },
+
+ onComplete () {
+ if (this.abort) {
+ fetchParams.controller.off('terminated', this.abort)
+ }
+
+ fetchParams.controller.ended = true
+
+ this.body.push(null)
+ },
+
+ onError (error) {
+ if (this.abort) {
+ fetchParams.controller.off('terminated', this.abort)
+ }
+
+ this.body?.destroy(error)
+
+ fetchParams.controller.terminate(error)
+
+ reject(error)
+ },
+
+ onRequestUpgrade (_controller, status, headers, socket) {
+ // We need to support 200 for websocket over h2 as per RFC-8441
+ // Absence of session means H1
+ if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
+ return false
+ }
+
+ const headersList = new HeadersList()
+
+ for (const [name, value] of Object.entries(headers)) {
+ if (value == null) {
+ continue
+ }
+
+ const headerName = name.toLowerCase()
+
+ if (Array.isArray(value)) {
+ for (const entry of value) {
+ headersList.append(headerName, String(entry), true)
+ }
+ } else {
+ headersList.append(headerName, String(value), true)
+ }
+ }
+
+ resolve({
+ status,
+ statusText: STATUS_CODES[status],
+ headersList,
+ socket
+ })
+
+ return true
+ },
+
+ onUpgrade (status, rawHeaders, socket) {
+ // We need to support 200 for websocket over h2 as per RFC-8441
+ // Absence of session means H1
+ if ((socket.session != null && status !== 200) || (socket.session == null && status !== 101)) {
+ return false
+ }
+
+ const headersList = new HeadersList()
+
+ for (let i = 0; i < rawHeaders.length; i += 2) {
+ headersList.append(bufferToLowerCasedHeaderName(rawHeaders[i]), rawHeaders[i + 1].toString('latin1'), true)
+ }
+
+ resolve({
+ status,
+ statusText: STATUS_CODES[status],
+ headersList,
+ socket
+ })
+
+ return true
+ }
+ }
+ ))
+ }
+}
+
+module.exports = {
+ fetch,
+ Fetch,
+ fetching,
+ finalizeAndReportTiming
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/request.js b/vanilla/node_modules/undici/lib/web/fetch/request.js
new file mode 100644
index 0000000..6ef40f9
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/request.js
@@ -0,0 +1,1115 @@
+/* globals AbortController */
+
+'use strict'
+
+const { extractBody, mixinBody, cloneBody, bodyUnusable } = require('./body')
+const { Headers, fill: fillHeaders, HeadersList, setHeadersGuard, getHeadersGuard, setHeadersList, getHeadersList } = require('./headers')
+const util = require('../../core/util')
+const nodeUtil = require('node:util')
+const {
+ isValidHTTPToken,
+ sameOrigin,
+ environmentSettingsObject
+} = require('./util')
+const {
+ forbiddenMethodsSet,
+ corsSafeListedMethodsSet,
+ referrerPolicy,
+ requestRedirect,
+ requestMode,
+ requestCredentials,
+ requestCache,
+ requestDuplex
+} = require('./constants')
+const { kEnumerableProperty, normalizedMethodRecordsBase, normalizedMethodRecords } = util
+const { webidl } = require('../webidl')
+const { URLSerializer } = require('./data-url')
+const { kConstruct } = require('../../core/symbols')
+const assert = require('node:assert')
+const { getMaxListeners, setMaxListeners, defaultMaxListeners } = require('node:events')
+
+const kAbortController = Symbol('abortController')
+
+const requestFinalizer = new FinalizationRegistry(({ signal, abort }) => {
+ signal.removeEventListener('abort', abort)
+})
+
+const dependentControllerMap = new WeakMap()
+
+let abortSignalHasEventHandlerLeakWarning
+
+try {
+ abortSignalHasEventHandlerLeakWarning = getMaxListeners(new AbortController().signal) > 0
+} catch {
+ abortSignalHasEventHandlerLeakWarning = false
+}
+
+function buildAbort (acRef) {
+ return abort
+
+ function abort () {
+ const ac = acRef.deref()
+ if (ac !== undefined) {
+ // Currently, there is a problem with FinalizationRegistry.
+ // https://github.com/nodejs/node/issues/49344
+ // https://github.com/nodejs/node/issues/47748
+ // In the case of abort, the first step is to unregister from it.
+ // If the controller can refer to it, it is still registered.
+ // It will be removed in the future.
+ requestFinalizer.unregister(abort)
+
+ // Unsubscribe a listener.
+ // FinalizationRegistry will no longer be called, so this must be done.
+ this.removeEventListener('abort', abort)
+
+ ac.abort(this.reason)
+
+ const controllerList = dependentControllerMap.get(ac.signal)
+
+ if (controllerList !== undefined) {
+ if (controllerList.size !== 0) {
+ for (const ref of controllerList) {
+ const ctrl = ref.deref()
+ if (ctrl !== undefined) {
+ ctrl.abort(this.reason)
+ }
+ }
+ controllerList.clear()
+ }
+ dependentControllerMap.delete(ac.signal)
+ }
+ }
+ }
+}
+
+let patchMethodWarning = false
+
+// https://fetch.spec.whatwg.org/#request-class
+class Request {
+ /** @type {AbortSignal} */
+ #signal
+
+ /** @type {import('../../dispatcher/dispatcher')} */
+ #dispatcher
+
+ /** @type {Headers} */
+ #headers
+
+ #state
+
+ // https://fetch.spec.whatwg.org/#dom-request
+ constructor (input, init = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (input === kConstruct) {
+ return
+ }
+
+ const prefix = 'Request constructor'
+ webidl.argumentLengthCheck(arguments, 1, prefix)
+
+ input = webidl.converters.RequestInfo(input)
+ init = webidl.converters.RequestInit(init)
+
+ // 1. Let request be null.
+ let request = null
+
+ // 2. Let fallbackMode be null.
+ let fallbackMode = null
+
+ // 3. Let baseURL be this’s relevant settings object’s API base URL.
+ const baseUrl = environmentSettingsObject.settingsObject.baseUrl
+
+ // 4. Let signal be null.
+ let signal = null
+
+ // 5. If input is a string, then:
+ if (typeof input === 'string') {
+ this.#dispatcher = init.dispatcher
+
+ // 1. Let parsedURL be the result of parsing input with baseURL.
+ // 2. If parsedURL is failure, then throw a TypeError.
+ let parsedURL
+ try {
+ parsedURL = new URL(input, baseUrl)
+ } catch (err) {
+ throw new TypeError('Failed to parse URL from ' + input, { cause: err })
+ }
+
+ // 3. If parsedURL includes credentials, then throw a TypeError.
+ if (parsedURL.username || parsedURL.password) {
+ throw new TypeError(
+ 'Request cannot be constructed from a URL that includes credentials: ' +
+ input
+ )
+ }
+
+ // 4. Set request to a new request whose URL is parsedURL.
+ request = makeRequest({ urlList: [parsedURL] })
+
+ // 5. Set fallbackMode to "cors".
+ fallbackMode = 'cors'
+ } else {
+ // 6. Otherwise:
+
+ // 7. Assert: input is a Request object.
+ assert(webidl.is.Request(input))
+
+ // 8. Set request to input’s request.
+ request = input.#state
+
+ // 9. Set signal to input’s signal.
+ signal = input.#signal
+
+ this.#dispatcher = init.dispatcher || input.#dispatcher
+ }
+
+ // 7. Let origin be this’s relevant settings object’s origin.
+ const origin = environmentSettingsObject.settingsObject.origin
+
+ // 8. Let window be "client".
+ let window = 'client'
+
+ // 9. If request’s window is an environment settings object and its origin
+ // is same origin with origin, then set window to request’s window.
+ if (
+ request.window?.constructor?.name === 'EnvironmentSettingsObject' &&
+ sameOrigin(request.window, origin)
+ ) {
+ window = request.window
+ }
+
+ // 10. If init["window"] exists and is non-null, then throw a TypeError.
+ if (init.window != null) {
+ throw new TypeError(`'window' option '${window}' must be null`)
+ }
+
+ // 11. If init["window"] exists, then set window to "no-window".
+ if ('window' in init) {
+ window = 'no-window'
+ }
+
+ // 12. Set request to a new request with the following properties:
+ request = makeRequest({
+ // URL request’s URL.
+ // undici implementation note: this is set as the first item in request's urlList in makeRequest
+ // method request’s method.
+ method: request.method,
+ // header list A copy of request’s header list.
+ // undici implementation note: headersList is cloned in makeRequest
+ headersList: request.headersList,
+ // unsafe-request flag Set.
+ unsafeRequest: request.unsafeRequest,
+ // client This’s relevant settings object.
+ client: environmentSettingsObject.settingsObject,
+ // window window.
+ window,
+ // priority request’s priority.
+ priority: request.priority,
+ // origin request’s origin. The propagation of the origin is only significant for navigation requests
+ // being handled by a service worker. In this scenario a request can have an origin that is different
+ // from the current client.
+ origin: request.origin,
+ // referrer request’s referrer.
+ referrer: request.referrer,
+ // referrer policy request’s referrer policy.
+ referrerPolicy: request.referrerPolicy,
+ // mode request’s mode.
+ mode: request.mode,
+ // credentials mode request’s credentials mode.
+ credentials: request.credentials,
+ // cache mode request’s cache mode.
+ cache: request.cache,
+ // redirect mode request’s redirect mode.
+ redirect: request.redirect,
+ // integrity metadata request’s integrity metadata.
+ integrity: request.integrity,
+ // keepalive request’s keepalive.
+ keepalive: request.keepalive,
+ // reload-navigation flag request’s reload-navigation flag.
+ reloadNavigation: request.reloadNavigation,
+ // history-navigation flag request’s history-navigation flag.
+ historyNavigation: request.historyNavigation,
+ // URL list A clone of request’s URL list.
+ urlList: [...request.urlList]
+ })
+
+ const initHasKey = Object.keys(init).length !== 0
+
+ // 13. If init is not empty, then:
+ if (initHasKey) {
+ // 1. If request’s mode is "navigate", then set it to "same-origin".
+ if (request.mode === 'navigate') {
+ request.mode = 'same-origin'
+ }
+
+ // 2. Unset request’s reload-navigation flag.
+ request.reloadNavigation = false
+
+ // 3. Unset request’s history-navigation flag.
+ request.historyNavigation = false
+
+ // 4. Set request’s origin to "client".
+ request.origin = 'client'
+
+ // 5. Set request’s referrer to "client"
+ request.referrer = 'client'
+
+ // 6. Set request’s referrer policy to the empty string.
+ request.referrerPolicy = ''
+
+ // 7. Set request’s URL to request’s current URL.
+ request.url = request.urlList[request.urlList.length - 1]
+
+ // 8. Set request’s URL list to « request’s URL ».
+ request.urlList = [request.url]
+ }
+
+ // 14. If init["referrer"] exists, then:
+ if (init.referrer !== undefined) {
+ // 1. Let referrer be init["referrer"].
+ const referrer = init.referrer
+
+ // 2. If referrer is the empty string, then set request’s referrer to "no-referrer".
+ if (referrer === '') {
+ request.referrer = 'no-referrer'
+ } else {
+ // 1. Let parsedReferrer be the result of parsing referrer with
+ // baseURL.
+ // 2. If parsedReferrer is failure, then throw a TypeError.
+ let parsedReferrer
+ try {
+ parsedReferrer = new URL(referrer, baseUrl)
+ } catch (err) {
+ throw new TypeError(`Referrer "${referrer}" is not a valid URL.`, { cause: err })
+ }
+
+ // 3. If one of the following is true
+ // - parsedReferrer’s scheme is "about" and path is the string "client"
+ // - parsedReferrer’s origin is not same origin with origin
+ // then set request’s referrer to "client".
+ if (
+ (parsedReferrer.protocol === 'about:' && parsedReferrer.hostname === 'client') ||
+ (origin && !sameOrigin(parsedReferrer, environmentSettingsObject.settingsObject.baseUrl))
+ ) {
+ request.referrer = 'client'
+ } else {
+ // 4. Otherwise, set request’s referrer to parsedReferrer.
+ request.referrer = parsedReferrer
+ }
+ }
+ }
+
+ // 15. If init["referrerPolicy"] exists, then set request’s referrer policy
+ // to it.
+ if (init.referrerPolicy !== undefined) {
+ request.referrerPolicy = init.referrerPolicy
+ }
+
+ // 16. Let mode be init["mode"] if it exists, and fallbackMode otherwise.
+ let mode
+ if (init.mode !== undefined) {
+ mode = init.mode
+ } else {
+ mode = fallbackMode
+ }
+
+ // 17. If mode is "navigate", then throw a TypeError.
+ if (mode === 'navigate') {
+ throw webidl.errors.exception({
+ header: 'Request constructor',
+ message: 'invalid request mode navigate.'
+ })
+ }
+
+ // 18. If mode is non-null, set request’s mode to mode.
+ if (mode != null) {
+ request.mode = mode
+ }
+
+ // 19. If init["credentials"] exists, then set request’s credentials mode
+ // to it.
+ if (init.credentials !== undefined) {
+ request.credentials = init.credentials
+ }
+
+ // 18. If init["cache"] exists, then set request’s cache mode to it.
+ if (init.cache !== undefined) {
+ request.cache = init.cache
+ }
+
+ // 21. If request’s cache mode is "only-if-cached" and request’s mode is
+ // not "same-origin", then throw a TypeError.
+ if (request.cache === 'only-if-cached' && request.mode !== 'same-origin') {
+ throw new TypeError(
+ "'only-if-cached' can be set only with 'same-origin' mode"
+ )
+ }
+
+ // 22. If init["redirect"] exists, then set request’s redirect mode to it.
+ if (init.redirect !== undefined) {
+ request.redirect = init.redirect
+ }
+
+ // 23. If init["integrity"] exists, then set request’s integrity metadata to it.
+ if (init.integrity != null) {
+ request.integrity = String(init.integrity)
+ }
+
+ // 24. If init["keepalive"] exists, then set request’s keepalive to it.
+ if (init.keepalive !== undefined) {
+ request.keepalive = Boolean(init.keepalive)
+ }
+
+ // 25. If init["method"] exists, then:
+ if (init.method !== undefined) {
+ // 1. Let method be init["method"].
+ let method = init.method
+
+ const mayBeNormalized = normalizedMethodRecords[method]
+
+ if (mayBeNormalized !== undefined) {
+ // Note: Bypass validation DELETE, GET, HEAD, OPTIONS, POST, PUT, PATCH and these lowercase ones
+ request.method = mayBeNormalized
+ } else {
+ // 2. If method is not a method or method is a forbidden method, then
+ // throw a TypeError.
+ if (!isValidHTTPToken(method)) {
+ throw new TypeError(`'${method}' is not a valid HTTP method.`)
+ }
+
+ const upperCase = method.toUpperCase()
+
+ if (forbiddenMethodsSet.has(upperCase)) {
+ throw new TypeError(`'${method}' HTTP method is unsupported.`)
+ }
+
+ // 3. Normalize method.
+ // https://fetch.spec.whatwg.org/#concept-method-normalize
+ // Note: must be in uppercase
+ method = normalizedMethodRecordsBase[upperCase] ?? method
+
+ // 4. Set request’s method to method.
+ request.method = method
+ }
+
+ if (!patchMethodWarning && request.method === 'patch') {
+ process.emitWarning('Using `patch` is highly likely to result in a `405 Method Not Allowed`. `PATCH` is much more likely to succeed.', {
+ code: 'UNDICI-FETCH-patch'
+ })
+
+ patchMethodWarning = true
+ }
+ }
+
+ // 26. If init["signal"] exists, then set signal to it.
+ if (init.signal !== undefined) {
+ signal = init.signal
+ }
+
+ // 27. Set this’s request to request.
+ this.#state = request
+
+ // 28. Set this’s signal to a new AbortSignal object with this’s relevant
+ // Realm.
+ // TODO: could this be simplified with AbortSignal.any
+ // (https://dom.spec.whatwg.org/#dom-abortsignal-any)
+ const ac = new AbortController()
+ this.#signal = ac.signal
+
+ // 29. If signal is not null, then make this’s signal follow signal.
+ if (signal != null) {
+ if (signal.aborted) {
+ ac.abort(signal.reason)
+ } else {
+ // Keep a strong ref to ac while request object
+ // is alive. This is needed to prevent AbortController
+ // from being prematurely garbage collected.
+ // See, https://github.com/nodejs/undici/issues/1926.
+ this[kAbortController] = ac
+
+ const acRef = new WeakRef(ac)
+ const abort = buildAbort(acRef)
+
+ // If the max amount of listeners is equal to the default, increase it
+ if (abortSignalHasEventHandlerLeakWarning && getMaxListeners(signal) === defaultMaxListeners) {
+ setMaxListeners(1500, signal)
+ }
+
+ util.addAbortListener(signal, abort)
+ // The third argument must be a registry key to be unregistered.
+ // Without it, you cannot unregister.
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
+ // abort is used as the unregister key. (because it is unique)
+ requestFinalizer.register(ac, { signal, abort }, abort)
+ }
+ }
+
+ // 30. Set this’s headers to a new Headers object with this’s relevant
+ // Realm, whose header list is request’s header list and guard is
+ // "request".
+ this.#headers = new Headers(kConstruct)
+ setHeadersList(this.#headers, request.headersList)
+ setHeadersGuard(this.#headers, 'request')
+
+ // 31. If this’s request’s mode is "no-cors", then:
+ if (mode === 'no-cors') {
+ // 1. If this’s request’s method is not a CORS-safelisted method,
+ // then throw a TypeError.
+ if (!corsSafeListedMethodsSet.has(request.method)) {
+ throw new TypeError(
+ `'${request.method} is unsupported in no-cors mode.`
+ )
+ }
+
+ // 2. Set this’s headers’s guard to "request-no-cors".
+ setHeadersGuard(this.#headers, 'request-no-cors')
+ }
+
+ // 32. If init is not empty, then:
+ if (initHasKey) {
+ /** @type {HeadersList} */
+ const headersList = getHeadersList(this.#headers)
+ // 1. Let headers be a copy of this’s headers and its associated header
+ // list.
+ // 2. If init["headers"] exists, then set headers to init["headers"].
+ const headers = init.headers !== undefined ? init.headers : new HeadersList(headersList)
+
+ // 3. Empty this’s headers’s header list.
+ headersList.clear()
+
+ // 4. If headers is a Headers object, then for each header in its header
+ // list, append header’s name/header’s value to this’s headers.
+ if (headers instanceof HeadersList) {
+ for (const { name, value } of headers.rawValues()) {
+ headersList.append(name, value, false)
+ }
+ // Note: Copy the `set-cookie` meta-data.
+ headersList.cookies = headers.cookies
+ } else {
+ // 5. Otherwise, fill this’s headers with headers.
+ fillHeaders(this.#headers, headers)
+ }
+ }
+
+ // 33. Let inputBody be input’s request’s body if input is a Request
+ // object; otherwise null.
+ const inputBody = webidl.is.Request(input) ? input.#state.body : null
+
+ // 34. If either init["body"] exists and is non-null or inputBody is
+ // non-null, and request’s method is `GET` or `HEAD`, then throw a
+ // TypeError.
+ if (
+ (init.body != null || inputBody != null) &&
+ (request.method === 'GET' || request.method === 'HEAD')
+ ) {
+ throw new TypeError('Request with GET/HEAD method cannot have body.')
+ }
+
+ // 35. Let initBody be null.
+ let initBody = null
+
+ // 36. If init["body"] exists and is non-null, then:
+ if (init.body != null) {
+ // 1. Let Content-Type be null.
+ // 2. Set initBody and Content-Type to the result of extracting
+ // init["body"], with keepalive set to request’s keepalive.
+ const [extractedBody, contentType] = extractBody(
+ init.body,
+ request.keepalive
+ )
+ initBody = extractedBody
+
+ // 3, If Content-Type is non-null and this’s headers’s header list does
+ // not contain `Content-Type`, then append `Content-Type`/Content-Type to
+ // this’s headers.
+ if (contentType && !getHeadersList(this.#headers).contains('content-type', true)) {
+ this.#headers.append('content-type', contentType, true)
+ }
+ }
+
+ // 37. Let inputOrInitBody be initBody if it is non-null; otherwise
+ // inputBody.
+ const inputOrInitBody = initBody ?? inputBody
+
+ // 38. If inputOrInitBody is non-null and inputOrInitBody’s source is
+ // null, then:
+ if (inputOrInitBody != null && inputOrInitBody.source == null) {
+ // 1. If initBody is non-null and init["duplex"] does not exist,
+ // then throw a TypeError.
+ if (initBody != null && init.duplex == null) {
+ throw new TypeError('RequestInit: duplex option is required when sending a body.')
+ }
+
+ // 2. If this’s request’s mode is neither "same-origin" nor "cors",
+ // then throw a TypeError.
+ if (request.mode !== 'same-origin' && request.mode !== 'cors') {
+ throw new TypeError(
+ 'If request is made from ReadableStream, mode should be "same-origin" or "cors"'
+ )
+ }
+
+ // 3. Set this’s request’s use-CORS-preflight flag.
+ request.useCORSPreflightFlag = true
+ }
+
+ // 39. Let finalBody be inputOrInitBody.
+ let finalBody = inputOrInitBody
+
+ // 40. If initBody is null and inputBody is non-null, then:
+ if (initBody == null && inputBody != null) {
+ // 1. If input is unusable, then throw a TypeError.
+ if (bodyUnusable(input.#state)) {
+ throw new TypeError(
+ 'Cannot construct a Request with a Request object that has already been used.'
+ )
+ }
+
+ // 2. Set finalBody to the result of creating a proxy for inputBody.
+ // https://streams.spec.whatwg.org/#readablestream-create-a-proxy
+ const identityTransform = new TransformStream()
+ inputBody.stream.pipeThrough(identityTransform)
+ finalBody = {
+ source: inputBody.source,
+ length: inputBody.length,
+ stream: identityTransform.readable
+ }
+ }
+
+ // 41. Set this’s request’s body to finalBody.
+ this.#state.body = finalBody
+ }
+
+ // Returns request’s HTTP method, which is "GET" by default.
+ get method () {
+ webidl.brandCheck(this, Request)
+
+ // The method getter steps are to return this’s request’s method.
+ return this.#state.method
+ }
+
+ // Returns the URL of request as a string.
+ get url () {
+ webidl.brandCheck(this, Request)
+
+ // The url getter steps are to return this’s request’s URL, serialized.
+ return URLSerializer(this.#state.url)
+ }
+
+ // Returns a Headers object consisting of the headers associated with request.
+ // Note that headers added in the network layer by the user agent will not
+ // be accounted for in this object, e.g., the "Host" header.
+ get headers () {
+ webidl.brandCheck(this, Request)
+
+ // The headers getter steps are to return this’s headers.
+ return this.#headers
+ }
+
+ // Returns the kind of resource requested by request, e.g., "document"
+ // or "script".
+ get destination () {
+ webidl.brandCheck(this, Request)
+
+ // The destination getter are to return this’s request’s destination.
+ return this.#state.destination
+ }
+
+ // Returns the referrer of request. Its value can be a same-origin URL if
+ // explicitly set in init, the empty string to indicate no referrer, and
+ // "about:client" when defaulting to the global’s default. This is used
+ // during fetching to determine the value of the `Referer` header of the
+ // request being made.
+ get referrer () {
+ webidl.brandCheck(this, Request)
+
+ // 1. If this’s request’s referrer is "no-referrer", then return the
+ // empty string.
+ if (this.#state.referrer === 'no-referrer') {
+ return ''
+ }
+
+ // 2. If this’s request’s referrer is "client", then return
+ // "about:client".
+ if (this.#state.referrer === 'client') {
+ return 'about:client'
+ }
+
+ // Return this’s request’s referrer, serialized.
+ return this.#state.referrer.toString()
+ }
+
+ // Returns the referrer policy associated with request.
+ // This is used during fetching to compute the value of the request’s
+ // referrer.
+ get referrerPolicy () {
+ webidl.brandCheck(this, Request)
+
+ // The referrerPolicy getter steps are to return this’s request’s referrer policy.
+ return this.#state.referrerPolicy
+ }
+
+ // Returns the mode associated with request, which is a string indicating
+ // whether the request will use CORS, or will be restricted to same-origin
+ // URLs.
+ get mode () {
+ webidl.brandCheck(this, Request)
+
+ // The mode getter steps are to return this’s request’s mode.
+ return this.#state.mode
+ }
+
+ // Returns the credentials mode associated with request,
+ // which is a string indicating whether credentials will be sent with the
+ // request always, never, or only when sent to a same-origin URL.
+ get credentials () {
+ webidl.brandCheck(this, Request)
+
+ // The credentials getter steps are to return this’s request’s credentials mode.
+ return this.#state.credentials
+ }
+
+ // Returns the cache mode associated with request,
+ // which is a string indicating how the request will
+ // interact with the browser’s cache when fetching.
+ get cache () {
+ webidl.brandCheck(this, Request)
+
+ // The cache getter steps are to return this’s request’s cache mode.
+ return this.#state.cache
+ }
+
+ // Returns the redirect mode associated with request,
+ // which is a string indicating how redirects for the
+ // request will be handled during fetching. A request
+ // will follow redirects by default.
+ get redirect () {
+ webidl.brandCheck(this, Request)
+
+ // The redirect getter steps are to return this’s request’s redirect mode.
+ return this.#state.redirect
+ }
+
+ // Returns request’s subresource integrity metadata, which is a
+ // cryptographic hash of the resource being fetched. Its value
+ // consists of multiple hashes separated by whitespace. [SRI]
+ get integrity () {
+ webidl.brandCheck(this, Request)
+
+ // The integrity getter steps are to return this’s request’s integrity
+ // metadata.
+ return this.#state.integrity
+ }
+
+ // Returns a boolean indicating whether or not request can outlive the
+ // global in which it was created.
+ get keepalive () {
+ webidl.brandCheck(this, Request)
+
+ // The keepalive getter steps are to return this’s request’s keepalive.
+ return this.#state.keepalive
+ }
+
+ // Returns a boolean indicating whether or not request is for a reload
+ // navigation.
+ get isReloadNavigation () {
+ webidl.brandCheck(this, Request)
+
+ // The isReloadNavigation getter steps are to return true if this’s
+ // request’s reload-navigation flag is set; otherwise false.
+ return this.#state.reloadNavigation
+ }
+
+ // Returns a boolean indicating whether or not request is for a history
+ // navigation (a.k.a. back-forward navigation).
+ get isHistoryNavigation () {
+ webidl.brandCheck(this, Request)
+
+ // The isHistoryNavigation getter steps are to return true if this’s request’s
+ // history-navigation flag is set; otherwise false.
+ return this.#state.historyNavigation
+ }
+
+ // Returns the signal associated with request, which is an AbortSignal
+ // object indicating whether or not request has been aborted, and its
+ // abort event handler.
+ get signal () {
+ webidl.brandCheck(this, Request)
+
+ // The signal getter steps are to return this’s signal.
+ return this.#signal
+ }
+
+ get body () {
+ webidl.brandCheck(this, Request)
+
+ return this.#state.body ? this.#state.body.stream : null
+ }
+
+ get bodyUsed () {
+ webidl.brandCheck(this, Request)
+
+ return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
+ }
+
+ get duplex () {
+ webidl.brandCheck(this, Request)
+
+ return 'half'
+ }
+
+ // Returns a clone of request.
+ clone () {
+ webidl.brandCheck(this, Request)
+
+ // 1. If this is unusable, then throw a TypeError.
+ if (bodyUnusable(this.#state)) {
+ throw new TypeError('unusable')
+ }
+
+ // 2. Let clonedRequest be the result of cloning this’s request.
+ const clonedRequest = cloneRequest(this.#state)
+
+ // 3. Let clonedRequestObject be the result of creating a Request object,
+ // given clonedRequest, this’s headers’s guard, and this’s relevant Realm.
+ // 4. Make clonedRequestObject’s signal follow this’s signal.
+ const ac = new AbortController()
+ if (this.signal.aborted) {
+ ac.abort(this.signal.reason)
+ } else {
+ let list = dependentControllerMap.get(this.signal)
+ if (list === undefined) {
+ list = new Set()
+ dependentControllerMap.set(this.signal, list)
+ }
+ const acRef = new WeakRef(ac)
+ list.add(acRef)
+ util.addAbortListener(
+ ac.signal,
+ buildAbort(acRef)
+ )
+ }
+
+ // 4. Return clonedRequestObject.
+ return fromInnerRequest(clonedRequest, this.#dispatcher, ac.signal, getHeadersGuard(this.#headers))
+ }
+
+ [nodeUtil.inspect.custom] (depth, options) {
+ if (options.depth === null) {
+ options.depth = 2
+ }
+
+ options.colors ??= true
+
+ const properties = {
+ method: this.method,
+ url: this.url,
+ headers: this.headers,
+ destination: this.destination,
+ referrer: this.referrer,
+ referrerPolicy: this.referrerPolicy,
+ mode: this.mode,
+ credentials: this.credentials,
+ cache: this.cache,
+ redirect: this.redirect,
+ integrity: this.integrity,
+ keepalive: this.keepalive,
+ isReloadNavigation: this.isReloadNavigation,
+ isHistoryNavigation: this.isHistoryNavigation,
+ signal: this.signal
+ }
+
+ return `Request ${nodeUtil.formatWithOptions(options, properties)}`
+ }
+
+ /**
+ * @param {Request} request
+ * @param {AbortSignal} newSignal
+ */
+ static setRequestSignal (request, newSignal) {
+ request.#signal = newSignal
+ return request
+ }
+
+ /**
+ * @param {Request} request
+ */
+ static getRequestDispatcher (request) {
+ return request.#dispatcher
+ }
+
+ /**
+ * @param {Request} request
+ * @param {import('../../dispatcher/dispatcher')} newDispatcher
+ */
+ static setRequestDispatcher (request, newDispatcher) {
+ request.#dispatcher = newDispatcher
+ }
+
+ /**
+ * @param {Request} request
+ * @param {Headers} newHeaders
+ */
+ static setRequestHeaders (request, newHeaders) {
+ request.#headers = newHeaders
+ }
+
+ /**
+ * @param {Request} request
+ */
+ static getRequestState (request) {
+ return request.#state
+ }
+
+ /**
+ * @param {Request} request
+ * @param {any} newState
+ */
+ static setRequestState (request, newState) {
+ request.#state = newState
+ }
+}
+
+const { setRequestSignal, getRequestDispatcher, setRequestDispatcher, setRequestHeaders, getRequestState, setRequestState } = Request
+Reflect.deleteProperty(Request, 'setRequestSignal')
+Reflect.deleteProperty(Request, 'getRequestDispatcher')
+Reflect.deleteProperty(Request, 'setRequestDispatcher')
+Reflect.deleteProperty(Request, 'setRequestHeaders')
+Reflect.deleteProperty(Request, 'getRequestState')
+Reflect.deleteProperty(Request, 'setRequestState')
+
+mixinBody(Request, getRequestState)
+
+// https://fetch.spec.whatwg.org/#requests
+function makeRequest (init) {
+ return {
+ method: init.method ?? 'GET',
+ localURLsOnly: init.localURLsOnly ?? false,
+ unsafeRequest: init.unsafeRequest ?? false,
+ body: init.body ?? null,
+ client: init.client ?? null,
+ reservedClient: init.reservedClient ?? null,
+ replacesClientId: init.replacesClientId ?? '',
+ window: init.window ?? 'client',
+ keepalive: init.keepalive ?? false,
+ serviceWorkers: init.serviceWorkers ?? 'all',
+ initiator: init.initiator ?? '',
+ destination: init.destination ?? '',
+ priority: init.priority ?? null,
+ origin: init.origin ?? 'client',
+ policyContainer: init.policyContainer ?? 'client',
+ referrer: init.referrer ?? 'client',
+ referrerPolicy: init.referrerPolicy ?? '',
+ mode: init.mode ?? 'no-cors',
+ useCORSPreflightFlag: init.useCORSPreflightFlag ?? false,
+ credentials: init.credentials ?? 'same-origin',
+ useCredentials: init.useCredentials ?? false,
+ cache: init.cache ?? 'default',
+ redirect: init.redirect ?? 'follow',
+ integrity: init.integrity ?? '',
+ cryptoGraphicsNonceMetadata: init.cryptoGraphicsNonceMetadata ?? '',
+ parserMetadata: init.parserMetadata ?? '',
+ reloadNavigation: init.reloadNavigation ?? false,
+ historyNavigation: init.historyNavigation ?? false,
+ userActivation: init.userActivation ?? false,
+ taintedOrigin: init.taintedOrigin ?? false,
+ redirectCount: init.redirectCount ?? 0,
+ responseTainting: init.responseTainting ?? 'basic',
+ preventNoCacheCacheControlHeaderModification: init.preventNoCacheCacheControlHeaderModification ?? false,
+ done: init.done ?? false,
+ timingAllowFailed: init.timingAllowFailed ?? false,
+ useURLCredentials: init.useURLCredentials ?? undefined,
+ traversableForUserPrompts: init.traversableForUserPrompts ?? 'client',
+ urlList: init.urlList,
+ url: init.urlList[0],
+ headersList: init.headersList
+ ? new HeadersList(init.headersList)
+ : new HeadersList()
+ }
+}
+
+// https://fetch.spec.whatwg.org/#concept-request-clone
+function cloneRequest (request) {
+ // To clone a request request, run these steps:
+
+ // 1. Let newRequest be a copy of request, except for its body.
+ const newRequest = makeRequest({ ...request, body: null })
+
+ // 2. If request’s body is non-null, set newRequest’s body to the
+ // result of cloning request’s body.
+ if (request.body != null) {
+ newRequest.body = cloneBody(request.body)
+ }
+
+ // 3. Return newRequest.
+ return newRequest
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#request-create
+ * @param {any} innerRequest
+ * @param {import('../../dispatcher/agent')} dispatcher
+ * @param {AbortSignal} signal
+ * @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
+ * @returns {Request}
+ */
+function fromInnerRequest (innerRequest, dispatcher, signal, guard) {
+ const request = new Request(kConstruct)
+ setRequestState(request, innerRequest)
+ setRequestDispatcher(request, dispatcher)
+ setRequestSignal(request, signal)
+ const headers = new Headers(kConstruct)
+ setRequestHeaders(request, headers)
+ setHeadersList(headers, innerRequest.headersList)
+ setHeadersGuard(headers, guard)
+ return request
+}
+
+Object.defineProperties(Request.prototype, {
+ method: kEnumerableProperty,
+ url: kEnumerableProperty,
+ headers: kEnumerableProperty,
+ redirect: kEnumerableProperty,
+ clone: kEnumerableProperty,
+ signal: kEnumerableProperty,
+ duplex: kEnumerableProperty,
+ destination: kEnumerableProperty,
+ body: kEnumerableProperty,
+ bodyUsed: kEnumerableProperty,
+ isHistoryNavigation: kEnumerableProperty,
+ isReloadNavigation: kEnumerableProperty,
+ keepalive: kEnumerableProperty,
+ integrity: kEnumerableProperty,
+ cache: kEnumerableProperty,
+ credentials: kEnumerableProperty,
+ attribute: kEnumerableProperty,
+ referrerPolicy: kEnumerableProperty,
+ referrer: kEnumerableProperty,
+ mode: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Request',
+ configurable: true
+ }
+})
+
+webidl.is.Request = webidl.util.MakeTypeAssertion(Request)
+
+/**
+ * @param {*} V
+ * @returns {import('../../../types/fetch').Request|string}
+ *
+ * @see https://fetch.spec.whatwg.org/#requestinfo
+ */
+webidl.converters.RequestInfo = function (V) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V)
+ }
+
+ if (webidl.is.Request(V)) {
+ return V
+ }
+
+ return webidl.converters.USVString(V)
+}
+
+/**
+ * @param {*} V
+ * @returns {import('../../../types/fetch').RequestInit}
+ * @see 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: referrerPolicy
+ },
+ {
+ key: 'mode',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#concept-request-mode
+ allowedValues: requestMode
+ },
+ {
+ key: 'credentials',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestcredentials
+ allowedValues: requestCredentials
+ },
+ {
+ key: 'cache',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestcache
+ allowedValues: requestCache
+ },
+ {
+ key: 'redirect',
+ converter: webidl.converters.DOMString,
+ // https://fetch.spec.whatwg.org/#requestredirect
+ allowedValues: requestRedirect
+ },
+ {
+ key: 'integrity',
+ converter: webidl.converters.DOMString
+ },
+ {
+ key: 'keepalive',
+ converter: webidl.converters.boolean
+ },
+ {
+ key: 'signal',
+ converter: webidl.nullableConverter(
+ (signal) => webidl.converters.AbortSignal(
+ signal,
+ 'RequestInit',
+ 'signal'
+ )
+ )
+ },
+ {
+ key: 'window',
+ converter: webidl.converters.any
+ },
+ {
+ key: 'duplex',
+ converter: webidl.converters.DOMString,
+ allowedValues: requestDuplex
+ },
+ {
+ key: 'dispatcher', // undici specific option
+ converter: webidl.converters.any
+ },
+ {
+ key: 'priority',
+ converter: webidl.converters.DOMString,
+ allowedValues: ['high', 'low', 'auto'],
+ defaultValue: () => 'auto'
+ }
+])
+
+module.exports = {
+ Request,
+ makeRequest,
+ fromInnerRequest,
+ cloneRequest,
+ getRequestDispatcher,
+ getRequestState
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/response.js b/vanilla/node_modules/undici/lib/web/fetch/response.js
new file mode 100644
index 0000000..ffb7ce1
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/response.js
@@ -0,0 +1,641 @@
+'use strict'
+
+const { Headers, HeadersList, fill, getHeadersGuard, setHeadersGuard, setHeadersList } = require('./headers')
+const { extractBody, cloneBody, mixinBody, streamRegistry, bodyUnusable } = require('./body')
+const util = require('../../core/util')
+const nodeUtil = require('node:util')
+const { kEnumerableProperty } = util
+const {
+ isValidReasonPhrase,
+ isCancelled,
+ isAborted,
+ isErrorLike,
+ environmentSettingsObject: relevantRealm
+} = require('./util')
+const {
+ redirectStatusSet,
+ nullBodyStatus
+} = require('./constants')
+const { webidl } = require('../webidl')
+const { URLSerializer } = require('./data-url')
+const { kConstruct } = require('../../core/symbols')
+const assert = require('node:assert')
+const { isomorphicEncode, serializeJavascriptValueToJSONString } = require('../infra')
+
+const textEncoder = new TextEncoder('utf-8')
+
+// https://fetch.spec.whatwg.org/#response-class
+class Response {
+ /** @type {Headers} */
+ #headers
+
+ #state
+
+ // Creates network error Response.
+ static error () {
+ // The static error() method steps are to return the result of creating a
+ // Response object, given a new network error, "immutable", and this’s
+ // relevant Realm.
+ const responseObject = fromInnerResponse(makeNetworkError(), 'immutable')
+
+ return responseObject
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-response-json
+ static json (data, init = undefined) {
+ webidl.argumentLengthCheck(arguments, 1, 'Response.json')
+
+ if (init !== null) {
+ init = webidl.converters.ResponseInit(init)
+ }
+
+ // 1. Let bytes the result of running serialize a JavaScript value to JSON bytes on data.
+ const bytes = textEncoder.encode(
+ serializeJavascriptValueToJSONString(data)
+ )
+
+ // 2. Let body be the result of extracting bytes.
+ const body = extractBody(bytes)
+
+ // 3. Let responseObject be the result of creating a Response object, given a new response,
+ // "response", and this’s relevant Realm.
+ const responseObject = fromInnerResponse(makeResponse({}), 'response')
+
+ // 4. Perform initialize a response given responseObject, init, and (body, "application/json").
+ initializeResponse(responseObject, init, { body: body[0], type: 'application/json' })
+
+ // 5. Return responseObject.
+ return responseObject
+ }
+
+ // Creates a redirect Response that redirects to url with status status.
+ static redirect (url, status = 302) {
+ webidl.argumentLengthCheck(arguments, 1, 'Response.redirect')
+
+ 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.
+ // 2. If parsedURL is failure, then throw a TypeError.
+ // TODO: base-URL?
+ let parsedURL
+ try {
+ parsedURL = new URL(url, relevantRealm.settingsObject.baseUrl)
+ } catch (err) {
+ throw new TypeError(`Failed to parse URL from ${url}`, { cause: err })
+ }
+
+ // 3. If status is not a redirect status, then throw a RangeError.
+ if (!redirectStatusSet.has(status)) {
+ throw new RangeError(`Invalid status code ${status}`)
+ }
+
+ // 4. Let responseObject be the result of creating a Response object,
+ // given a new response, "immutable", and this’s relevant Realm.
+ const responseObject = fromInnerResponse(makeResponse({}), 'immutable')
+
+ // 5. Set responseObject’s response’s status to status.
+ responseObject.#state.status = status
+
+ // 6. Let value be parsedURL, serialized and isomorphic encoded.
+ const value = isomorphicEncode(URLSerializer(parsedURL))
+
+ // 7. Append `Location`/value to responseObject’s response’s header list.
+ responseObject.#state.headersList.append('location', value, true)
+
+ // 8. Return responseObject.
+ return responseObject
+ }
+
+ // https://fetch.spec.whatwg.org/#dom-response
+ constructor (body = null, init = undefined) {
+ webidl.util.markAsUncloneable(this)
+
+ if (body === kConstruct) {
+ return
+ }
+
+ if (body !== null) {
+ body = webidl.converters.BodyInit(body, 'Response', 'body')
+ }
+
+ init = webidl.converters.ResponseInit(init)
+
+ // 1. Set this’s response to a new response.
+ this.#state = makeResponse({})
+
+ // 2. Set this’s headers to a new Headers object with this’s relevant
+ // Realm, whose header list is this’s response’s header list and guard
+ // is "response".
+ this.#headers = new Headers(kConstruct)
+ setHeadersGuard(this.#headers, 'response')
+ setHeadersList(this.#headers, this.#state.headersList)
+
+ // 3. Let bodyWithType be null.
+ let bodyWithType = null
+
+ // 4. If body is non-null, then set bodyWithType to the result of extracting body.
+ if (body != null) {
+ const [extractedBody, type] = extractBody(body)
+ bodyWithType = { body: extractedBody, type }
+ }
+
+ // 5. Perform initialize a response given this, init, and bodyWithType.
+ initializeResponse(this, init, bodyWithType)
+ }
+
+ // Returns response’s type, e.g., "cors".
+ get type () {
+ webidl.brandCheck(this, Response)
+
+ // The type getter steps are to return this’s response’s type.
+ return this.#state.type
+ }
+
+ // Returns response’s URL, if it has one; otherwise the empty string.
+ get url () {
+ webidl.brandCheck(this, Response)
+
+ const urlList = this.#state.urlList
+
+ // The url getter steps are to return the empty string if this’s
+ // response’s URL is null; otherwise this’s response’s URL,
+ // serialized with exclude fragment set to true.
+ const url = urlList[urlList.length - 1] ?? null
+
+ if (url === null) {
+ return ''
+ }
+
+ return URLSerializer(url, true)
+ }
+
+ // Returns whether response was obtained through a redirect.
+ get redirected () {
+ webidl.brandCheck(this, Response)
+
+ // The redirected getter steps are to return true if this’s response’s URL
+ // list has more than one item; otherwise false.
+ return this.#state.urlList.length > 1
+ }
+
+ // Returns response’s status.
+ get status () {
+ webidl.brandCheck(this, Response)
+
+ // The status getter steps are to return this’s response’s status.
+ return this.#state.status
+ }
+
+ // Returns whether response’s status is an ok status.
+ get ok () {
+ webidl.brandCheck(this, Response)
+
+ // The ok getter steps are to return true if this’s response’s status is an
+ // ok status; otherwise false.
+ return this.#state.status >= 200 && this.#state.status <= 299
+ }
+
+ // Returns response’s status message.
+ get statusText () {
+ webidl.brandCheck(this, Response)
+
+ // The statusText getter steps are to return this’s response’s status
+ // message.
+ return this.#state.statusText
+ }
+
+ // Returns response’s headers as Headers.
+ get headers () {
+ webidl.brandCheck(this, Response)
+
+ // The headers getter steps are to return this’s headers.
+ return this.#headers
+ }
+
+ get body () {
+ webidl.brandCheck(this, Response)
+
+ return this.#state.body ? this.#state.body.stream : null
+ }
+
+ get bodyUsed () {
+ webidl.brandCheck(this, Response)
+
+ return !!this.#state.body && util.isDisturbed(this.#state.body.stream)
+ }
+
+ // Returns a clone of response.
+ clone () {
+ webidl.brandCheck(this, Response)
+
+ // 1. If this is unusable, then throw a TypeError.
+ if (bodyUnusable(this.#state)) {
+ throw webidl.errors.exception({
+ header: 'Response.clone',
+ message: 'Body has already been consumed.'
+ })
+ }
+
+ // 2. Let clonedResponse be the result of cloning this’s response.
+ const clonedResponse = cloneResponse(this.#state)
+
+ // Note: To re-register because of a new stream.
+ // Don't set finalizers other than for fetch responses.
+ if (this.#state.urlList.length !== 0 && this.#state.body?.stream) {
+ streamRegistry.register(this, new WeakRef(this.#state.body.stream))
+ }
+
+ // 3. Return the result of creating a Response object, given
+ // clonedResponse, this’s headers’s guard, and this’s relevant Realm.
+ return fromInnerResponse(clonedResponse, getHeadersGuard(this.#headers))
+ }
+
+ [nodeUtil.inspect.custom] (depth, options) {
+ if (options.depth === null) {
+ options.depth = 2
+ }
+
+ options.colors ??= true
+
+ const properties = {
+ status: this.status,
+ statusText: this.statusText,
+ headers: this.headers,
+ body: this.body,
+ bodyUsed: this.bodyUsed,
+ ok: this.ok,
+ redirected: this.redirected,
+ type: this.type,
+ url: this.url
+ }
+
+ return `Response ${nodeUtil.formatWithOptions(options, properties)}`
+ }
+
+ /**
+ * @param {Response} response
+ */
+ static getResponseHeaders (response) {
+ return response.#headers
+ }
+
+ /**
+ * @param {Response} response
+ * @param {Headers} newHeaders
+ */
+ static setResponseHeaders (response, newHeaders) {
+ response.#headers = newHeaders
+ }
+
+ /**
+ * @param {Response} response
+ */
+ static getResponseState (response) {
+ return response.#state
+ }
+
+ /**
+ * @param {Response} response
+ * @param {any} newState
+ */
+ static setResponseState (response, newState) {
+ response.#state = newState
+ }
+}
+
+const { getResponseHeaders, setResponseHeaders, getResponseState, setResponseState } = Response
+Reflect.deleteProperty(Response, 'getResponseHeaders')
+Reflect.deleteProperty(Response, 'setResponseHeaders')
+Reflect.deleteProperty(Response, 'getResponseState')
+Reflect.deleteProperty(Response, 'setResponseState')
+
+mixinBody(Response, getResponseState)
+
+Object.defineProperties(Response.prototype, {
+ type: kEnumerableProperty,
+ url: kEnumerableProperty,
+ status: kEnumerableProperty,
+ ok: kEnumerableProperty,
+ redirected: kEnumerableProperty,
+ statusText: kEnumerableProperty,
+ headers: kEnumerableProperty,
+ clone: kEnumerableProperty,
+ body: kEnumerableProperty,
+ bodyUsed: kEnumerableProperty,
+ [Symbol.toStringTag]: {
+ value: 'Response',
+ configurable: true
+ }
+})
+
+Object.defineProperties(Response, {
+ json: kEnumerableProperty,
+ redirect: kEnumerableProperty,
+ error: kEnumerableProperty
+})
+
+// https://fetch.spec.whatwg.org/#concept-response-clone
+function cloneResponse (response) {
+ // To clone a response response, run these steps:
+
+ // 1. If response is a filtered response, then return a new identical
+ // filtered response whose internal response is a clone of response’s
+ // internal response.
+ if (response.internalResponse) {
+ return filterResponse(
+ cloneResponse(response.internalResponse),
+ response.type
+ )
+ }
+
+ // 2. Let newResponse be a copy of response, except for its body.
+ const newResponse = makeResponse({ ...response, body: null })
+
+ // 3. If response’s body is non-null, then set newResponse’s body to the
+ // result of cloning response’s body.
+ if (response.body != null) {
+ newResponse.body = cloneBody(response.body)
+ }
+
+ // 4. Return newResponse.
+ return newResponse
+}
+
+function makeResponse (init) {
+ return {
+ aborted: false,
+ rangeRequested: false,
+ timingAllowPassed: false,
+ requestIncludesCredentials: false,
+ type: 'default',
+ status: 200,
+ timingInfo: null,
+ cacheState: '',
+ statusText: '',
+ ...init,
+ headersList: init?.headersList
+ ? new HeadersList(init?.headersList)
+ : new HeadersList(),
+ urlList: init?.urlList ? [...init.urlList] : []
+ }
+}
+
+function makeNetworkError (reason) {
+ const isError = isErrorLike(reason)
+ return makeResponse({
+ type: 'error',
+ status: 0,
+ error: isError
+ ? reason
+ : new Error(reason ? String(reason) : reason),
+ aborted: reason && reason.name === 'AbortError'
+ })
+}
+
+// @see https://fetch.spec.whatwg.org/#concept-network-error
+function isNetworkError (response) {
+ return (
+ // A network error is a response whose type is "error",
+ response.type === 'error' &&
+ // status is 0
+ response.status === 0
+ )
+}
+
+function makeFilteredResponse (response, state) {
+ state = {
+ internalResponse: response,
+ ...state
+ }
+
+ return new Proxy(response, {
+ get (target, p) {
+ return p in state ? state[p] : target[p]
+ },
+ set (target, p, value) {
+ assert(!(p in state))
+ target[p] = value
+ return true
+ }
+ })
+}
+
+// https://fetch.spec.whatwg.org/#concept-filtered-response
+function filterResponse (response, type) {
+ // Set response to the following filtered response with response as its
+ // internal response, depending on request’s response tainting:
+ if (type === 'basic') {
+ // A basic filtered response is a filtered response whose type is "basic"
+ // and header list excludes any headers in internal response’s header list
+ // whose name is a forbidden response-header name.
+
+ // Note: undici does not implement forbidden response-header names
+ return makeFilteredResponse(response, {
+ type: 'basic',
+ headersList: response.headersList
+ })
+ } else if (type === 'cors') {
+ // A CORS filtered response is a filtered response whose type is "cors"
+ // and header list excludes any headers in internal response’s header
+ // list whose name is not a CORS-safelisted response-header name, given
+ // internal response’s CORS-exposed header-name list.
+
+ // Note: undici does not implement CORS-safelisted response-header names
+ return makeFilteredResponse(response, {
+ type: 'cors',
+ headersList: response.headersList
+ })
+ } else if (type === 'opaque') {
+ // An opaque filtered response is a filtered response whose type is
+ // "opaque", URL list is the empty list, status is 0, status message
+ // is the empty byte sequence, header list is empty, and body is null.
+
+ return makeFilteredResponse(response, {
+ type: 'opaque',
+ urlList: [],
+ status: 0,
+ statusText: '',
+ body: null
+ })
+ } else if (type === 'opaqueredirect') {
+ // An opaque-redirect filtered response is a filtered response whose type
+ // is "opaqueredirect", status is 0, status message is the empty byte
+ // sequence, header list is empty, and body is null.
+
+ return makeFilteredResponse(response, {
+ type: 'opaqueredirect',
+ status: 0,
+ statusText: '',
+ headersList: [],
+ body: null
+ })
+ } else {
+ assert(false)
+ }
+}
+
+// https://fetch.spec.whatwg.org/#appropriate-network-error
+function makeAppropriateNetworkError (fetchParams, err = null) {
+ // 1. Assert: fetchParams is canceled.
+ assert(isCancelled(fetchParams))
+
+ // 2. Return an aborted network error if fetchParams is aborted;
+ // otherwise return a network error.
+ return isAborted(fetchParams)
+ ? makeNetworkError(Object.assign(new DOMException('The operation was aborted.', 'AbortError'), { cause: err }))
+ : makeNetworkError(Object.assign(new DOMException('Request was cancelled.'), { cause: err }))
+}
+
+// https://whatpr.org/fetch/1392.html#initialize-a-response
+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)) {
+ throw new RangeError('init["status"] must be in the range of 200 to 599, inclusive.')
+ }
+
+ // 2. If init["statusText"] does not match the reason-phrase token production,
+ // then throw a TypeError.
+ if ('statusText' in init && init.statusText != null) {
+ // See, https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2:
+ // reason-phrase = *( HTAB / SP / VCHAR / obs-text )
+ if (!isValidReasonPhrase(String(init.statusText))) {
+ throw new TypeError('Invalid statusText')
+ }
+ }
+
+ // 3. Set response’s response’s status to init["status"].
+ if ('status' in init && init.status != null) {
+ getResponseState(response).status = init.status
+ }
+
+ // 4. Set response’s response’s status message to init["statusText"].
+ if ('statusText' in init && init.statusText != null) {
+ getResponseState(response).statusText = init.statusText
+ }
+
+ // 5. If init["headers"] exists, then fill response’s headers with init["headers"].
+ if ('headers' in init && init.headers != null) {
+ fill(getResponseHeaders(response), init.headers)
+ }
+
+ // 6. If body was given, then:
+ if (body) {
+ // 1. If response's status is a null body status, then throw a TypeError.
+ if (nullBodyStatus.includes(response.status)) {
+ throw webidl.errors.exception({
+ header: 'Response constructor',
+ message: `Invalid response status code ${response.status}`
+ })
+ }
+
+ // 2. Set response's body to body's body.
+ getResponseState(response).body = body.body
+
+ // 3. If body's type is non-null and response's header list does not contain
+ // `Content-Type`, then append (`Content-Type`, body's type) to response's header list.
+ if (body.type != null && !getResponseState(response).headersList.contains('content-type', true)) {
+ getResponseState(response).headersList.append('content-type', body.type, true)
+ }
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#response-create
+ * @param {any} innerResponse
+ * @param {'request' | 'immutable' | 'request-no-cors' | 'response' | 'none'} guard
+ * @returns {Response}
+ */
+function fromInnerResponse (innerResponse, guard) {
+ const response = new Response(kConstruct)
+ setResponseState(response, innerResponse)
+ const headers = new Headers(kConstruct)
+ setResponseHeaders(response, headers)
+ setHeadersList(headers, innerResponse.headersList)
+ setHeadersGuard(headers, guard)
+
+ // Note: If innerResponse's urlList contains a URL, it is a fetch response.
+ if (innerResponse.urlList.length !== 0 && innerResponse.body?.stream) {
+ // If the target (response) is reclaimed, the cleanup callback may be called at some point with
+ // the held value provided for it (innerResponse.body.stream). The held value can be any value:
+ // a primitive or an object, even undefined. If the held value is an object, the registry keeps
+ // a strong reference to it (so it can pass it to the cleanup callback later). Reworded from
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry
+ streamRegistry.register(response, new WeakRef(innerResponse.body.stream))
+ }
+
+ return response
+}
+
+// https://fetch.spec.whatwg.org/#typedefdef-xmlhttprequestbodyinit
+webidl.converters.XMLHttpRequestBodyInit = function (V, prefix, name) {
+ if (typeof V === 'string') {
+ return webidl.converters.USVString(V, prefix, name)
+ }
+
+ if (webidl.is.Blob(V)) {
+ return V
+ }
+
+ if (webidl.is.BufferSource(V)) {
+ return V
+ }
+
+ if (webidl.is.FormData(V)) {
+ return V
+ }
+
+ if (webidl.is.URLSearchParams(V)) {
+ return V
+ }
+
+ return webidl.converters.DOMString(V, prefix, name)
+}
+
+// https://fetch.spec.whatwg.org/#bodyinit
+webidl.converters.BodyInit = function (V, prefix, argument) {
+ if (webidl.is.ReadableStream(V)) {
+ return 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, prefix, argument)
+}
+
+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
+ }
+])
+
+webidl.is.Response = webidl.util.MakeTypeAssertion(Response)
+
+module.exports = {
+ isNetworkError,
+ makeNetworkError,
+ makeResponse,
+ makeAppropriateNetworkError,
+ filterResponse,
+ Response,
+ cloneResponse,
+ fromInnerResponse,
+ getResponseState
+}
diff --git a/vanilla/node_modules/undici/lib/web/fetch/util.js b/vanilla/node_modules/undici/lib/web/fetch/util.js
new file mode 100644
index 0000000..fe63cb3
--- /dev/null
+++ b/vanilla/node_modules/undici/lib/web/fetch/util.js
@@ -0,0 +1,1520 @@
+'use strict'
+
+const { Transform } = require('node:stream')
+const zlib = require('node:zlib')
+const { redirectStatusSet, referrerPolicyTokens, badPortsSet } = require('./constants')
+const { getGlobalOrigin } = require('./global')
+const { collectAnHTTPQuotedString, parseMIMEType } = require('./data-url')
+const { performance } = require('node:perf_hooks')
+const { ReadableStreamFrom, isValidHTTPToken, normalizedMethodRecordsBase } = require('../../core/util')
+const assert = require('node:assert')
+const { isUint8Array } = require('node:util/types')
+const { webidl } = require('../webidl')
+const { isomorphicEncode, collectASequenceOfCodePoints, removeChars } = require('../infra')
+
+function responseURL (response) {
+ // https://fetch.spec.whatwg.org/#responses
+ // A response has an associated URL. It is a pointer to the last URL
+ // in response’s URL list and null if response’s URL list is empty.
+ const urlList = response.urlList
+ const length = urlList.length
+ return length === 0 ? null : urlList[length - 1].toString()
+}
+
+// https://fetch.spec.whatwg.org/#concept-response-location-url
+function responseLocationURL (response, requestFragment) {
+ // 1. If response’s status is not a redirect status, then return null.
+ if (!redirectStatusSet.has(response.status)) {
+ return null
+ }
+
+ // 2. Let location be the result of extracting header list values given
+ // `Location` and response’s header list.
+ let location = response.headersList.get('location', true)
+
+ // 3. If location is a header value, then set location to the result of
+ // parsing location with response’s URL.
+ if (location !== null && isValidHeaderValue(location)) {
+ if (!isValidEncodedURL(location)) {
+ // Some websites respond location header in UTF-8 form without encoding them as ASCII
+ // and major browsers redirect them to correctly UTF-8 encoded addresses.
+ // Here, we handle that behavior in the same way.
+ location = normalizeBinaryStringToUtf8(location)
+ }
+ location = new URL(location, responseURL(response))
+ }
+
+ // 4. If location is a URL whose fragment is null, then set location’s
+ // fragment to requestFragment.
+ if (location && !location.hash) {
+ location.hash = requestFragment
+ }
+
+ // 5. Return location.
+ return location
+}
+
+/**
+ * @see https://www.rfc-editor.org/rfc/rfc1738#section-2.2
+ * @param {string} url
+ * @returns {boolean}
+ */
+function isValidEncodedURL (url) {
+ for (let i = 0; i < url.length; ++i) {
+ const code = url.charCodeAt(i)
+
+ if (
+ code > 0x7E || // Non-US-ASCII + DEL
+ code < 0x20 // Control characters NUL - US
+ ) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * If string contains non-ASCII characters, assumes it's UTF-8 encoded and decodes it.
+ * Since UTF-8 is a superset of ASCII, this will work for ASCII strings as well.
+ * @param {string} value
+ * @returns {string}
+ */
+function normalizeBinaryStringToUtf8 (value) {
+ return Buffer.from(value, 'binary').toString('utf8')
+}
+
+/** @returns {URL} */
+function requestCurrentURL (request) {
+ return request.urlList[request.urlList.length - 1]
+}
+
+function requestBadPort (request) {
+ // 1. Let url be request’s current URL.
+ const url = requestCurrentURL(request)
+
+ // 2. If url’s scheme is an HTTP(S) scheme and url’s port is a bad port,
+ // then return blocked.
+ if (urlIsHttpHttpsScheme(url) && badPortsSet.has(url.port)) {
+ return 'blocked'
+ }
+
+ // 3. Return allowed.
+ return 'allowed'
+}
+
+function isErrorLike (object) {
+ return object instanceof Error || (
+ object?.constructor?.name === 'Error' ||
+ object?.constructor?.name === 'DOMException'
+ )
+}
+
+// Check whether |statusText| is a ByteString and
+// matches the Reason-Phrase token production.
+// RFC 2616: https://tools.ietf.org/html/rfc2616
+// RFC 7230: https://tools.ietf.org/html/rfc7230
+// "reason-phrase = *( HTAB / SP / VCHAR / obs-text )"
+// https://github.com/chromium/chromium/blob/94.0.4604.1/third_party/blink/renderer/core/fetch/response.cc#L116
+function isValidReasonPhrase (statusText) {
+ for (let i = 0; i < statusText.length; ++i) {
+ const c = statusText.charCodeAt(i)
+ if (
+ !(
+ (
+ c === 0x09 || // HTAB
+ (c >= 0x20 && c <= 0x7e) || // SP / VCHAR
+ (c >= 0x80 && c <= 0xff)
+ ) // obs-text
+ )
+ ) {
+ return false
+ }
+ }
+ return true
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#header-name
+ * @param {string} potentialValue
+ */
+const isValidHeaderName = isValidHTTPToken
+
+/**
+ * @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.
+ return (
+ potentialValue[0] === '\t' ||
+ potentialValue[0] === ' ' ||
+ potentialValue[potentialValue.length - 1] === '\t' ||
+ potentialValue[potentialValue.length - 1] === ' ' ||
+ potentialValue.includes('\n') ||
+ potentialValue.includes('\r') ||
+ potentialValue.includes('\0')
+ ) === false
+}
+
+/**
+ * Parse a referrer policy from a Referrer-Policy header
+ * @see https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header
+ */
+function parseReferrerPolicy (actualResponse) {
+ // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` and response’s header list.
+ const policyHeader = (actualResponse.headersList.get('referrer-policy', true) ?? '').split(',')
+
+ // 2. Let policy be the empty string.
+ let policy = ''
+
+ // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty string, then set policy to token.
+
+ // Note: As the referrer-policy can contain multiple policies
+ // separated by comma, we need to loop through all of them
+ // and pick the first valid one.
+ // Ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy#specify_a_fallback_policy
+ if (policyHeader.length) {
+ // The right-most policy takes precedence.
+ // The left-most policy is the fallback.
+ for (let i = policyHeader.length; i !== 0; i--) {
+ const token = policyHeader[i - 1].trim()
+ if (referrerPolicyTokens.has(token)) {
+ policy = token
+ break
+ }
+ }
+ }
+
+ // 4. Return policy.
+ return policy
+}
+
+/**
+ * Given a request request and a response actualResponse, this algorithm
+ * updates request’s referrer policy according to the Referrer-Policy
+ * header (if any) in actualResponse.
+ * @see https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect
+ * @param {import('./request').Request} request
+ * @param {import('./response').Response} actualResponse
+ */
+function setRequestReferrerPolicyOnRedirect (request, actualResponse) {
+ // 1. Let policy be the result of executing § 8.1 Parse a referrer policy
+ // from a Referrer-Policy header on actualResponse.
+ const policy = parseReferrerPolicy(actualResponse)
+
+ // 2. If policy is not the empty string, then set request’s referrer policy to policy.
+ if (policy !== '') {
+ request.referrerPolicy = policy
+ }
+}
+
+// https://fetch.spec.whatwg.org/#cross-origin-resource-policy-check
+function crossOriginResourcePolicyCheck () {
+ // TODO
+ return 'allowed'
+}
+
+// https://fetch.spec.whatwg.org/#concept-cors-check
+function corsCheck () {
+ // TODO
+ return 'success'
+}
+
+// https://fetch.spec.whatwg.org/#concept-tao-check
+function TAOCheck () {
+ // TODO
+ return 'success'
+}
+
+function appendFetchMetadata (httpRequest) {
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-dest-header
+ // TODO
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-mode-header
+
+ // 1. Assert: r’s url is a potentially trustworthy URL.
+ // TODO
+
+ // 2. Let header be a Structured Header whose value is a token.
+ let header = null
+
+ // 3. Set header’s value to r’s mode.
+ header = httpRequest.mode
+
+ // 4. Set a structured field value `Sec-Fetch-Mode`/header in r’s header list.
+ httpRequest.headersList.set('sec-fetch-mode', header, true)
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-site-header
+ // TODO
+
+ // https://w3c.github.io/webappsec-fetch-metadata/#sec-fetch-user-header
+ // TODO
+}
+
+// https://fetch.spec.whatwg.org/#append-a-request-origin-header
+function appendRequestOriginHeader (request) {
+ // 1. Let serializedOrigin be the result of byte-serializing a request origin
+ // with request.
+ // TODO: implement "byte-serializing a request origin"
+ let serializedOrigin = request.origin
+
+ // - "'client' is changed to an origin during fetching."
+ // This doesn't happen in undici (in most cases) because undici, by default,
+ // has no concept of origin.
+ // - request.origin can also be set to request.client.origin (client being
+ // an environment settings object), which is undefined without using
+ // setGlobalOrigin.
+ if (serializedOrigin === 'client' || serializedOrigin === undefined) {
+ return
+ }
+
+ // 2. If request’s response tainting is "cors" or request’s mode is "websocket",
+ // then append (`Origin`, serializedOrigin) to request’s header list.
+ // 3. Otherwise, if request’s method is neither `GET` nor `HEAD`, then:
+ if (request.responseTainting === 'cors' || request.mode === 'websocket') {
+ request.headersList.append('origin', serializedOrigin, true)
+ } else if (request.method !== 'GET' && request.method !== 'HEAD') {
+ // 1. Switch on request’s referrer policy:
+ switch (request.referrerPolicy) {
+ case 'no-referrer':
+ // Set serializedOrigin to `null`.
+ serializedOrigin = null
+ break
+ case 'no-referrer-when-downgrade':
+ case 'strict-origin':
+ case 'strict-origin-when-cross-origin':
+ // If request’s origin is a tuple origin, its scheme is "https", and
+ // request’s current URL’s scheme is not "https", then set
+ // serializedOrigin to `null`.
+ if (request.origin && urlHasHttpsScheme(request.origin) && !urlHasHttpsScheme(requestCurrentURL(request))) {
+ serializedOrigin = null
+ }
+ break
+ case 'same-origin':
+ // If request’s origin is not same origin with request’s current URL’s
+ // origin, then set serializedOrigin to `null`.
+ if (!sameOrigin(request, requestCurrentURL(request))) {
+ serializedOrigin = null
+ }
+ break
+ default:
+ // Do nothing.
+ }
+
+ // 2. Append (`Origin`, serializedOrigin) to request’s header list.
+ request.headersList.append('origin', serializedOrigin, true)
+ }
+}
+
+// https://w3c.github.io/hr-time/#dfn-coarsen-time
+function coarsenTime (timestamp, crossOriginIsolatedCapability) {
+ // TODO
+ return timestamp
+}
+
+// https://fetch.spec.whatwg.org/#clamp-and-coarsen-connection-timing-info
+function clampAndCoarsenConnectionTimingInfo (connectionTimingInfo, defaultStartTime, crossOriginIsolatedCapability) {
+ if (!connectionTimingInfo?.startTime || connectionTimingInfo.startTime < defaultStartTime) {
+ return {
+ domainLookupStartTime: defaultStartTime,
+ domainLookupEndTime: defaultStartTime,
+ connectionStartTime: defaultStartTime,
+ connectionEndTime: defaultStartTime,
+ secureConnectionStartTime: defaultStartTime,
+ ALPNNegotiatedProtocol: connectionTimingInfo?.ALPNNegotiatedProtocol
+ }
+ }
+
+ return {
+ domainLookupStartTime: coarsenTime(connectionTimingInfo.domainLookupStartTime, crossOriginIsolatedCapability),
+ domainLookupEndTime: coarsenTime(connectionTimingInfo.domainLookupEndTime, crossOriginIsolatedCapability),
+ connectionStartTime: coarsenTime(connectionTimingInfo.connectionStartTime, crossOriginIsolatedCapability),
+ connectionEndTime: coarsenTime(connectionTimingInfo.connectionEndTime, crossOriginIsolatedCapability),
+ secureConnectionStartTime: coarsenTime(connectionTimingInfo.secureConnectionStartTime, crossOriginIsolatedCapability),
+ ALPNNegotiatedProtocol: connectionTimingInfo.ALPNNegotiatedProtocol
+ }
+}
+
+// https://w3c.github.io/hr-time/#dfn-coarsened-shared-current-time
+function coarsenedSharedCurrentTime (crossOriginIsolatedCapability) {
+ return coarsenTime(performance.now(), crossOriginIsolatedCapability)
+}
+
+// https://fetch.spec.whatwg.org/#create-an-opaque-timing-info
+function createOpaqueTimingInfo (timingInfo) {
+ return {
+ startTime: timingInfo.startTime ?? 0,
+ redirectStartTime: 0,
+ redirectEndTime: 0,
+ postRedirectStartTime: timingInfo.startTime ?? 0,
+ finalServiceWorkerStartTime: 0,
+ finalNetworkResponseStartTime: 0,
+ finalNetworkRequestStartTime: 0,
+ endTime: 0,
+ encodedBodySize: 0,
+ decodedBodySize: 0,
+ finalConnectionTimingInfo: null
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/origin.html#policy-container
+function makePolicyContainer () {
+ // Note: the fetch spec doesn't make use of embedder policy or CSP list
+ return {
+ referrerPolicy: 'strict-origin-when-cross-origin'
+ }
+}
+
+// https://html.spec.whatwg.org/multipage/origin.html#clone-a-policy-container
+function clonePolicyContainer (policyContainer) {
+ return {
+ referrerPolicy: policyContainer.referrerPolicy
+ }
+}
+
+/**
+ * Determine request’s Referrer
+ *
+ * @see https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer
+ */
+function determineRequestsReferrer (request) {
+ // Given a request request, we can determine the correct referrer information
+ // to send by examining its referrer policy as detailed in the following
+ // steps, which return either no referrer or a URL:
+
+ // 1. Let policy be request's referrer policy.
+ const policy = request.referrerPolicy
+
+ // Note: policy cannot (shouldn't) be null or an empty string.
+ assert(policy)
+
+ // 2. Let environment be request’s client.
+
+ let referrerSource = null
+
+ // 3. Switch on request’s referrer:
+
+ // "client"
+ if (request.referrer === 'client') {
+ // Note: node isn't a browser and doesn't implement document/iframes,
+ // so we bypass this step and replace it with our own.
+
+ const globalOrigin = getGlobalOrigin()
+
+ if (!globalOrigin || globalOrigin.origin === 'null') {
+ return 'no-referrer'
+ }
+
+ // Note: we need to clone it as it's mutated
+ referrerSource = new URL(globalOrigin)
+ // a URL
+ } else if (webidl.is.URL(request.referrer)) {
+ // Let referrerSource be request’s referrer.
+ referrerSource = request.referrer
+ }
+
+ // 4. Let request’s referrerURL be the result of stripping referrerSource for
+ // use as a referrer.
+ let referrerURL = stripURLForReferrer(referrerSource)
+
+ // 5. Let referrerOrigin be the result of stripping referrerSource for use as
+ // a referrer, with the origin-only flag set to true.
+ const referrerOrigin = stripURLForReferrer(referrerSource, true)
+
+ // 6. If the result of serializing referrerURL is a string whose length is
+ // greater than 4096, set referrerURL to referrerOrigin.
+ if (referrerURL.toString().length > 4096) {
+ referrerURL = referrerOrigin
+ }
+
+ // 7. The user agent MAY alter referrerURL or referrerOrigin at this point
+ // to enforce arbitrary policy considerations in the interests of minimizing
+ // data leakage. For example, the user agent could strip the URL down to an
+ // origin, modify its host, replace it with an empty string, etc.
+
+ // 8. Execute the switch statements corresponding to the value of policy:
+ switch (policy) {
+ case 'no-referrer':
+ // Return no referrer
+ return 'no-referrer'
+ case 'origin':
+ // Return referrerOrigin
+ if (referrerOrigin != null) {
+ return referrerOrigin
+ }
+ return stripURLForReferrer(referrerSource, true)
+ case 'unsafe-url':
+ // Return referrerURL.
+ return referrerURL
+ case 'strict-origin': {
+ const currentURL = requestCurrentURL(request)
+
+ // 1. If referrerURL is a potentially trustworthy URL and request’s
+ // current URL is not a potentially trustworthy URL, then return no
+ // referrer.
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
+ return 'no-referrer'
+ }
+ // 2. Return referrerOrigin
+ return referrerOrigin
+ }
+ case 'strict-origin-when-cross-origin': {
+ const currentURL = requestCurrentURL(request)
+
+ // 1. If the origin of referrerURL and the origin of request’s current
+ // URL are the same, then return referrerURL.
+ if (sameOrigin(referrerURL, currentURL)) {
+ return referrerURL
+ }
+
+ // 2. If referrerURL is a potentially trustworthy URL and request’s
+ // current URL is not a potentially trustworthy URL, then return no
+ // referrer.
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
+ return 'no-referrer'
+ }
+
+ // 3. Return referrerOrigin.
+ return referrerOrigin
+ }
+ case 'same-origin':
+ // 1. If the origin of referrerURL and the origin of request’s current
+ // URL are the same, then return referrerURL.
+ if (sameOrigin(request, referrerURL)) {
+ return referrerURL
+ }
+ // 2. Return no referrer.
+ return 'no-referrer'
+ case 'origin-when-cross-origin':
+ // 1. If the origin of referrerURL and the origin of request’s current
+ // URL are the same, then return referrerURL.
+ if (sameOrigin(request, referrerURL)) {
+ return referrerURL
+ }
+ // 2. Return referrerOrigin.
+ return referrerOrigin
+ case 'no-referrer-when-downgrade': {
+ const currentURL = requestCurrentURL(request)
+
+ // 1. If referrerURL is a potentially trustworthy URL and request’s
+ // current URL is not a potentially trustworthy URL, then return no
+ // referrer.
+ if (isURLPotentiallyTrustworthy(referrerURL) && !isURLPotentiallyTrustworthy(currentURL)) {
+ return 'no-referrer'
+ }
+ // 2. Return referrerURL.
+ return referrerURL
+ }
+ }
+}
+
+/**
+ * Certain portions of URLs must not be included when sending a URL as the
+ * value of a `Referer` header: a URLs fragment, username, and password
+ * components must be stripped from the URL before it’s sent out. This
+ * algorithm accepts a origin-only flag, which defaults to false. If set to
+ * true, the algorithm will additionally remove the URL’s path and query
+ * components, leaving only the scheme, host, and port.
+ *
+ * @see https://w3c.github.io/webappsec-referrer-policy/#strip-url
+ * @param {URL} url
+ * @param {boolean} [originOnly=false]
+ */
+function stripURLForReferrer (url, originOnly = false) {
+ // 1. Assert: url is a URL.
+ assert(webidl.is.URL(url))
+
+ // Note: Create a new URL instance to avoid mutating the original URL.
+ url = new URL(url)
+
+ // 2. If url’s scheme is a local scheme, then return no referrer.
+ if (urlIsLocal(url)) {
+ return 'no-referrer'
+ }
+
+ // 3. Set url’s username to the empty string.
+ url.username = ''
+
+ // 4. Set url’s password to the empty string.
+ url.password = ''
+
+ // 5. Set url’s fragment to null.
+ url.hash = ''
+
+ // 6. If the origin-only flag is true, then:
+ if (originOnly === true) {
+ // 1. Set url’s path to « the empty string ».
+ url.pathname = ''
+
+ // 2. Set url’s query to null.
+ url.search = ''
+ }
+
+ // 7. Return url.
+ return url
+}
+
+const isPotentialleTrustworthyIPv4 = RegExp.prototype.test
+ .bind(/^127\.(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)\.){2}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)$/)
+
+const isPotentiallyTrustworthyIPv6 = RegExp.prototype.test
+ .bind(/^(?:(?:0{1,4}:){7}|(?:0{1,4}:){1,6}:|::)0{0,3}1$/)
+
+/**
+ * Check if host matches one of the CIDR notations 127.0.0.0/8 or ::1/128.
+ *
+ * @param {string} origin
+ * @returns {boolean}
+ */
+function isOriginIPPotentiallyTrustworthy (origin) {
+ // IPv6
+ if (origin.includes(':')) {
+ // Remove brackets from IPv6 addresses
+ if (origin[0] === '[' && origin[origin.length - 1] === ']') {
+ origin = origin.slice(1, -1)
+ }
+ return isPotentiallyTrustworthyIPv6(origin)
+ }
+
+ // IPv4
+ return isPotentialleTrustworthyIPv4(origin)
+}
+
+/**
+ * A potentially trustworthy origin is one which a user agent can generally
+ * trust as delivering data securely.
+ *
+ * Return value `true` means `Potentially Trustworthy`.
+ * Return value `false` means `Not Trustworthy`.
+ *
+ * @see https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy
+ * @param {string} origin
+ * @returns {boolean}
+ */
+function isOriginPotentiallyTrustworthy (origin) {
+ // 1. If origin is an opaque origin, return "Not Trustworthy".
+ if (origin == null || origin === 'null') {
+ return false
+ }
+
+ // 2. Assert: origin is a tuple origin.
+ origin = new URL(origin)
+
+ // 3. If origin’s scheme is either "https" or "wss",
+ // return "Potentially Trustworthy".
+ if (origin.protocol === 'https:' || origin.protocol === 'wss:') {
+ return true
+ }
+
+ // 4. If origin’s host matches one of the CIDR notations 127.0.0.0/8 or
+ // ::1/128 [RFC4632], return "Potentially Trustworthy".
+ if (isOriginIPPotentiallyTrustworthy(origin.hostname)) {
+ return true
+ }
+
+ // 5. If the user agent conforms to the name resolution rules in
+ // [let-localhost-be-localhost] and one of the following is true:
+
+ // origin’s host is "localhost" or "localhost."
+ if (origin.hostname === 'localhost' || origin.hostname === 'localhost.') {
+ return true
+ }
+
+ // origin’s host ends with ".localhost" or ".localhost."
+ if (origin.hostname.endsWith('.localhost') || origin.hostname.endsWith('.localhost.')) {
+ return true
+ }
+
+ // 6. If origin’s scheme is "file", return "Potentially Trustworthy".
+ if (origin.protocol === 'file:') {
+ return true
+ }
+
+ // 7. If origin’s scheme component is one which the user agent considers to
+ // be authenticated, return "Potentially Trustworthy".
+
+ // 8. If origin has been configured as a trustworthy origin, return
+ // "Potentially Trustworthy".
+
+ // 9. Return "Not Trustworthy".
+ return false
+}
+
+/**
+ * A potentially trustworthy URL is one which either inherits context from its
+ * creator (about:blank, about:srcdoc, data) or one whose origin is a
+ * potentially trustworthy origin.
+ *
+ * Return value `true` means `Potentially Trustworthy`.
+ * Return value `false` means `Not Trustworthy`.
+ *
+ * @see https://www.w3.org/TR/secure-contexts/#is-url-trustworthy
+ * @param {URL} url
+ * @returns {boolean}
+ */
+function isURLPotentiallyTrustworthy (url) {
+ // Given a URL record (url), the following algorithm returns "Potentially
+ // Trustworthy" or "Not Trustworthy" as appropriate:
+ if (!webidl.is.URL(url)) {
+ return false
+ }
+
+ // 1. If url is "about:blank" or "about:srcdoc",
+ // return "Potentially Trustworthy".
+ if (url.href === 'about:blank' || url.href === 'about:srcdoc') {
+ return true
+ }
+
+ // 2. If url’s scheme is "data", return "Potentially Trustworthy".
+ if (url.protocol === 'data:') return true
+
+ // Note: The origin of blob: URLs is the origin of the context in which they
+ // were created. Therefore, blobs created in a trustworthy origin will
+ // themselves be potentially trustworthy.
+ if (url.protocol === 'blob:') return true
+
+ // 3. Return the result of executing § 3.1 Is origin potentially trustworthy?
+ // on url’s origin.
+ return isOriginPotentiallyTrustworthy(url.origin)
+}
+
+// https://w3c.github.io/webappsec-upgrade-insecure-requests/#upgrade-request
+function tryUpgradeRequestToAPotentiallyTrustworthyURL (request) {
+ // TODO
+}
+
+/**
+ * @link {https://html.spec.whatwg.org/multipage/origin.html#same-origin}
+ * @param {URL} A
+ * @param {URL} B
+ */
+function sameOrigin (A, B) {
+ // 1. If A and B are the same opaque origin, then return true.
+ if (A.origin === B.origin && A.origin === 'null') {
+ return true
+ }
+
+ // 2. If A and B are both tuple origins and their schemes,
+ // hosts, and port are identical, then return true.
+ if (A.protocol === B.protocol && A.hostname === B.hostname && A.port === B.port) {
+ return true
+ }
+
+ // 3. Return false.
+ return false
+}
+
+function isAborted (fetchParams) {
+ return fetchParams.controller.state === 'aborted'
+}
+
+function isCancelled (fetchParams) {
+ return fetchParams.controller.state === 'aborted' ||
+ fetchParams.controller.state === 'terminated'
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-method-normalize
+ * @param {string} method
+ */
+function normalizeMethod (method) {
+ return normalizedMethodRecordsBase[method.toLowerCase()] ?? method
+}
+
+// https://tc39.es/ecma262/#sec-%25iteratorprototype%25-object
+const esIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf([][Symbol.iterator]()))
+
+/**
+ * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ * @param {string} name name of the instance
+ * @param {((target: any) => any)} kInternalIterator
+ * @param {string | number} [keyIndex]
+ * @param {string | number} [valueIndex]
+ */
+function createIterator (name, kInternalIterator, keyIndex = 0, valueIndex = 1) {
+ class FastIterableIterator {
+ /** @type {any} */
+ #target
+ /** @type {'key' | 'value' | 'key+value'} */
+ #kind
+ /** @type {number} */
+ #index
+
+ /**
+ * @see https://webidl.spec.whatwg.org/#dfn-default-iterator-object
+ * @param {unknown} target
+ * @param {'key' | 'value' | 'key+value'} kind
+ */
+ constructor (target, kind) {
+ this.#target = target
+ this.#kind = kind
+ this.#index = 0
+ }
+
+ next () {
+ // 1. Let interface be the interface for which the iterator prototype object exists.
+ // 2. Let thisValue be the this value.
+ // 3. Let object be ? ToObject(thisValue).
+ // 4. If object is a platform object, then perform a security
+ // check, passing:
+ // 5. If object is not a default iterator object for interface,
+ // then throw a TypeError.
+ if (typeof this !== 'object' || this === null || !(#target in this)) {
+ throw new TypeError(
+ `'next' called on an object that does not implement interface ${name} Iterator.`
+ )
+ }
+
+ // 6. Let index be object’s index.
+ // 7. Let kind be object’s kind.
+ // 8. Let values be object’s target's value pairs to iterate over.
+ const index = this.#index
+ const values = kInternalIterator(this.#target)
+
+ // 9. Let len be the length of values.
+ const len = values.length
+
+ // 10. If index is greater than or equal to len, then return
+ // CreateIterResultObject(undefined, true).
+ if (index >= len) {
+ return {
+ value: undefined,
+ done: true
+ }
+ }
+
+ // 11. Let pair be the entry in values at index index.
+ const { [keyIndex]: key, [valueIndex]: value } = values[index]
+
+ // 12. Set object’s index to index + 1.
+ this.#index = index + 1
+
+ // 13. Return the iterator result for pair and kind.
+
+ // https://webidl.spec.whatwg.org/#iterator-result
+
+ // 1. Let result be a value determined by the value of kind:
+ let result
+ switch (this.#kind) {
+ case 'key':
+ // 1. Let idlKey be pair’s key.
+ // 2. Let key be the result of converting idlKey to an
+ // ECMAScript value.
+ // 3. result is key.
+ result = key
+ break
+ case 'value':
+ // 1. Let idlValue be pair’s value.
+ // 2. Let value be the result of converting idlValue to
+ // an ECMAScript value.
+ // 3. result is value.
+ result = value
+ break
+ case 'key+value':
+ // 1. Let idlKey be pair’s key.
+ // 2. Let idlValue be pair’s value.
+ // 3. Let key be the result of converting idlKey to an
+ // ECMAScript value.
+ // 4. Let value be the result of converting idlValue to
+ // an ECMAScript value.
+ // 5. Let array be ! ArrayCreate(2).
+ // 6. Call ! CreateDataProperty(array, "0", key).
+ // 7. Call ! CreateDataProperty(array, "1", value).
+ // 8. result is array.
+ result = [key, value]
+ break
+ }
+
+ // 2. Return CreateIterResultObject(result, false).
+ return {
+ value: result,
+ done: false
+ }
+ }
+ }
+
+ // https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ // @ts-ignore
+ delete FastIterableIterator.prototype.constructor
+
+ Object.setPrototypeOf(FastIterableIterator.prototype, esIteratorPrototype)
+
+ Object.defineProperties(FastIterableIterator.prototype, {
+ [Symbol.toStringTag]: {
+ writable: false,
+ enumerable: false,
+ configurable: true,
+ value: `${name} Iterator`
+ },
+ next: { writable: true, enumerable: true, configurable: true }
+ })
+
+ /**
+ * @param {unknown} target
+ * @param {'key' | 'value' | 'key+value'} kind
+ * @returns {IterableIterator<any>}
+ */
+ return function (target, kind) {
+ return new FastIterableIterator(target, kind)
+ }
+}
+
+/**
+ * @see https://webidl.spec.whatwg.org/#dfn-iterator-prototype-object
+ * @param {string} name name of the instance
+ * @param {any} object class
+ * @param {(target: any) => any} kInternalIterator
+ * @param {string | number} [keyIndex]
+ * @param {string | number} [valueIndex]
+ */
+function iteratorMixin (name, object, kInternalIterator, keyIndex = 0, valueIndex = 1) {
+ const makeIterator = createIterator(name, kInternalIterator, keyIndex, valueIndex)
+
+ const properties = {
+ keys: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function keys () {
+ webidl.brandCheck(this, object)
+ return makeIterator(this, 'key')
+ }
+ },
+ values: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function values () {
+ webidl.brandCheck(this, object)
+ return makeIterator(this, 'value')
+ }
+ },
+ entries: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function entries () {
+ webidl.brandCheck(this, object)
+ return makeIterator(this, 'key+value')
+ }
+ },
+ forEach: {
+ writable: true,
+ enumerable: true,
+ configurable: true,
+ value: function forEach (callbackfn, thisArg = globalThis) {
+ webidl.brandCheck(this, object)
+ webidl.argumentLengthCheck(arguments, 1, `${name}.forEach`)
+ if (typeof callbackfn !== 'function') {
+ throw new TypeError(
+ `Failed to execute 'forEach' on '${name}': parameter 1 is not of type 'Function'.`
+ )
+ }
+ for (const { 0: key, 1: value } of makeIterator(this, 'key+value')) {
+ callbackfn.call(thisArg, value, key, this)
+ }
+ }
+ }
+ }
+
+ return Object.defineProperties(object.prototype, {
+ ...properties,
+ [Symbol.iterator]: {
+ writable: true,
+ enumerable: false,
+ configurable: true,
+ value: properties.entries.value
+ }
+ })
+}
+
+/**
+ * @param {import('./body').ExtractBodyResult} body
+ * @param {(bytes: Uint8Array) => void} processBody
+ * @param {(error: Error) => void} processBodyError
+ * @returns {void}
+ *
+ * @see https://fetch.spec.whatwg.org/#body-fully-read
+ */
+function fullyReadBody (body, processBody, processBodyError) {
+ // 1. If taskDestination is null, then set taskDestination to
+ // the result of starting a new parallel queue.
+
+ // 2. Let successSteps given a byte sequence bytes be to queue a
+ // fetch task to run processBody given bytes, with taskDestination.
+ const successSteps = processBody
+
+ // 3. Let errorSteps be to queue a fetch task to run processBodyError,
+ // with taskDestination.
+ const errorSteps = processBodyError
+
+ try {
+ // 4. Let reader be the result of getting a reader for body’s stream.
+ // If that threw an exception, then run errorSteps with that
+ // exception and return.
+ const reader = body.stream.getReader()
+
+ // 5. Read all bytes from reader, given successSteps and errorSteps.
+ readAllBytes(reader, successSteps, errorSteps)
+ } catch (e) {
+ errorSteps(e)
+ }
+}
+
+/**
+ * @param {ReadableStreamController<Uint8Array>} controller
+ */
+function readableStreamClose (controller) {
+ try {
+ controller.close()
+ controller.byobRequest?.respond(0)
+ } catch (err) {
+ // TODO: add comment explaining why this error occurs.
+ if (!err.message.includes('Controller is already closed') && !err.message.includes('ReadableStream is already closed')) {
+ throw err
+ }
+ }
+}
+
+/**
+ * @see https://streams.spec.whatwg.org/#readablestreamdefaultreader-read-all-bytes
+ * @see https://streams.spec.whatwg.org/#read-loop
+ * @param {ReadableStream<Uint8Array<ArrayBuffer>>} reader
+ * @param {(bytes: Uint8Array) => void} successSteps
+ * @param {(error: Error) => void} failureSteps
+ * @returns {Promise<void>}
+ */
+async function readAllBytes (reader, successSteps, failureSteps) {
+ try {
+ const bytes = []
+ let byteLength = 0
+
+ do {
+ const { done, value: chunk } = await reader.read()
+
+ if (done) {
+ // 1. Call successSteps with bytes.
+ successSteps(Buffer.concat(bytes, byteLength))
+ return
+ }
+
+ // 1. If chunk is not a Uint8Array object, call failureSteps
+ // with a TypeError and abort these steps.
+ if (!isUint8Array(chunk)) {
+ failureSteps(new TypeError('Received non-Uint8Array chunk'))
+ return
+ }
+
+ // 2. Append the bytes represented by chunk to bytes.
+ bytes.push(chunk)
+ byteLength += chunk.length
+
+ // 3. Read-loop given reader, bytes, successSteps, and failureSteps.
+ } while (true)
+ } catch (e) {
+ // 1. Call failureSteps with e.
+ failureSteps(e)
+ }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#is-local
+ * @param {URL} url
+ * @returns {boolean}
+ */
+function urlIsLocal (url) {
+ assert('protocol' in url) // ensure it's a url object
+
+ const protocol = url.protocol
+
+ // A URL is local if its scheme is a local scheme.
+ // A local scheme is "about", "blob", or "data".
+ return protocol === 'about:' || protocol === 'blob:' || protocol === 'data:'
+}
+
+/**
+ * @param {string|URL} url
+ * @returns {boolean}
+ */
+function urlHasHttpsScheme (url) {
+ return (
+ (
+ typeof url === 'string' &&
+ url[5] === ':' &&
+ url[0] === 'h' &&
+ url[1] === 't' &&
+ url[2] === 't' &&
+ url[3] === 'p' &&
+ url[4] === 's'
+ ) ||
+ url.protocol === 'https:'
+ )
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#http-scheme
+ * @param {URL} url
+ */
+function urlIsHttpHttpsScheme (url) {
+ assert('protocol' in url) // ensure it's a url object
+
+ const protocol = url.protocol
+
+ return protocol === 'http:' || protocol === 'https:'
+}
+
+/**
+ * @typedef {Object} RangeHeaderValue
+ * @property {number|null} rangeStartValue
+ * @property {number|null} rangeEndValue
+ */
+
+/**
+ * @see https://fetch.spec.whatwg.org/#simple-range-header-value
+ * @param {string} value
+ * @param {boolean} allowWhitespace
+ * @return {RangeHeaderValue|'failure'}
+ */
+function simpleRangeHeaderValue (value, allowWhitespace) {
+ // 1. Let data be the isomorphic decoding of value.
+ // Note: isomorphic decoding takes a sequence of bytes (ie. a Uint8Array) and turns it into a string,
+ // nothing more. We obviously don't need to do that if value is a string already.
+ const data = value
+
+ // 2. If data does not start with "bytes", then return failure.
+ if (!data.startsWith('bytes')) {
+ return 'failure'
+ }
+
+ // 3. Let position be a position variable for data, initially pointing at the 5th code point of data.
+ const position = { position: 5 }
+
+ // 4. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space,
+ // from data given position.
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 5. If the code point at position within data is not U+003D (=), then return failure.
+ if (data.charCodeAt(position.position) !== 0x3D) {
+ return 'failure'
+ }
+
+ // 6. Advance position by 1.
+ position.position++
+
+ // 7. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space, from
+ // data given position.
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 8. Let rangeStart be the result of collecting a sequence of code points that are ASCII digits,
+ // from data given position.
+ const rangeStart = collectASequenceOfCodePoints(
+ (char) => {
+ const code = char.charCodeAt(0)
+
+ return code >= 0x30 && code <= 0x39
+ },
+ data,
+ position
+ )
+
+ // 9. Let rangeStartValue be rangeStart, interpreted as decimal number, if rangeStart is not the
+ // empty string; otherwise null.
+ const rangeStartValue = rangeStart.length ? Number(rangeStart) : null
+
+ // 10. If allowWhitespace is true, collect a sequence of code points that are HTTP tab or space,
+ // from data given position.
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 11. If the code point at position within data is not U+002D (-), then return failure.
+ if (data.charCodeAt(position.position) !== 0x2D) {
+ return 'failure'
+ }
+
+ // 12. Advance position by 1.
+ position.position++
+
+ // 13. If allowWhitespace is true, collect a sequence of code points that are HTTP tab
+ // or space, from data given position.
+ // Note from Khafra: its the same step as in #8 again lol
+ if (allowWhitespace) {
+ collectASequenceOfCodePoints(
+ (char) => char === '\t' || char === ' ',
+ data,
+ position
+ )
+ }
+
+ // 14. Let rangeEnd be the result of collecting a sequence of code points that are
+ // ASCII digits, from data given position.
+ // Note from Khafra: you wouldn't guess it, but this is also the same step as #8
+ const rangeEnd = collectASequenceOfCodePoints(
+ (char) => {
+ const code = char.charCodeAt(0)
+
+ return code >= 0x30 && code <= 0x39
+ },
+ data,
+ position
+ )
+
+ // 15. Let rangeEndValue be rangeEnd, interpreted as decimal number, if rangeEnd
+ // is not the empty string; otherwise null.
+ // Note from Khafra: THE SAME STEP, AGAIN!!!
+ // Note: why interpret as a decimal if we only collect ascii digits?
+ const rangeEndValue = rangeEnd.length ? Number(rangeEnd) : null
+
+ // 16. If position is not past the end of data, then return failure.
+ if (position.position < data.length) {
+ return 'failure'
+ }
+
+ // 17. If rangeEndValue and rangeStartValue are null, then return failure.
+ if (rangeEndValue === null && rangeStartValue === null) {
+ return 'failure'
+ }
+
+ // 18. If rangeStartValue and rangeEndValue are numbers, and rangeStartValue is
+ // greater than rangeEndValue, then return failure.
+ // Note: ... when can they not be numbers?
+ if (rangeStartValue > rangeEndValue) {
+ return 'failure'
+ }
+
+ // 19. Return (rangeStartValue, rangeEndValue).
+ return { rangeStartValue, rangeEndValue }
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#build-a-content-range
+ * @param {number} rangeStart
+ * @param {number} rangeEnd
+ * @param {number} fullLength
+ */
+function buildContentRange (rangeStart, rangeEnd, fullLength) {
+ // 1. Let contentRange be `bytes `.
+ let contentRange = 'bytes '
+
+ // 2. Append rangeStart, serialized and isomorphic encoded, to contentRange.
+ contentRange += isomorphicEncode(`${rangeStart}`)
+
+ // 3. Append 0x2D (-) to contentRange.
+ contentRange += '-'
+
+ // 4. Append rangeEnd, serialized and isomorphic encoded to contentRange.
+ contentRange += isomorphicEncode(`${rangeEnd}`)
+
+ // 5. Append 0x2F (/) to contentRange.
+ contentRange += '/'
+
+ // 6. Append fullLength, serialized and isomorphic encoded to contentRange.
+ contentRange += isomorphicEncode(`${fullLength}`)
+
+ // 7. Return contentRange.
+ return contentRange
+}
+
+// A Stream, which pipes the response to zlib.createInflate() or
+// zlib.createInflateRaw() depending on the first byte of the Buffer.
+// If the lower byte of the first byte is 0x08, then the stream is
+// interpreted as a zlib stream, otherwise it's interpreted as a
+// raw deflate stream.
+class InflateStream extends Transform {
+ #zlibOptions
+
+ /** @param {zlib.ZlibOptions} [zlibOptions] */
+ constructor (zlibOptions) {
+ super()
+ this.#zlibOptions = zlibOptions
+ }
+
+ _transform (chunk, encoding, callback) {
+ if (!this._inflateStream) {
+ if (chunk.length === 0) {
+ callback()
+ return
+ }
+ this._inflateStream = (chunk[0] & 0x0F) === 0x08
+ ? zlib.createInflate(this.#zlibOptions)
+ : zlib.createInflateRaw(this.#zlibOptions)
+
+ this._inflateStream.on('data', this.push.bind(this))
+ this._inflateStream.on('end', () => this.push(null))
+ this._inflateStream.on('error', (err) => this.destroy(err))
+ }
+
+ this._inflateStream.write(chunk, encoding, callback)
+ }
+
+ _final (callback) {
+ if (this._inflateStream) {
+ this._inflateStream.end()
+ this._inflateStream = null
+ }
+ callback()
+ }
+}
+
+/**
+ * @param {zlib.ZlibOptions} [zlibOptions]
+ * @returns {InflateStream}
+ */
+function createInflate (zlibOptions) {
+ return new InflateStream(zlibOptions)
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-header-extract-mime-type
+ * @param {import('./headers').HeadersList} headers
+ */
+function extractMimeType (headers) {
+ // 1. Let charset be null.
+ let charset = null
+
+ // 2. Let essence be null.
+ let essence = null
+
+ // 3. Let mimeType be null.
+ let mimeType = null
+
+ // 4. Let values be the result of getting, decoding, and splitting `Content-Type` from headers.
+ const values = getDecodeSplit('content-type', headers)
+
+ // 5. If values is null, then return failure.
+ if (values === null) {
+ return 'failure'
+ }
+
+ // 6. For each value of values:
+ for (const value of values) {
+ // 6.1. Let temporaryMimeType be the result of parsing value.
+ const temporaryMimeType = parseMIMEType(value)
+
+ // 6.2. If temporaryMimeType is failure or its essence is "*/*", then continue.
+ if (temporaryMimeType === 'failure' || temporaryMimeType.essence === '*/*') {
+ continue
+ }
+
+ // 6.3. Set mimeType to temporaryMimeType.
+ mimeType = temporaryMimeType
+
+ // 6.4. If mimeType’s essence is not essence, then:
+ if (mimeType.essence !== essence) {
+ // 6.4.1. Set charset to null.
+ charset = null
+
+ // 6.4.2. If mimeType’s parameters["charset"] exists, then set charset to
+ // mimeType’s parameters["charset"].
+ if (mimeType.parameters.has('charset')) {
+ charset = mimeType.parameters.get('charset')
+ }
+
+ // 6.4.3. Set essence to mimeType’s essence.
+ essence = mimeType.essence
+ } else if (!mimeType.parameters.has('charset') && charset !== null) {
+ // 6.5. Otherwise, if mimeType’s parameters["charset"] does not exist, and
+ // charset is non-null, set mimeType’s parameters["charset"] to charset.
+ mimeType.parameters.set('charset', charset)
+ }
+ }
+
+ // 7. If mimeType is null, then return failure.
+ if (mimeType == null) {
+ return 'failure'
+ }
+
+ // 8. Return mimeType.
+ return mimeType
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#header-value-get-decode-and-split
+ * @param {string|null} value
+ */
+function gettingDecodingSplitting (value) {
+ // 1. Let input be the result of isomorphic decoding value.
+ const input = value
+
+ // 2. Let position be a position variable for input, initially pointing at the start of input.
+ const position = { position: 0 }
+
+ // 3. Let values be a list of strings, initially empty.
+ const values = []
+
+ // 4. Let temporaryValue be the empty string.
+ let temporaryValue = ''
+
+ // 5. While position is not past the end of input:
+ while (position.position < input.length) {
+ // 5.1. Append the result of collecting a sequence of code points that are not U+0022 (")
+ // or U+002C (,) from input, given position, to temporaryValue.
+ temporaryValue += collectASequenceOfCodePoints(
+ (char) => char !== '"' && char !== ',',
+ input,
+ position
+ )
+
+ // 5.2. If position is not past the end of input, then:
+ if (position.position < input.length) {
+ // 5.2.1. If the code point at position within input is U+0022 ("), then:
+ if (input.charCodeAt(position.position) === 0x22) {
+ // 5.2.1.1. Append the result of collecting an HTTP quoted string from input, given position, to temporaryValue.
+ temporaryValue += collectAnHTTPQuotedString(
+ input,
+ position
+ )
+
+ // 5.2.1.2. If position is not past the end of input, then continue.
+ if (position.position < input.length) {
+ continue
+ }
+ } else {
+ // 5.2.2. Otherwise:
+
+ // 5.2.2.1. Assert: the code point at position within input is U+002C (,).
+ assert(input.charCodeAt(position.position) === 0x2C)
+
+ // 5.2.2.2. Advance position by 1.
+ position.position++
+ }
+ }
+
+ // 5.3. Remove all HTTP tab or space from the start and end of temporaryValue.
+ temporaryValue = removeChars(temporaryValue, true, true, (char) => char === 0x9 || char === 0x20)
+
+ // 5.4. Append temporaryValue to values.
+ values.push(temporaryValue)
+
+ // 5.6. Set temporaryValue to the empty string.
+ temporaryValue = ''
+ }
+
+ // 6. Return values.
+ return values
+}
+
+/**
+ * @see https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split
+ * @param {string} name lowercase header name
+ * @param {import('./headers').HeadersList} list
+ */
+function getDecodeSplit (name, list) {
+ // 1. Let value be the result of getting name from list.
+ const value = list.get(name, true)
+
+ // 2. If value is null, then return null.
+ if (value === null) {
+ return null
+ }
+
+ // 3. Return the result of getting, decoding, and splitting value.
+ return gettingDecodingSplitting(value)
+}
+
+function hasAuthenticationEntry (request) {
+ return false
+}
+
+/**
+ * @see https://url.spec.whatwg.org/#include-credentials
+ * @param {URL} url
+ */
+function includesCredentials (url) {
+ // A URL includes credentials if its username or password is not the empty string.
+ return !!(url.username || url.password)
+}
+
+/**
+ * @see https://html.spec.whatwg.org/multipage/document-sequences.html#traversable-navigable
+ * @param {object|string} navigable
+ */
+function isTraversableNavigable (navigable) {
+ // TODO
+ return true
+}
+
+class EnvironmentSettingsObjectBase {
+ get baseUrl () {
+ return getGlobalOrigin()
+ }
+
+ get origin () {
+ return this.baseUrl?.origin
+ }
+
+ policyContainer = makePolicyContainer()
+}
+
+class EnvironmentSettingsObject {
+ settingsObject = new EnvironmentSettingsObjectBase()
+}
+
+const environmentSettingsObject = new EnvironmentSettingsObject()
+
+module.exports = {
+ isAborted,
+ isCancelled,
+ isValidEncodedURL,
+ ReadableStreamFrom,
+ tryUpgradeRequestToAPotentiallyTrustworthyURL,
+ clampAndCoarsenConnectionTimingInfo,
+ coarsenedSharedCurrentTime,
+ determineRequestsReferrer,
+ makePolicyContainer,
+ clonePolicyContainer,
+ appendFetchMetadata,
+ appendRequestOriginHeader,
+ TAOCheck,
+ corsCheck,
+ crossOriginResourcePolicyCheck,
+ createOpaqueTimingInfo,
+ setRequestReferrerPolicyOnRedirect,
+ isValidHTTPToken,
+ requestBadPort,
+ requestCurrentURL,
+ responseURL,
+ responseLocationURL,
+ isURLPotentiallyTrustworthy,
+ isValidReasonPhrase,
+ sameOrigin,
+ normalizeMethod,
+ iteratorMixin,
+ createIterator,
+ isValidHeaderName,
+ isValidHeaderValue,
+ isErrorLike,
+ fullyReadBody,
+ readableStreamClose,
+ urlIsLocal,
+ urlHasHttpsScheme,
+ urlIsHttpHttpsScheme,
+ readAllBytes,
+ simpleRangeHeaderValue,
+ buildContentRange,
+ createInflate,
+ extractMimeType,
+ getDecodeSplit,
+ environmentSettingsObject,
+ isOriginIPPotentiallyTrustworthy,
+ hasAuthenticationEntry,
+ includesCredentials,
+ isTraversableNavigable
+}