diff options
Diffstat (limited to 'ext/fetch')
-rw-r--r-- | ext/fetch/01_fetch_util.js | 22 | ||||
-rw-r--r-- | ext/fetch/20_headers.js | 808 | ||||
-rw-r--r-- | ext/fetch/21_formdata.js | 888 | ||||
-rw-r--r-- | ext/fetch/22_body.js | 820 | ||||
-rw-r--r-- | ext/fetch/22_http_client.js | 54 | ||||
-rw-r--r-- | ext/fetch/23_request.js | 1161 | ||||
-rw-r--r-- | ext/fetch/23_response.js | 918 | ||||
-rw-r--r-- | ext/fetch/26_fetch.js | 1033 | ||||
-rw-r--r-- | ext/fetch/internal.d.ts | 188 | ||||
-rw-r--r-- | ext/fetch/lib.rs | 3 |
10 files changed, 2932 insertions, 2963 deletions
diff --git a/ext/fetch/01_fetch_util.js b/ext/fetch/01_fetch_util.js deleted file mode 100644 index 3ed554ecb..000000000 --- a/ext/fetch/01_fetch_util.js +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -"use strict"; - -((window) => { - const { TypeError } = window.__bootstrap.primordials; - function requiredArguments( - name, - length, - required, - ) { - if (length < required) { - const errMsg = `${name} requires at least ${required} argument${ - required === 1 ? "" : "s" - }, but only ${length} present`; - throw new TypeError(errMsg); - } - } - - window.__bootstrap.fetchUtil = { - requiredArguments, - }; -})(this); diff --git a/ext/fetch/20_headers.js b/ext/fetch/20_headers.js index 54e635522..9790bb69f 100644 --- a/ext/fetch/20_headers.js +++ b/ext/fetch/20_headers.js @@ -8,472 +8,470 @@ /// <reference path="../web/06_streams_types.d.ts" /> /// <reference path="./lib.deno_fetch.d.ts" /> /// <reference lib="esnext" /> -"use strict"; - -((window) => { - const webidl = window.__bootstrap.webidl; - const { - HTTP_TAB_OR_SPACE_PREFIX_RE, - HTTP_TAB_OR_SPACE_SUFFIX_RE, - HTTP_TOKEN_CODE_POINT_RE, - byteLowerCase, - collectSequenceOfCodepoints, - collectHttpQuotedString, - httpTrim, - } = window.__bootstrap.infra; - const { - ArrayIsArray, - ArrayPrototypeMap, - ArrayPrototypePush, - ArrayPrototypeSort, - ArrayPrototypeJoin, - ArrayPrototypeSplice, - ArrayPrototypeFilter, - ObjectPrototypeHasOwnProperty, - ObjectEntries, - RegExpPrototypeTest, - SafeArrayIterator, - Symbol, - SymbolFor, - SymbolIterator, - StringPrototypeReplaceAll, - TypeError, - } = window.__bootstrap.primordials; - - const _headerList = Symbol("header list"); - const _iterableHeaders = Symbol("iterable headers"); - const _guard = Symbol("guard"); - - /** - * @typedef Header - * @type {[string, string]} - */ - /** - * @typedef HeaderList - * @type {Header[]} - */ +import * as webidl from "internal:ext/webidl/00_webidl.js"; +import { + byteLowerCase, + collectHttpQuotedString, + collectSequenceOfCodepoints, + HTTP_TAB_OR_SPACE_PREFIX_RE, + HTTP_TAB_OR_SPACE_SUFFIX_RE, + HTTP_TOKEN_CODE_POINT_RE, + httpTrim, +} from "internal:ext/web/00_infra.js"; +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayIsArray, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeSort, + ArrayPrototypeJoin, + ArrayPrototypeSplice, + ArrayPrototypeFilter, + ObjectPrototypeHasOwnProperty, + ObjectEntries, + RegExpPrototypeTest, + SafeArrayIterator, + Symbol, + SymbolFor, + SymbolIterator, + StringPrototypeReplaceAll, + TypeError, +} = primordials; + +const _headerList = Symbol("header list"); +const _iterableHeaders = Symbol("iterable headers"); +const _guard = Symbol("guard"); + +/** + * @typedef Header + * @type {[string, string]} + */ + +/** + * @typedef HeaderList + * @type {Header[]} + */ + +/** + * @param {string} potentialValue + * @returns {string} + */ +function normalizeHeaderValue(potentialValue) { + return httpTrim(potentialValue); +} + +/** + * @param {Headers} headers + * @param {HeadersInit} object + */ +function fillHeaders(headers, object) { + if (ArrayIsArray(object)) { + for (let i = 0; i < object.length; ++i) { + const header = object[i]; + if (header.length !== 2) { + throw new TypeError( + `Invalid header. Length must be 2, but is ${header.length}`, + ); + } + appendHeader(headers, header[0], header[1]); + } + } else { + for (const key in object) { + if (!ObjectPrototypeHasOwnProperty(object, key)) { + continue; + } + appendHeader(headers, key, object[key]); + } + } +} + +// Regex matching illegal chars in a header value +// deno-lint-ignore no-control-regex +const ILLEGAL_VALUE_CHARS = /[\x00\x0A\x0D]/; + +/** + * https://fetch.spec.whatwg.org/#concept-headers-append + * @param {Headers} headers + * @param {string} name + * @param {string} value + */ +function appendHeader(headers, name, value) { + // 1. + value = normalizeHeaderValue(value); + + // 2. + if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { + throw new TypeError("Header name is not valid."); + } + if (RegExpPrototypeTest(ILLEGAL_VALUE_CHARS, value)) { + throw new TypeError("Header value is not valid."); + } - /** - * @param {string} potentialValue - * @returns {string} - */ - function normalizeHeaderValue(potentialValue) { - return httpTrim(potentialValue); + // 3. + if (headers[_guard] == "immutable") { + throw new TypeError("Headers are immutable."); } - /** - * @param {Headers} headers - * @param {HeadersInit} object - */ - function fillHeaders(headers, object) { - if (ArrayIsArray(object)) { - for (let i = 0; i < object.length; ++i) { - const header = object[i]; - if (header.length !== 2) { - throw new TypeError( - `Invalid header. Length must be 2, but is ${header.length}`, - ); + // 7. + const list = headers[_headerList]; + const lowercaseName = byteLowerCase(name); + for (let i = 0; i < list.length; i++) { + if (byteLowerCase(list[i][0]) === lowercaseName) { + name = list[i][0]; + break; + } + } + ArrayPrototypePush(list, [name, value]); +} + +/** + * https://fetch.spec.whatwg.org/#concept-header-list-get + * @param {HeaderList} list + * @param {string} name + */ +function getHeader(list, name) { + const lowercaseName = byteLowerCase(name); + const entries = ArrayPrototypeMap( + ArrayPrototypeFilter( + list, + (entry) => byteLowerCase(entry[0]) === lowercaseName, + ), + (entry) => entry[1], + ); + if (entries.length === 0) { + return null; + } else { + return ArrayPrototypeJoin(entries, "\x2C\x20"); + } +} + +/** + * https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split + * @param {HeaderList} list + * @param {string} name + * @returns {string[] | null} + */ +function getDecodeSplitHeader(list, name) { + const initialValue = getHeader(list, name); + if (initialValue === null) return null; + const input = initialValue; + let position = 0; + const values = []; + let value = ""; + while (position < initialValue.length) { + // 7.1. collect up to " or , + const res = collectSequenceOfCodepoints( + initialValue, + position, + (c) => c !== "\u0022" && c !== "\u002C", + ); + value += res.result; + position = res.position; + + if (position < initialValue.length) { + if (input[position] === "\u0022") { + const res = collectHttpQuotedString(input, position, false); + value += res.result; + position = res.position; + if (position < initialValue.length) { + continue; } - appendHeader(headers, header[0], header[1]); + } else { + if (input[position] !== "\u002C") throw new TypeError("Unreachable"); + position += 1; } - } else { - for (const key in object) { - if (!ObjectPrototypeHasOwnProperty(object, key)) { - continue; + } + + value = StringPrototypeReplaceAll(value, HTTP_TAB_OR_SPACE_PREFIX_RE, ""); + value = StringPrototypeReplaceAll(value, HTTP_TAB_OR_SPACE_SUFFIX_RE, ""); + + ArrayPrototypePush(values, value); + value = ""; + } + return values; +} + +class Headers { + /** @type {HeaderList} */ + [_headerList] = []; + /** @type {"immutable" | "request" | "request-no-cors" | "response" | "none"} */ + [_guard]; + + get [_iterableHeaders]() { + const list = this[_headerList]; + + // The order of steps are not similar to the ones suggested by the + // spec but produce the same result. + const headers = {}; + const cookies = []; + for (let i = 0; i < list.length; ++i) { + const entry = list[i]; + const name = byteLowerCase(entry[0]); + const value = entry[1]; + if (value === null) throw new TypeError("Unreachable"); + // The following if statement is not spec compliant. + // `set-cookie` is the only header that can not be concatenated, + // so must be given to the user as multiple headers. + // The else block of the if statement is spec compliant again. + if (name === "set-cookie") { + ArrayPrototypePush(cookies, [name, value]); + } else { + // The following code has the same behaviour as getHeader() + // at the end of loop. But it avoids looping through the entire + // list to combine multiple values with same header name. It + // instead gradually combines them as they are found. + let header = headers[name]; + if (header && header.length > 0) { + header += "\x2C\x20" + value; + } else { + header = value; } - appendHeader(headers, key, object[key]); + headers[name] = header; } } + + return ArrayPrototypeSort( + [ + ...new SafeArrayIterator(ObjectEntries(headers)), + ...new SafeArrayIterator(cookies), + ], + (a, b) => { + const akey = a[0]; + const bkey = b[0]; + if (akey > bkey) return 1; + if (akey < bkey) return -1; + return 0; + }, + ); } - // Regex matching illegal chars in a header value - // deno-lint-ignore no-control-regex - const ILLEGAL_VALUE_CHARS = /[\x00\x0A\x0D]/; + /** @param {HeadersInit} [init] */ + constructor(init = undefined) { + const prefix = "Failed to construct 'Headers'"; + if (init !== undefined) { + init = webidl.converters["HeadersInit"](init, { + prefix, + context: "Argument 1", + }); + } + + this[webidl.brand] = webidl.brand; + this[_guard] = "none"; + if (init !== undefined) { + fillHeaders(this, init); + } + } /** - * https://fetch.spec.whatwg.org/#concept-headers-append - * @param {Headers} headers * @param {string} name * @param {string} value */ - function appendHeader(headers, name, value) { - // 1. - value = normalizeHeaderValue(value); + append(name, value) { + webidl.assertBranded(this, HeadersPrototype); + const prefix = "Failed to execute 'append' on 'Headers'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + name = webidl.converters["ByteString"](name, { + prefix, + context: "Argument 1", + }); + value = webidl.converters["ByteString"](value, { + prefix, + context: "Argument 2", + }); + appendHeader(this, name, value); + } + + /** + * @param {string} name + */ + delete(name) { + const prefix = "Failed to execute 'delete' on 'Headers'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters["ByteString"](name, { + prefix, + context: "Argument 1", + }); - // 2. if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { throw new TypeError("Header name is not valid."); } - if (RegExpPrototypeTest(ILLEGAL_VALUE_CHARS, value)) { - throw new TypeError("Header value is not valid."); - } - - // 3. - if (headers[_guard] == "immutable") { + if (this[_guard] == "immutable") { throw new TypeError("Headers are immutable."); } - // 7. - const list = headers[_headerList]; + const list = this[_headerList]; const lowercaseName = byteLowerCase(name); for (let i = 0; i < list.length; i++) { if (byteLowerCase(list[i][0]) === lowercaseName) { - name = list[i][0]; - break; + ArrayPrototypeSplice(list, i, 1); + i--; } } - ArrayPrototypePush(list, [name, value]); } /** - * https://fetch.spec.whatwg.org/#concept-header-list-get - * @param {HeaderList} list * @param {string} name */ - function getHeader(list, name) { - const lowercaseName = byteLowerCase(name); - const entries = ArrayPrototypeMap( - ArrayPrototypeFilter( - list, - (entry) => byteLowerCase(entry[0]) === lowercaseName, - ), - (entry) => entry[1], - ); - if (entries.length === 0) { - return null; - } else { - return ArrayPrototypeJoin(entries, "\x2C\x20"); + get(name) { + const prefix = "Failed to execute 'get' on 'Headers'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters["ByteString"](name, { + prefix, + context: "Argument 1", + }); + + if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { + throw new TypeError("Header name is not valid."); } + + const list = this[_headerList]; + return getHeader(list, name); } /** - * https://fetch.spec.whatwg.org/#concept-header-list-get-decode-split - * @param {HeaderList} list * @param {string} name - * @returns {string[] | null} */ - function getDecodeSplitHeader(list, name) { - const initialValue = getHeader(list, name); - if (initialValue === null) return null; - const input = initialValue; - let position = 0; - const values = []; - let value = ""; - while (position < initialValue.length) { - // 7.1. collect up to " or , - const res = collectSequenceOfCodepoints( - initialValue, - position, - (c) => c !== "\u0022" && c !== "\u002C", - ); - value += res.result; - position = res.position; - - if (position < initialValue.length) { - if (input[position] === "\u0022") { - const res = collectHttpQuotedString(input, position, false); - value += res.result; - position = res.position; - if (position < initialValue.length) { - continue; - } - } else { - if (input[position] !== "\u002C") throw new TypeError("Unreachable"); - position += 1; - } - } - - value = StringPrototypeReplaceAll(value, HTTP_TAB_OR_SPACE_PREFIX_RE, ""); - value = StringPrototypeReplaceAll(value, HTTP_TAB_OR_SPACE_SUFFIX_RE, ""); + has(name) { + const prefix = "Failed to execute 'has' on 'Headers'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + name = webidl.converters["ByteString"](name, { + prefix, + context: "Argument 1", + }); - ArrayPrototypePush(values, value); - value = ""; + if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { + throw new TypeError("Header name is not valid."); } - return values; - } - class Headers { - /** @type {HeaderList} */ - [_headerList] = []; - /** @type {"immutable" | "request" | "request-no-cors" | "response" | "none"} */ - [_guard]; - - get [_iterableHeaders]() { - const list = this[_headerList]; - - // The order of steps are not similar to the ones suggested by the - // spec but produce the same result. - const headers = {}; - const cookies = []; - for (let i = 0; i < list.length; ++i) { - const entry = list[i]; - const name = byteLowerCase(entry[0]); - const value = entry[1]; - if (value === null) throw new TypeError("Unreachable"); - // The following if statement is not spec compliant. - // `set-cookie` is the only header that can not be concatenated, - // so must be given to the user as multiple headers. - // The else block of the if statement is spec compliant again. - if (name === "set-cookie") { - ArrayPrototypePush(cookies, [name, value]); - } else { - // The following code has the same behaviour as getHeader() - // at the end of loop. But it avoids looping through the entire - // list to combine multiple values with same header name. It - // instead gradually combines them as they are found. - let header = headers[name]; - if (header && header.length > 0) { - header += "\x2C\x20" + value; - } else { - header = value; - } - headers[name] = header; - } + const list = this[_headerList]; + const lowercaseName = byteLowerCase(name); + for (let i = 0; i < list.length; i++) { + if (byteLowerCase(list[i][0]) === lowercaseName) { + return true; } - - return ArrayPrototypeSort( - [ - ...new SafeArrayIterator(ObjectEntries(headers)), - ...new SafeArrayIterator(cookies), - ], - (a, b) => { - const akey = a[0]; - const bkey = b[0]; - if (akey > bkey) return 1; - if (akey < bkey) return -1; - return 0; - }, - ); } + return false; + } - /** @param {HeadersInit} [init] */ - constructor(init = undefined) { - const prefix = "Failed to construct 'Headers'"; - if (init !== undefined) { - init = webidl.converters["HeadersInit"](init, { - prefix, - context: "Argument 1", - }); - } + /** + * @param {string} name + * @param {string} value + */ + set(name, value) { + webidl.assertBranded(this, HeadersPrototype); + const prefix = "Failed to execute 'set' on 'Headers'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + name = webidl.converters["ByteString"](name, { + prefix, + context: "Argument 1", + }); + value = webidl.converters["ByteString"](value, { + prefix, + context: "Argument 2", + }); - this[webidl.brand] = webidl.brand; - this[_guard] = "none"; - if (init !== undefined) { - fillHeaders(this, init); - } - } + value = normalizeHeaderValue(value); - /** - * @param {string} name - * @param {string} value - */ - append(name, value) { - webidl.assertBranded(this, HeadersPrototype); - const prefix = "Failed to execute 'append' on 'Headers'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - name = webidl.converters["ByteString"](name, { - prefix, - context: "Argument 1", - }); - value = webidl.converters["ByteString"](value, { - prefix, - context: "Argument 2", - }); - appendHeader(this, name, value); + // 2. + if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { + throw new TypeError("Header name is not valid."); } - - /** - * @param {string} name - */ - delete(name) { - const prefix = "Failed to execute 'delete' on 'Headers'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); - name = webidl.converters["ByteString"](name, { - prefix, - context: "Argument 1", - }); - - if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { - throw new TypeError("Header name is not valid."); - } - if (this[_guard] == "immutable") { - throw new TypeError("Headers are immutable."); - } - - const list = this[_headerList]; - const lowercaseName = byteLowerCase(name); - for (let i = 0; i < list.length; i++) { - if (byteLowerCase(list[i][0]) === lowercaseName) { - ArrayPrototypeSplice(list, i, 1); - i--; - } - } + if (RegExpPrototypeTest(ILLEGAL_VALUE_CHARS, value)) { + throw new TypeError("Header value is not valid."); } - /** - * @param {string} name - */ - get(name) { - const prefix = "Failed to execute 'get' on 'Headers'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); - name = webidl.converters["ByteString"](name, { - prefix, - context: "Argument 1", - }); - - if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { - throw new TypeError("Header name is not valid."); - } - - const list = this[_headerList]; - return getHeader(list, name); + if (this[_guard] == "immutable") { + throw new TypeError("Headers are immutable."); } - /** - * @param {string} name - */ - has(name) { - const prefix = "Failed to execute 'has' on 'Headers'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); - name = webidl.converters["ByteString"](name, { - prefix, - context: "Argument 1", - }); - - if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { - throw new TypeError("Header name is not valid."); - } - - const list = this[_headerList]; - const lowercaseName = byteLowerCase(name); - for (let i = 0; i < list.length; i++) { - if (byteLowerCase(list[i][0]) === lowercaseName) { - return true; + const list = this[_headerList]; + const lowercaseName = byteLowerCase(name); + let added = false; + for (let i = 0; i < list.length; i++) { + if (byteLowerCase(list[i][0]) === lowercaseName) { + if (!added) { + list[i][1] = value; + added = true; + } else { + ArrayPrototypeSplice(list, i, 1); + i--; } } - return false; } - - /** - * @param {string} name - * @param {string} value - */ - set(name, value) { - webidl.assertBranded(this, HeadersPrototype); - const prefix = "Failed to execute 'set' on 'Headers'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - name = webidl.converters["ByteString"](name, { - prefix, - context: "Argument 1", - }); - value = webidl.converters["ByteString"](value, { - prefix, - context: "Argument 2", - }); - - value = normalizeHeaderValue(value); - - // 2. - if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, name)) { - throw new TypeError("Header name is not valid."); - } - if (RegExpPrototypeTest(ILLEGAL_VALUE_CHARS, value)) { - throw new TypeError("Header value is not valid."); - } - - if (this[_guard] == "immutable") { - throw new TypeError("Headers are immutable."); - } - - const list = this[_headerList]; - const lowercaseName = byteLowerCase(name); - let added = false; - for (let i = 0; i < list.length; i++) { - if (byteLowerCase(list[i][0]) === lowercaseName) { - if (!added) { - list[i][1] = value; - added = true; - } else { - ArrayPrototypeSplice(list, i, 1); - i--; - } - } - } - if (!added) { - ArrayPrototypePush(list, [name, value]); - } + if (!added) { + ArrayPrototypePush(list, [name, value]); } + } - [SymbolFor("Deno.privateCustomInspect")](inspect) { - const headers = {}; - // deno-lint-ignore prefer-primordials - for (const header of this) { - headers[header[0]] = header[1]; - } - return `Headers ${inspect(headers)}`; + [SymbolFor("Deno.privateCustomInspect")](inspect) { + const headers = {}; + // deno-lint-ignore prefer-primordials + for (const header of this) { + headers[header[0]] = header[1]; } + return `Headers ${inspect(headers)}`; } +} - webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1); +webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1); - webidl.configurePrototype(Headers); - const HeadersPrototype = Headers.prototype; +webidl.configurePrototype(Headers); +const HeadersPrototype = Headers.prototype; - webidl.converters["HeadersInit"] = (V, opts) => { - // Union for (sequence<sequence<ByteString>> or record<ByteString, ByteString>) - if (webidl.type(V) === "Object" && V !== null) { - if (V[SymbolIterator] !== undefined) { - return webidl.converters["sequence<sequence<ByteString>>"](V, opts); - } - return webidl.converters["record<ByteString, ByteString>"](V, opts); +webidl.converters["HeadersInit"] = (V, opts) => { + // Union for (sequence<sequence<ByteString>> or record<ByteString, ByteString>) + if (webidl.type(V) === "Object" && V !== null) { + if (V[SymbolIterator] !== undefined) { + return webidl.converters["sequence<sequence<ByteString>>"](V, opts); } - throw webidl.makeException( - TypeError, - "The provided value is not of type '(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'", - opts, - ); - }; - webidl.converters["Headers"] = webidl.createInterfaceConverter( - "Headers", - Headers.prototype, - ); - - /** - * @param {HeaderList} list - * @param {"immutable" | "request" | "request-no-cors" | "response" | "none"} guard - * @returns {Headers} - */ - function headersFromHeaderList(list, guard) { - const headers = webidl.createBranded(Headers); - headers[_headerList] = list; - headers[_guard] = guard; - return headers; + return webidl.converters["record<ByteString, ByteString>"](V, opts); } - - /** - * @param {Headers} - * @returns {HeaderList} - */ - function headerListFromHeaders(headers) { - return headers[_headerList]; - } - - /** - * @param {Headers} - * @returns {"immutable" | "request" | "request-no-cors" | "response" | "none"} - */ - function guardFromHeaders(headers) { - return headers[_guard]; - } - - window.__bootstrap.headers = { - headersFromHeaderList, - headerListFromHeaders, - getDecodeSplitHeader, - guardFromHeaders, - fillHeaders, - getHeader, - Headers, - }; -})(this); + throw webidl.makeException( + TypeError, + "The provided value is not of type '(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'", + opts, + ); +}; +webidl.converters["Headers"] = webidl.createInterfaceConverter( + "Headers", + Headers.prototype, +); + +/** + * @param {HeaderList} list + * @param {"immutable" | "request" | "request-no-cors" | "response" | "none"} guard + * @returns {Headers} + */ +function headersFromHeaderList(list, guard) { + const headers = webidl.createBranded(Headers); + headers[_headerList] = list; + headers[_guard] = guard; + return headers; +} + +/** + * @param {Headers} headers + * @returns {HeaderList} + */ +function headerListFromHeaders(headers) { + return headers[_headerList]; +} + +/** + * @param {Headers} headers + * @returns {"immutable" | "request" | "request-no-cors" | "response" | "none"} + */ +function guardFromHeaders(headers) { + return headers[_guard]; +} + +export { + fillHeaders, + getDecodeSplitHeader, + getHeader, + guardFromHeaders, + headerListFromHeaders, + Headers, + headersFromHeaderList, +}; diff --git a/ext/fetch/21_formdata.js b/ext/fetch/21_formdata.js index d253976ef..1639646e0 100644 --- a/ext/fetch/21_formdata.js +++ b/ext/fetch/21_formdata.js @@ -8,516 +8,518 @@ /// <reference path="../web/06_streams_types.d.ts" /> /// <reference path="./lib.deno_fetch.d.ts" /> /// <reference lib="esnext" /> -"use strict"; - -((window) => { - const core = window.Deno.core; - const webidl = globalThis.__bootstrap.webidl; - const { Blob, BlobPrototype, File, FilePrototype } = - globalThis.__bootstrap.file; - const { - ArrayPrototypePush, - ArrayPrototypeSlice, - ArrayPrototypeSplice, - Map, - MapPrototypeGet, - MapPrototypeSet, - MathRandom, - ObjectPrototypeIsPrototypeOf, - Symbol, - StringFromCharCode, - StringPrototypeTrim, - StringPrototypeSlice, - StringPrototypeSplit, - StringPrototypeReplace, - StringPrototypeIndexOf, - StringPrototypePadStart, - StringPrototypeCodePointAt, - StringPrototypeReplaceAll, - TypeError, - TypedArrayPrototypeSubarray, - } = window.__bootstrap.primordials; - - const entryList = Symbol("entry list"); - /** - * @param {string} name - * @param {string | Blob} value - * @param {string | undefined} filename - * @returns {FormDataEntry} - */ - function createEntry(name, value, filename) { - if ( - ObjectPrototypeIsPrototypeOf(BlobPrototype, value) && - !ObjectPrototypeIsPrototypeOf(FilePrototype, value) - ) { - value = new File([value], "blob", { type: value.type }); - } - if ( - ObjectPrototypeIsPrototypeOf(FilePrototype, value) && - filename !== undefined - ) { - value = new File([value], filename, { - type: value.type, - lastModified: value.lastModified, - }); +const core = globalThis.Deno.core; +import * as webidl from "internal:ext/webidl/00_webidl.js"; +import { + Blob, + BlobPrototype, + File, + FilePrototype, +} from "internal:ext/web/09_file.js"; +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + Map, + MapPrototypeGet, + MapPrototypeSet, + MathRandom, + ObjectPrototypeIsPrototypeOf, + Symbol, + StringFromCharCode, + StringPrototypeTrim, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeReplace, + StringPrototypeIndexOf, + StringPrototypePadStart, + StringPrototypeCodePointAt, + StringPrototypeReplaceAll, + TypeError, + TypedArrayPrototypeSubarray, +} = primordials; + +const entryList = Symbol("entry list"); + +/** + * @param {string} name + * @param {string | Blob} value + * @param {string | undefined} filename + * @returns {FormDataEntry} + */ +function createEntry(name, value, filename) { + if ( + ObjectPrototypeIsPrototypeOf(BlobPrototype, value) && + !ObjectPrototypeIsPrototypeOf(FilePrototype, value) + ) { + value = new File([value], "blob", { type: value.type }); + } + if ( + ObjectPrototypeIsPrototypeOf(FilePrototype, value) && + filename !== undefined + ) { + value = new File([value], filename, { + type: value.type, + lastModified: value.lastModified, + }); + } + return { + name, + // @ts-expect-error because TS is not smart enough + value, + }; +} + +/** + * @typedef FormDataEntry + * @property {string} name + * @property {FormDataEntryValue} value + */ + +class FormData { + /** @type {FormDataEntry[]} */ + [entryList] = []; + + /** @param {void} form */ + constructor(form) { + if (form !== undefined) { + webidl.illegalConstructor(); } - return { - name, - // @ts-expect-error because TS is not smart enough - value, - }; + this[webidl.brand] = webidl.brand; } /** - * @typedef FormDataEntry - * @property {string} name - * @property {FormDataEntryValue} value + * @param {string} name + * @param {string | Blob} valueOrBlobValue + * @param {string} [filename] + * @returns {void} */ - - class FormData { - /** @type {FormDataEntry[]} */ - [entryList] = []; - - /** @param {void} form */ - constructor(form) { - if (form !== undefined) { - webidl.illegalConstructor(); - } - this[webidl.brand] = webidl.brand; - } - - /** - * @param {string} name - * @param {string | Blob} valueOrBlobValue - * @param {string} [filename] - * @returns {void} - */ - append(name, valueOrBlobValue, filename) { - webidl.assertBranded(this, FormDataPrototype); - const prefix = "Failed to execute 'append' on 'FormData'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - - name = webidl.converters["USVString"](name, { + append(name, valueOrBlobValue, filename) { + webidl.assertBranded(this, FormDataPrototype); + const prefix = "Failed to execute 'append' on 'FormData'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + + name = webidl.converters["USVString"](name, { + prefix, + context: "Argument 1", + }); + if (ObjectPrototypeIsPrototypeOf(BlobPrototype, valueOrBlobValue)) { + valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, { prefix, - context: "Argument 1", + context: "Argument 2", }); - if (ObjectPrototypeIsPrototypeOf(BlobPrototype, valueOrBlobValue)) { - valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, { - prefix, - context: "Argument 2", - }); - if (filename !== undefined) { - filename = webidl.converters["USVString"](filename, { - prefix, - context: "Argument 3", - }); - } - } else { - valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, { + if (filename !== undefined) { + filename = webidl.converters["USVString"](filename, { prefix, - context: "Argument 2", + context: "Argument 3", }); } - - const entry = createEntry(name, valueOrBlobValue, filename); - - ArrayPrototypePush(this[entryList], entry); - } - - /** - * @param {string} name - * @returns {void} - */ - delete(name) { - webidl.assertBranded(this, FormDataPrototype); - const prefix = "Failed to execute 'name' on 'FormData'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); - - name = webidl.converters["USVString"](name, { + } else { + valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, { prefix, - context: "Argument 1", + context: "Argument 2", }); - - const list = this[entryList]; - for (let i = 0; i < list.length; i++) { - if (list[i].name === name) { - ArrayPrototypeSplice(list, i, 1); - i--; - } - } } - /** - * @param {string} name - * @returns {FormDataEntryValue | null} - */ - get(name) { - webidl.assertBranded(this, FormDataPrototype); - const prefix = "Failed to execute 'get' on 'FormData'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); + const entry = createEntry(name, valueOrBlobValue, filename); - name = webidl.converters["USVString"](name, { - prefix, - context: "Argument 1", - }); + ArrayPrototypePush(this[entryList], entry); + } - const entries = this[entryList]; - for (let i = 0; i < entries.length; ++i) { - const entry = entries[i]; - if (entry.name === name) return entry.value; + /** + * @param {string} name + * @returns {void} + */ + delete(name) { + webidl.assertBranded(this, FormDataPrototype); + const prefix = "Failed to execute 'name' on 'FormData'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + name = webidl.converters["USVString"](name, { + prefix, + context: "Argument 1", + }); + + const list = this[entryList]; + for (let i = 0; i < list.length; i++) { + if (list[i].name === name) { + ArrayPrototypeSplice(list, i, 1); + i--; } - return null; } + } - /** - * @param {string} name - * @returns {FormDataEntryValue[]} - */ - getAll(name) { - webidl.assertBranded(this, FormDataPrototype); - const prefix = "Failed to execute 'getAll' on 'FormData'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); - - name = webidl.converters["USVString"](name, { - prefix, - context: "Argument 1", - }); + /** + * @param {string} name + * @returns {FormDataEntryValue | null} + */ + get(name) { + webidl.assertBranded(this, FormDataPrototype); + const prefix = "Failed to execute 'get' on 'FormData'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + name = webidl.converters["USVString"](name, { + prefix, + context: "Argument 1", + }); - const returnList = []; - const entries = this[entryList]; - for (let i = 0; i < entries.length; ++i) { - const entry = entries[i]; - if (entry.name === name) ArrayPrototypePush(returnList, entry.value); - } - return returnList; + const entries = this[entryList]; + for (let i = 0; i < entries.length; ++i) { + const entry = entries[i]; + if (entry.name === name) return entry.value; } + return null; + } - /** - * @param {string} name - * @returns {boolean} - */ - has(name) { - webidl.assertBranded(this, FormDataPrototype); - const prefix = "Failed to execute 'has' on 'FormData'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); + /** + * @param {string} name + * @returns {FormDataEntryValue[]} + */ + getAll(name) { + webidl.assertBranded(this, FormDataPrototype); + const prefix = "Failed to execute 'getAll' on 'FormData'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + name = webidl.converters["USVString"](name, { + prefix, + context: "Argument 1", + }); - name = webidl.converters["USVString"](name, { - prefix, - context: "Argument 1", - }); + const returnList = []; + const entries = this[entryList]; + for (let i = 0; i < entries.length; ++i) { + const entry = entries[i]; + if (entry.name === name) ArrayPrototypePush(returnList, entry.value); + } + return returnList; + } - const entries = this[entryList]; - for (let i = 0; i < entries.length; ++i) { - const entry = entries[i]; - if (entry.name === name) return true; - } - return false; + /** + * @param {string} name + * @returns {boolean} + */ + has(name) { + webidl.assertBranded(this, FormDataPrototype); + const prefix = "Failed to execute 'has' on 'FormData'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + + name = webidl.converters["USVString"](name, { + prefix, + context: "Argument 1", + }); + + const entries = this[entryList]; + for (let i = 0; i < entries.length; ++i) { + const entry = entries[i]; + if (entry.name === name) return true; } + return false; + } - /** - * @param {string} name - * @param {string | Blob} valueOrBlobValue - * @param {string} [filename] - * @returns {void} - */ - set(name, valueOrBlobValue, filename) { - webidl.assertBranded(this, FormDataPrototype); - const prefix = "Failed to execute 'set' on 'FormData'"; - webidl.requiredArguments(arguments.length, 2, { prefix }); - - name = webidl.converters["USVString"](name, { + /** + * @param {string} name + * @param {string | Blob} valueOrBlobValue + * @param {string} [filename] + * @returns {void} + */ + set(name, valueOrBlobValue, filename) { + webidl.assertBranded(this, FormDataPrototype); + const prefix = "Failed to execute 'set' on 'FormData'"; + webidl.requiredArguments(arguments.length, 2, { prefix }); + + name = webidl.converters["USVString"](name, { + prefix, + context: "Argument 1", + }); + if (ObjectPrototypeIsPrototypeOf(BlobPrototype, valueOrBlobValue)) { + valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, { prefix, - context: "Argument 1", + context: "Argument 2", }); - if (ObjectPrototypeIsPrototypeOf(BlobPrototype, valueOrBlobValue)) { - valueOrBlobValue = webidl.converters["Blob"](valueOrBlobValue, { - prefix, - context: "Argument 2", - }); - if (filename !== undefined) { - filename = webidl.converters["USVString"](filename, { - prefix, - context: "Argument 3", - }); - } - } else { - valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, { + if (filename !== undefined) { + filename = webidl.converters["USVString"](filename, { prefix, - context: "Argument 2", + context: "Argument 3", }); } + } else { + valueOrBlobValue = webidl.converters["USVString"](valueOrBlobValue, { + prefix, + context: "Argument 2", + }); + } - const entry = createEntry(name, valueOrBlobValue, filename); + const entry = createEntry(name, valueOrBlobValue, filename); - const list = this[entryList]; - let added = false; - for (let i = 0; i < list.length; i++) { - if (list[i].name === name) { - if (!added) { - list[i] = entry; - added = true; - } else { - ArrayPrototypeSplice(list, i, 1); - i--; - } + const list = this[entryList]; + let added = false; + for (let i = 0; i < list.length; i++) { + if (list[i].name === name) { + if (!added) { + list[i] = entry; + added = true; + } else { + ArrayPrototypeSplice(list, i, 1); + i--; } } - if (!added) { - ArrayPrototypePush(list, entry); - } + } + if (!added) { + ArrayPrototypePush(list, entry); } } +} - webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value"); +webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value"); - webidl.configurePrototype(FormData); - const FormDataPrototype = FormData.prototype; +webidl.configurePrototype(FormData); +const FormDataPrototype = FormData.prototype; - const escape = (str, isFilename) => { - const escapeMap = { - "\n": "%0A", - "\r": "%0D", - '"': "%22", - }; - - return StringPrototypeReplace( - isFilename ? str : StringPrototypeReplace(str, /\r?\n|\r/g, "\r\n"), - /([\n\r"])/g, - (c) => escapeMap[c], - ); +const escape = (str, isFilename) => { + const escapeMap = { + "\n": "%0A", + "\r": "%0D", + '"': "%22", }; - /** - * convert FormData to a Blob synchronous without reading all of the files - * @param {globalThis.FormData} formData - */ - function formDataToBlob(formData) { - const boundary = StringPrototypePadStart( - StringPrototypeSlice( - StringPrototypeReplaceAll(`${MathRandom()}${MathRandom()}`, ".", ""), - -28, - ), - 32, - "-", - ); - const chunks = []; - const prefix = `--${boundary}\r\nContent-Disposition: form-data; name="`; - - // deno-lint-ignore prefer-primordials - for (const { 0: name, 1: value } of formData) { - if (typeof value === "string") { - ArrayPrototypePush( - chunks, - prefix + escape(name) + '"' + CRLF + CRLF + - StringPrototypeReplace(value, /\r(?!\n)|(?<!\r)\n/g, CRLF) + CRLF, - ); - } else { - ArrayPrototypePush( - chunks, - prefix + escape(name) + `"; filename="${escape(value.name, true)}"` + - CRLF + - `Content-Type: ${value.type || "application/octet-stream"}\r\n\r\n`, - value, - CRLF, - ); - } + return StringPrototypeReplace( + isFilename ? str : StringPrototypeReplace(str, /\r?\n|\r/g, "\r\n"), + /([\n\r"])/g, + (c) => escapeMap[c], + ); +}; + +/** + * convert FormData to a Blob synchronous without reading all of the files + * @param {globalThis.FormData} formData + */ +function formDataToBlob(formData) { + const boundary = StringPrototypePadStart( + StringPrototypeSlice( + StringPrototypeReplaceAll(`${MathRandom()}${MathRandom()}`, ".", ""), + -28, + ), + 32, + "-", + ); + const chunks = []; + const prefix = `--${boundary}\r\nContent-Disposition: form-data; name="`; + + // deno-lint-ignore prefer-primordials + for (const { 0: name, 1: value } of formData) { + if (typeof value === "string") { + ArrayPrototypePush( + chunks, + prefix + escape(name) + '"' + CRLF + CRLF + + StringPrototypeReplace(value, /\r(?!\n)|(?<!\r)\n/g, CRLF) + CRLF, + ); + } else { + ArrayPrototypePush( + chunks, + prefix + escape(name) + `"; filename="${escape(value.name, true)}"` + + CRLF + + `Content-Type: ${value.type || "application/octet-stream"}\r\n\r\n`, + value, + CRLF, + ); } + } - ArrayPrototypePush(chunks, `--${boundary}--`); - - return new Blob(chunks, { - type: "multipart/form-data; boundary=" + boundary, - }); + ArrayPrototypePush(chunks, `--${boundary}--`); + + return new Blob(chunks, { + type: "multipart/form-data; boundary=" + boundary, + }); +} + +/** + * @param {string} value + * @returns {Map<string, string>} + */ +function parseContentDisposition(value) { + /** @type {Map<string, string>} */ + const params = new Map(); + // Forced to do so for some Map constructor param mismatch + const values = ArrayPrototypeSlice(StringPrototypeSplit(value, ";"), 1); + for (let i = 0; i < values.length; i++) { + const entries = StringPrototypeSplit(StringPrototypeTrim(values[i]), "="); + if (entries.length > 1) { + MapPrototypeSet( + params, + entries[0], + StringPrototypeReplace(entries[1], /^"([^"]*)"$/, "$1"), + ); + } } + return params; +} +const CRLF = "\r\n"; +const LF = StringPrototypeCodePointAt(CRLF, 1); +const CR = StringPrototypeCodePointAt(CRLF, 0); + +class MultipartParser { /** - * @param {string} value - * @returns {Map<string, string>} + * @param {Uint8Array} body + * @param {string | undefined} boundary */ - function parseContentDisposition(value) { - /** @type {Map<string, string>} */ - const params = new Map(); - // Forced to do so for some Map constructor param mismatch - const values = ArrayPrototypeSlice(StringPrototypeSplit(value, ";"), 1); - for (let i = 0; i < values.length; i++) { - const entries = StringPrototypeSplit(StringPrototypeTrim(values[i]), "="); - if (entries.length > 1) { - MapPrototypeSet( - params, - entries[0], - StringPrototypeReplace(entries[1], /^"([^"]*)"$/, "$1"), - ); - } + constructor(body, boundary) { + if (!boundary) { + throw new TypeError("multipart/form-data must provide a boundary"); } - return params; + + this.boundary = `--${boundary}`; + this.body = body; + this.boundaryChars = core.encode(this.boundary); } - const CRLF = "\r\n"; - const LF = StringPrototypeCodePointAt(CRLF, 1); - const CR = StringPrototypeCodePointAt(CRLF, 0); - - class MultipartParser { - /** - * @param {Uint8Array} body - * @param {string | undefined} boundary - */ - constructor(body, boundary) { - if (!boundary) { - throw new TypeError("multipart/form-data must provide a boundary"); + /** + * @param {string} headersText + * @returns {{ headers: Headers, disposition: Map<string, string> }} + */ + #parseHeaders(headersText) { + const headers = new Headers(); + const rawHeaders = StringPrototypeSplit(headersText, "\r\n"); + for (let i = 0; i < rawHeaders.length; ++i) { + const rawHeader = rawHeaders[i]; + const sepIndex = StringPrototypeIndexOf(rawHeader, ":"); + if (sepIndex < 0) { + continue; // Skip this header } - - this.boundary = `--${boundary}`; - this.body = body; - this.boundaryChars = core.encode(this.boundary); + const key = StringPrototypeSlice(rawHeader, 0, sepIndex); + const value = StringPrototypeSlice(rawHeader, sepIndex + 1); + headers.set(key, value); } - /** - * @param {string} headersText - * @returns {{ headers: Headers, disposition: Map<string, string> }} - */ - #parseHeaders(headersText) { - const headers = new Headers(); - const rawHeaders = StringPrototypeSplit(headersText, "\r\n"); - for (let i = 0; i < rawHeaders.length; ++i) { - const rawHeader = rawHeaders[i]; - const sepIndex = StringPrototypeIndexOf(rawHeader, ":"); - if (sepIndex < 0) { - continue; // Skip this header - } - const key = StringPrototypeSlice(rawHeader, 0, sepIndex); - const value = StringPrototypeSlice(rawHeader, sepIndex + 1); - headers.set(key, value); - } - - const disposition = parseContentDisposition( - headers.get("Content-Disposition") ?? "", - ); + const disposition = parseContentDisposition( + headers.get("Content-Disposition") ?? "", + ); - return { headers, disposition }; - } + return { headers, disposition }; + } - /** - * @returns {FormData} - */ - parse() { - // To have fields body must be at least 2 boundaries + \r\n + -- - // on the last boundary. - if (this.body.length < (this.boundary.length * 2) + 4) { - const decodedBody = core.decode(this.body); - const lastBoundary = this.boundary + "--"; - // check if it's an empty valid form data - if ( - decodedBody === lastBoundary || - decodedBody === lastBoundary + "\r\n" - ) { - return new FormData(); - } - throw new TypeError("Unable to parse body as form data."); + /** + * @returns {FormData} + */ + parse() { + // To have fields body must be at least 2 boundaries + \r\n + -- + // on the last boundary. + if (this.body.length < (this.boundary.length * 2) + 4) { + const decodedBody = core.decode(this.body); + const lastBoundary = this.boundary + "--"; + // check if it's an empty valid form data + if ( + decodedBody === lastBoundary || + decodedBody === lastBoundary + "\r\n" + ) { + return new FormData(); } + throw new TypeError("Unable to parse body as form data."); + } - const formData = new FormData(); - let headerText = ""; - let boundaryIndex = 0; - let state = 0; - let fileStart = 0; + const formData = new FormData(); + let headerText = ""; + let boundaryIndex = 0; + let state = 0; + let fileStart = 0; - for (let i = 0; i < this.body.length; i++) { - const byte = this.body[i]; - const prevByte = this.body[i - 1]; - const isNewLine = byte === LF && prevByte === CR; + for (let i = 0; i < this.body.length; i++) { + const byte = this.body[i]; + const prevByte = this.body[i - 1]; + const isNewLine = byte === LF && prevByte === CR; - if (state === 1 || state === 2 || state == 3) { - headerText += StringFromCharCode(byte); - } - if (state === 0 && isNewLine) { - state = 1; - } else if (state === 1 && isNewLine) { - state = 2; - const headersDone = this.body[i + 1] === CR && - this.body[i + 2] === LF; - - if (headersDone) { - state = 3; - } - } else if (state === 2 && isNewLine) { + if (state === 1 || state === 2 || state == 3) { + headerText += StringFromCharCode(byte); + } + if (state === 0 && isNewLine) { + state = 1; + } else if (state === 1 && isNewLine) { + state = 2; + const headersDone = this.body[i + 1] === CR && + this.body[i + 2] === LF; + + if (headersDone) { state = 3; - } else if (state === 3 && isNewLine) { - state = 4; - fileStart = i + 1; - } else if (state === 4) { - if (this.boundaryChars[boundaryIndex] !== byte) { - boundaryIndex = 0; - } else { - boundaryIndex++; + } + } else if (state === 2 && isNewLine) { + state = 3; + } else if (state === 3 && isNewLine) { + state = 4; + fileStart = i + 1; + } else if (state === 4) { + if (this.boundaryChars[boundaryIndex] !== byte) { + boundaryIndex = 0; + } else { + boundaryIndex++; + } + + if (boundaryIndex >= this.boundary.length) { + const { headers, disposition } = this.#parseHeaders(headerText); + const content = TypedArrayPrototypeSubarray( + this.body, + fileStart, + i - boundaryIndex - 1, + ); + // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata + const filename = MapPrototypeGet(disposition, "filename"); + const name = MapPrototypeGet(disposition, "name"); + + state = 5; + // Reset + boundaryIndex = 0; + headerText = ""; + + if (!name) { + continue; // Skip, unknown name } - if (boundaryIndex >= this.boundary.length) { - const { headers, disposition } = this.#parseHeaders(headerText); - const content = TypedArrayPrototypeSubarray( - this.body, - fileStart, - i - boundaryIndex - 1, - ); - // https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata - const filename = MapPrototypeGet(disposition, "filename"); - const name = MapPrototypeGet(disposition, "name"); - - state = 5; - // Reset - boundaryIndex = 0; - headerText = ""; - - if (!name) { - continue; // Skip, unknown name - } - - if (filename) { - const blob = new Blob([content], { - type: headers.get("Content-Type") || "application/octet-stream", - }); - formData.append(name, blob, filename); - } else { - formData.append(name, core.decode(content)); - } + if (filename) { + const blob = new Blob([content], { + type: headers.get("Content-Type") || "application/octet-stream", + }); + formData.append(name, blob, filename); + } else { + formData.append(name, core.decode(content)); } - } else if (state === 5 && isNewLine) { - state = 1; } + } else if (state === 5 && isNewLine) { + state = 1; } - - return formData; } - } - - /** - * @param {Uint8Array} body - * @param {string | undefined} boundary - * @returns {FormData} - */ - function parseFormData(body, boundary) { - const parser = new MultipartParser(body, boundary); - return parser.parse(); - } - /** - * @param {FormDataEntry[]} entries - * @returns {FormData} - */ - function formDataFromEntries(entries) { - const fd = new FormData(); - fd[entryList] = entries; - return fd; + return formData; } - - webidl.converters["FormData"] = webidl - .createInterfaceConverter("FormData", FormDataPrototype); - - globalThis.__bootstrap.formData = { - FormData, - FormDataPrototype, - formDataToBlob, - parseFormData, - formDataFromEntries, - }; -})(globalThis); +} + +/** + * @param {Uint8Array} body + * @param {string | undefined} boundary + * @returns {FormData} + */ +function parseFormData(body, boundary) { + const parser = new MultipartParser(body, boundary); + return parser.parse(); +} + +/** + * @param {FormDataEntry[]} entries + * @returns {FormData} + */ +function formDataFromEntries(entries) { + const fd = new FormData(); + fd[entryList] = entries; + return fd; +} + +webidl.converters["FormData"] = webidl + .createInterfaceConverter("FormData", FormDataPrototype); + +export { + FormData, + formDataFromEntries, + FormDataPrototype, + formDataToBlob, + parseFormData, +}; diff --git a/ext/fetch/22_body.js b/ext/fetch/22_body.js index bea1abce2..48819650a 100644 --- a/ext/fetch/22_body.js +++ b/ext/fetch/22_body.js @@ -10,462 +10,462 @@ /// <reference path="../web/06_streams_types.d.ts" /> /// <reference path="./lib.deno_fetch.d.ts" /> /// <reference lib="esnext" /> -"use strict"; -((window) => { - const core = window.Deno.core; - const webidl = globalThis.__bootstrap.webidl; - const { parseUrlEncoded } = globalThis.__bootstrap.url; - const { URLSearchParamsPrototype } = globalThis.__bootstrap.url; - const { - parseFormData, - formDataFromEntries, - formDataToBlob, - FormDataPrototype, - } = globalThis.__bootstrap.formData; - const mimesniff = globalThis.__bootstrap.mimesniff; - const { BlobPrototype } = globalThis.__bootstrap.file; - const { - isReadableStreamDisturbed, - errorReadableStream, - readableStreamClose, - readableStreamDisturb, - readableStreamCollectIntoUint8Array, - readableStreamThrowIfErrored, - createProxy, - ReadableStreamPrototype, - } = globalThis.__bootstrap.streams; - const { - ArrayBufferPrototype, - ArrayBufferIsView, - ArrayPrototypeMap, - JSONParse, - ObjectDefineProperties, - ObjectPrototypeIsPrototypeOf, - // TODO(lucacasonato): add SharedArrayBuffer to primordials - // SharedArrayBufferPrototype - TypedArrayPrototypeSlice, - TypeError, - Uint8Array, - Uint8ArrayPrototype, - } = window.__bootstrap.primordials; +const core = globalThis.Deno.core; +import * as webidl from "internal:ext/webidl/00_webidl.js"; +import { + parseUrlEncoded, + URLSearchParamsPrototype, +} from "internal:ext/url/00_url.js"; +import { + formDataFromEntries, + FormDataPrototype, + formDataToBlob, + parseFormData, +} from "internal:ext/fetch/21_formdata.js"; +import * as mimesniff from "internal:ext/web/01_mimesniff.js"; +import { BlobPrototype } from "internal:ext/web/09_file.js"; +import { + createProxy, + errorReadableStream, + isReadableStreamDisturbed, + readableStreamClose, + readableStreamCollectIntoUint8Array, + readableStreamDisturb, + ReadableStreamPrototype, + readableStreamThrowIfErrored, +} from "internal:ext/web/06_streams.js"; +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayBufferPrototype, + ArrayBufferIsView, + ArrayPrototypeMap, + JSONParse, + ObjectDefineProperties, + ObjectPrototypeIsPrototypeOf, + // TODO(lucacasonato): add SharedArrayBuffer to primordials + // SharedArrayBufferPrototype + TypedArrayPrototypeSlice, + TypeError, + Uint8Array, + Uint8ArrayPrototype, +} = primordials; - /** - * @param {Uint8Array | string} chunk - * @returns {Uint8Array} - */ - function chunkToU8(chunk) { - return typeof chunk === "string" ? core.encode(chunk) : chunk; - } +/** + * @param {Uint8Array | string} chunk + * @returns {Uint8Array} + */ +function chunkToU8(chunk) { + return typeof chunk === "string" ? core.encode(chunk) : chunk; +} +/** + * @param {Uint8Array | string} chunk + * @returns {string} + */ +function chunkToString(chunk) { + return typeof chunk === "string" ? chunk : core.decode(chunk); +} + +class InnerBody { /** - * @param {Uint8Array | string} chunk - * @returns {string} + * @param {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} stream */ - function chunkToString(chunk) { - return typeof chunk === "string" ? chunk : core.decode(chunk); + constructor(stream) { + /** @type {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} */ + this.streamOrStatic = stream ?? + { body: new Uint8Array(), consumed: false }; + /** @type {null | Uint8Array | string | Blob | FormData} */ + this.source = null; + /** @type {null | number} */ + this.length = null; } - class InnerBody { - /** - * @param {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} stream - */ - constructor(stream) { - /** @type {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} */ - this.streamOrStatic = stream ?? - { body: new Uint8Array(), consumed: false }; - /** @type {null | Uint8Array | string | Blob | FormData} */ - this.source = null; - /** @type {null | number} */ - this.length = null; - } - - get stream() { - if ( - !ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - this.streamOrStatic, - ) - ) { - const { body, consumed } = this.streamOrStatic; - if (consumed) { - this.streamOrStatic = new ReadableStream(); - this.streamOrStatic.getReader(); - readableStreamDisturb(this.streamOrStatic); - readableStreamClose(this.streamOrStatic); - } else { - this.streamOrStatic = new ReadableStream({ - start(controller) { - controller.enqueue(chunkToU8(body)); - controller.close(); - }, - }); - } - } - return this.streamOrStatic; - } - - /** - * https://fetch.spec.whatwg.org/#body-unusable - * @returns {boolean} - */ - unusable() { - if ( - ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - this.streamOrStatic, - ) - ) { - return this.streamOrStatic.locked || - isReadableStreamDisturbed(this.streamOrStatic); + get stream() { + if ( + !ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + this.streamOrStatic, + ) + ) { + const { body, consumed } = this.streamOrStatic; + if (consumed) { + this.streamOrStatic = new ReadableStream(); + this.streamOrStatic.getReader(); + readableStreamDisturb(this.streamOrStatic); + readableStreamClose(this.streamOrStatic); + } else { + this.streamOrStatic = new ReadableStream({ + start(controller) { + controller.enqueue(chunkToU8(body)); + controller.close(); + }, + }); } - return this.streamOrStatic.consumed; } + return this.streamOrStatic; + } - /** - * @returns {boolean} - */ - consumed() { - if ( - ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - this.streamOrStatic, - ) - ) { - return isReadableStreamDisturbed(this.streamOrStatic); - } - return this.streamOrStatic.consumed; + /** + * https://fetch.spec.whatwg.org/#body-unusable + * @returns {boolean} + */ + unusable() { + if ( + ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + this.streamOrStatic, + ) + ) { + return this.streamOrStatic.locked || + isReadableStreamDisturbed(this.streamOrStatic); } + return this.streamOrStatic.consumed; + } - /** - * https://fetch.spec.whatwg.org/#concept-body-consume-body - * @returns {Promise<Uint8Array>} - */ - consume() { - if (this.unusable()) throw new TypeError("Body already consumed."); - if ( - ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - this.streamOrStatic, - ) - ) { - readableStreamThrowIfErrored(this.stream); - return readableStreamCollectIntoUint8Array(this.stream); - } else { - this.streamOrStatic.consumed = true; - return this.streamOrStatic.body; - } + /** + * @returns {boolean} + */ + consumed() { + if ( + ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + this.streamOrStatic, + ) + ) { + return isReadableStreamDisturbed(this.streamOrStatic); } + return this.streamOrStatic.consumed; + } - cancel(error) { - if ( - ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - this.streamOrStatic, - ) - ) { - this.streamOrStatic.cancel(error); - } else { - this.streamOrStatic.consumed = true; - } + /** + * https://fetch.spec.whatwg.org/#concept-body-consume-body + * @returns {Promise<Uint8Array>} + */ + consume() { + if (this.unusable()) throw new TypeError("Body already consumed."); + if ( + ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + this.streamOrStatic, + ) + ) { + readableStreamThrowIfErrored(this.stream); + return readableStreamCollectIntoUint8Array(this.stream); + } else { + this.streamOrStatic.consumed = true; + return this.streamOrStatic.body; } + } - error(error) { - if ( - ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - this.streamOrStatic, - ) - ) { - errorReadableStream(this.streamOrStatic, error); - } else { - this.streamOrStatic.consumed = true; - } + cancel(error) { + if ( + ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + this.streamOrStatic, + ) + ) { + this.streamOrStatic.cancel(error); + } else { + this.streamOrStatic.consumed = true; } + } - /** - * @returns {InnerBody} - */ - clone() { - const { 0: out1, 1: out2 } = this.stream.tee(); - this.streamOrStatic = out1; - const second = new InnerBody(out2); - second.source = core.deserialize(core.serialize(this.source)); - second.length = this.length; - return second; + error(error) { + if ( + ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + this.streamOrStatic, + ) + ) { + errorReadableStream(this.streamOrStatic, error); + } else { + this.streamOrStatic.consumed = true; } + } - /** - * @returns {InnerBody} - */ - createProxy() { - let proxyStreamOrStatic; - if ( - ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - this.streamOrStatic, - ) - ) { - proxyStreamOrStatic = createProxy(this.streamOrStatic); - } else { - proxyStreamOrStatic = { ...this.streamOrStatic }; - this.streamOrStatic.consumed = true; - } - const proxy = new InnerBody(proxyStreamOrStatic); - proxy.source = this.source; - proxy.length = this.length; - return proxy; - } + /** + * @returns {InnerBody} + */ + clone() { + const { 0: out1, 1: out2 } = this.stream.tee(); + this.streamOrStatic = out1; + const second = new InnerBody(out2); + second.source = core.deserialize(core.serialize(this.source)); + second.length = this.length; + return second; } /** - * @param {any} prototype - * @param {symbol} bodySymbol - * @param {symbol} mimeTypeSymbol - * @returns {void} + * @returns {InnerBody} */ - function mixinBody(prototype, bodySymbol, mimeTypeSymbol) { - async function consumeBody(object, type) { - webidl.assertBranded(object, prototype); + createProxy() { + let proxyStreamOrStatic; + if ( + ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + this.streamOrStatic, + ) + ) { + proxyStreamOrStatic = createProxy(this.streamOrStatic); + } else { + proxyStreamOrStatic = { ...this.streamOrStatic }; + this.streamOrStatic.consumed = true; + } + const proxy = new InnerBody(proxyStreamOrStatic); + proxy.source = this.source; + proxy.length = this.length; + return proxy; + } +} - const body = object[bodySymbol] !== null - ? await object[bodySymbol].consume() - : new Uint8Array(); +/** + * @param {any} prototype + * @param {symbol} bodySymbol + * @param {symbol} mimeTypeSymbol + * @returns {void} + */ +function mixinBody(prototype, bodySymbol, mimeTypeSymbol) { + async function consumeBody(object, type) { + webidl.assertBranded(object, prototype); - const mimeType = type === "Blob" || type === "FormData" - ? object[mimeTypeSymbol] - : null; - return packageData(body, type, mimeType); - } + const body = object[bodySymbol] !== null + ? await object[bodySymbol].consume() + : new Uint8Array(); - /** @type {PropertyDescriptorMap} */ - const mixin = { - body: { - /** - * @returns {ReadableStream<Uint8Array> | null} - */ - get() { - webidl.assertBranded(this, prototype); - if (this[bodySymbol] === null) { - return null; - } else { - return this[bodySymbol].stream; - } - }, - configurable: true, - enumerable: true, + const mimeType = type === "Blob" || type === "FormData" + ? object[mimeTypeSymbol] + : null; + return packageData(body, type, mimeType); + } + + /** @type {PropertyDescriptorMap} */ + const mixin = { + body: { + /** + * @returns {ReadableStream<Uint8Array> | null} + */ + get() { + webidl.assertBranded(this, prototype); + if (this[bodySymbol] === null) { + return null; + } else { + return this[bodySymbol].stream; + } }, - bodyUsed: { - /** - * @returns {boolean} - */ - get() { - webidl.assertBranded(this, prototype); - if (this[bodySymbol] !== null) { - return this[bodySymbol].consumed(); - } - return false; - }, - configurable: true, - enumerable: true, + configurable: true, + enumerable: true, + }, + bodyUsed: { + /** + * @returns {boolean} + */ + get() { + webidl.assertBranded(this, prototype); + if (this[bodySymbol] !== null) { + return this[bodySymbol].consumed(); + } + return false; }, - arrayBuffer: { - /** @returns {Promise<ArrayBuffer>} */ - value: function arrayBuffer() { - return consumeBody(this, "ArrayBuffer"); - }, - writable: true, - configurable: true, - enumerable: true, + configurable: true, + enumerable: true, + }, + arrayBuffer: { + /** @returns {Promise<ArrayBuffer>} */ + value: function arrayBuffer() { + return consumeBody(this, "ArrayBuffer"); }, - blob: { - /** @returns {Promise<Blob>} */ - value: function blob() { - return consumeBody(this, "Blob"); - }, - writable: true, - configurable: true, - enumerable: true, + writable: true, + configurable: true, + enumerable: true, + }, + blob: { + /** @returns {Promise<Blob>} */ + value: function blob() { + return consumeBody(this, "Blob"); }, - formData: { - /** @returns {Promise<FormData>} */ - value: function formData() { - return consumeBody(this, "FormData"); - }, - writable: true, - configurable: true, - enumerable: true, + writable: true, + configurable: true, + enumerable: true, + }, + formData: { + /** @returns {Promise<FormData>} */ + value: function formData() { + return consumeBody(this, "FormData"); }, - json: { - /** @returns {Promise<any>} */ - value: function json() { - return consumeBody(this, "JSON"); - }, - writable: true, - configurable: true, - enumerable: true, + writable: true, + configurable: true, + enumerable: true, + }, + json: { + /** @returns {Promise<any>} */ + value: function json() { + return consumeBody(this, "JSON"); }, - text: { - /** @returns {Promise<string>} */ - value: function text() { - return consumeBody(this, "text"); - }, - writable: true, - configurable: true, - enumerable: true, + writable: true, + configurable: true, + enumerable: true, + }, + text: { + /** @returns {Promise<string>} */ + value: function text() { + return consumeBody(this, "text"); }, - }; - return ObjectDefineProperties(prototype, mixin); - } + writable: true, + configurable: true, + enumerable: true, + }, + }; + return ObjectDefineProperties(prototype, mixin); +} - /** - * https://fetch.spec.whatwg.org/#concept-body-package-data - * @param {Uint8Array | string} bytes - * @param {"ArrayBuffer" | "Blob" | "FormData" | "JSON" | "text"} type - * @param {MimeType | null} [mimeType] - */ - function packageData(bytes, type, mimeType) { - switch (type) { - case "ArrayBuffer": - return chunkToU8(bytes).buffer; - case "Blob": - return new Blob([bytes], { - type: mimeType !== null ? mimesniff.serializeMimeType(mimeType) : "", - }); - case "FormData": { - if (mimeType !== null) { - const essence = mimesniff.essence(mimeType); - if (essence === "multipart/form-data") { - const boundary = mimeType.parameters.get("boundary"); - if (boundary === null) { - throw new TypeError( - "Missing boundary parameter in mime type of multipart formdata.", - ); - } - return parseFormData(chunkToU8(bytes), boundary); - } else if (essence === "application/x-www-form-urlencoded") { - // TODO(@AaronO): pass as-is with StringOrBuffer in op-layer - const entries = parseUrlEncoded(chunkToU8(bytes)); - return formDataFromEntries( - ArrayPrototypeMap( - entries, - (x) => ({ name: x[0], value: x[1] }), - ), +/** + * https://fetch.spec.whatwg.org/#concept-body-package-data + * @param {Uint8Array | string} bytes + * @param {"ArrayBuffer" | "Blob" | "FormData" | "JSON" | "text"} type + * @param {MimeType | null} [mimeType] + */ +function packageData(bytes, type, mimeType) { + switch (type) { + case "ArrayBuffer": + return chunkToU8(bytes).buffer; + case "Blob": + return new Blob([bytes], { + type: mimeType !== null ? mimesniff.serializeMimeType(mimeType) : "", + }); + case "FormData": { + if (mimeType !== null) { + const essence = mimesniff.essence(mimeType); + if (essence === "multipart/form-data") { + const boundary = mimeType.parameters.get("boundary"); + if (boundary === null) { + throw new TypeError( + "Missing boundary parameter in mime type of multipart formdata.", ); } - throw new TypeError("Body can not be decoded as form data"); + return parseFormData(chunkToU8(bytes), boundary); + } else if (essence === "application/x-www-form-urlencoded") { + // TODO(@AaronO): pass as-is with StringOrBuffer in op-layer + const entries = parseUrlEncoded(chunkToU8(bytes)); + return formDataFromEntries( + ArrayPrototypeMap( + entries, + (x) => ({ name: x[0], value: x[1] }), + ), + ); } - throw new TypeError("Missing content type"); + throw new TypeError("Body can not be decoded as form data"); } - case "JSON": - return JSONParse(chunkToString(bytes)); - case "text": - return chunkToString(bytes); + throw new TypeError("Missing content type"); } + case "JSON": + return JSONParse(chunkToString(bytes)); + case "text": + return chunkToString(bytes); } +} - /** - * @param {BodyInit} object - * @returns {{body: InnerBody, contentType: string | null}} - */ - function extractBody(object) { - /** @type {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} */ - let stream; - let source = null; - let length = null; - let contentType = null; - if (typeof object === "string") { - source = object; - contentType = "text/plain;charset=UTF-8"; - } else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, object)) { - stream = object.stream(); - source = object; - length = object.size; - if (object.type.length !== 0) { - contentType = object.type; - } - } else if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, object)) { - // Fast(er) path for common case of Uint8Array - const copy = TypedArrayPrototypeSlice(object, 0, object.byteLength); - source = copy; - } else if ( - ArrayBufferIsView(object) || - ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, object) - ) { - const u8 = ArrayBufferIsView(object) - ? new Uint8Array( - object.buffer, - object.byteOffset, - object.byteLength, - ) - : new Uint8Array(object); - const copy = TypedArrayPrototypeSlice(u8, 0, u8.byteLength); - source = copy; - } else if (ObjectPrototypeIsPrototypeOf(FormDataPrototype, object)) { - const res = formDataToBlob(object); - stream = res.stream(); - source = res; - length = res.size; - contentType = res.type; - } else if ( - ObjectPrototypeIsPrototypeOf(URLSearchParamsPrototype, object) - ) { - // TODO(@satyarohith): not sure what primordial here. - source = object.toString(); - contentType = "application/x-www-form-urlencoded;charset=UTF-8"; - } else if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, object)) { - stream = object; - if (object.locked || isReadableStreamDisturbed(object)) { - throw new TypeError("ReadableStream is locked or disturbed"); - } +/** + * @param {BodyInit} object + * @returns {{body: InnerBody, contentType: string | null}} + */ +function extractBody(object) { + /** @type {ReadableStream<Uint8Array> | { body: Uint8Array | string, consumed: boolean }} */ + let stream; + let source = null; + let length = null; + let contentType = null; + if (typeof object === "string") { + source = object; + contentType = "text/plain;charset=UTF-8"; + } else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, object)) { + stream = object.stream(); + source = object; + length = object.size; + if (object.type.length !== 0) { + contentType = object.type; } - if (typeof source === "string") { - // WARNING: this deviates from spec (expects length to be set) - // https://fetch.spec.whatwg.org/#bodyinit > 7. - // no observable side-effect for users so far, but could change - stream = { body: source, consumed: false }; - length = null; // NOTE: string length != byte length - } else if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, source)) { - stream = { body: source, consumed: false }; - length = source.byteLength; + } else if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, object)) { + // Fast(er) path for common case of Uint8Array + const copy = TypedArrayPrototypeSlice(object, 0, object.byteLength); + source = copy; + } else if ( + ArrayBufferIsView(object) || + ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, object) + ) { + const u8 = ArrayBufferIsView(object) + ? new Uint8Array( + object.buffer, + object.byteOffset, + object.byteLength, + ) + : new Uint8Array(object); + const copy = TypedArrayPrototypeSlice(u8, 0, u8.byteLength); + source = copy; + } else if (ObjectPrototypeIsPrototypeOf(FormDataPrototype, object)) { + const res = formDataToBlob(object); + stream = res.stream(); + source = res; + length = res.size; + contentType = res.type; + } else if ( + ObjectPrototypeIsPrototypeOf(URLSearchParamsPrototype, object) + ) { + // TODO(@satyarohith): not sure what primordial here. + source = object.toString(); + contentType = "application/x-www-form-urlencoded;charset=UTF-8"; + } else if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, object)) { + stream = object; + if (object.locked || isReadableStreamDisturbed(object)) { + throw new TypeError("ReadableStream is locked or disturbed"); } - const body = new InnerBody(stream); - body.source = source; - body.length = length; - return { body, contentType }; } + if (typeof source === "string") { + // WARNING: this deviates from spec (expects length to be set) + // https://fetch.spec.whatwg.org/#bodyinit > 7. + // no observable side-effect for users so far, but could change + stream = { body: source, consumed: false }; + length = null; // NOTE: string length != byte length + } else if (ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, source)) { + stream = { body: source, consumed: false }; + length = source.byteLength; + } + const body = new InnerBody(stream); + body.source = source; + body.length = length; + return { body, contentType }; +} - webidl.converters["BodyInit_DOMString"] = (V, opts) => { - // Union for (ReadableStream or Blob or ArrayBufferView or ArrayBuffer or FormData or URLSearchParams or USVString) - if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, V)) { - return webidl.converters["ReadableStream"](V, opts); - } else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, V)) { - return webidl.converters["Blob"](V, opts); - } else if (ObjectPrototypeIsPrototypeOf(FormDataPrototype, V)) { - return webidl.converters["FormData"](V, opts); - } else if (ObjectPrototypeIsPrototypeOf(URLSearchParamsPrototype, V)) { - return webidl.converters["URLSearchParams"](V, opts); +webidl.converters["BodyInit_DOMString"] = (V, opts) => { + // Union for (ReadableStream or Blob or ArrayBufferView or ArrayBuffer or FormData or URLSearchParams or USVString) + if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, V)) { + return webidl.converters["ReadableStream"](V, opts); + } else if (ObjectPrototypeIsPrototypeOf(BlobPrototype, V)) { + return webidl.converters["Blob"](V, opts); + } else if (ObjectPrototypeIsPrototypeOf(FormDataPrototype, V)) { + return webidl.converters["FormData"](V, opts); + } else if (ObjectPrototypeIsPrototypeOf(URLSearchParamsPrototype, V)) { + return webidl.converters["URLSearchParams"](V, opts); + } + if (typeof V === "object") { + if ( + ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V) || + // deno-lint-ignore prefer-primordials + ObjectPrototypeIsPrototypeOf(SharedArrayBuffer.prototype, V) + ) { + return webidl.converters["ArrayBuffer"](V, opts); } - if (typeof V === "object") { - if ( - ObjectPrototypeIsPrototypeOf(ArrayBufferPrototype, V) || - // deno-lint-ignore prefer-primordials - ObjectPrototypeIsPrototypeOf(SharedArrayBuffer.prototype, V) - ) { - return webidl.converters["ArrayBuffer"](V, opts); - } - if (ArrayBufferIsView(V)) { - return webidl.converters["ArrayBufferView"](V, opts); - } + if (ArrayBufferIsView(V)) { + return webidl.converters["ArrayBufferView"](V, opts); } - // BodyInit conversion is passed to extractBody(), which calls core.encode(). - // core.encode() will UTF-8 encode strings with replacement, being equivalent to the USV normalization. - // Therefore we can convert to DOMString instead of USVString and avoid a costly redundant conversion. - return webidl.converters["DOMString"](V, opts); - }; - webidl.converters["BodyInit_DOMString?"] = webidl.createNullableConverter( - webidl.converters["BodyInit_DOMString"], - ); + } + // BodyInit conversion is passed to extractBody(), which calls core.encode(). + // core.encode() will UTF-8 encode strings with replacement, being equivalent to the USV normalization. + // Therefore we can convert to DOMString instead of USVString and avoid a costly redundant conversion. + return webidl.converters["DOMString"](V, opts); +}; +webidl.converters["BodyInit_DOMString?"] = webidl.createNullableConverter( + webidl.converters["BodyInit_DOMString"], +); - window.__bootstrap.fetchBody = { mixinBody, InnerBody, extractBody }; -})(globalThis); +export { extractBody, InnerBody, mixinBody }; diff --git a/ext/fetch/22_http_client.js b/ext/fetch/22_http_client.js index 7b9f5c446..9d37f1b7f 100644 --- a/ext/fetch/22_http_client.js +++ b/ext/fetch/22_http_client.js @@ -9,40 +9,34 @@ /// <reference path="../web/06_streams_types.d.ts" /> /// <reference path="./lib.deno_fetch.d.ts" /> /// <reference lib="esnext" /> -"use strict"; -((window) => { - const core = window.Deno.core; - const ops = core.ops; +const core = globalThis.Deno.core; +const ops = core.ops; +/** + * @param {Deno.CreateHttpClientOptions} options + * @returns {HttpClient} + */ +function createHttpClient(options) { + options.caCerts ??= []; + return new HttpClient( + ops.op_fetch_custom_client( + options, + ), + ); +} + +class HttpClient { /** - * @param {Deno.CreateHttpClientOptions} options - * @returns {HttpClient} + * @param {number} rid */ - function createHttpClient(options) { - options.caCerts ??= []; - return new HttpClient( - ops.op_fetch_custom_client( - options, - ), - ); + constructor(rid) { + this.rid = rid; } - - class HttpClient { - /** - * @param {number} rid - */ - constructor(rid) { - this.rid = rid; - } - close() { - core.close(this.rid); - } + close() { + core.close(this.rid); } - const HttpClientPrototype = HttpClient.prototype; +} +const HttpClientPrototype = HttpClient.prototype; - window.__bootstrap.fetch ??= {}; - window.__bootstrap.fetch.createHttpClient = createHttpClient; - window.__bootstrap.fetch.HttpClient = HttpClient; - window.__bootstrap.fetch.HttpClientPrototype = HttpClientPrototype; -})(globalThis); +export { createHttpClient, HttpClient, HttpClientPrototype }; diff --git a/ext/fetch/23_request.js b/ext/fetch/23_request.js index e266a7e44..1e8d5c1ec 100644 --- a/ext/fetch/23_request.js +++ b/ext/fetch/23_request.js @@ -8,657 +8,664 @@ /// <reference path="../web/06_streams_types.d.ts" /> /// <reference path="./lib.deno_fetch.d.ts" /> /// <reference lib="esnext" /> -"use strict"; - -((window) => { - const webidl = window.__bootstrap.webidl; - const consoleInternal = window.__bootstrap.console; - const { HTTP_TOKEN_CODE_POINT_RE, byteUpperCase } = window.__bootstrap.infra; - const { URL } = window.__bootstrap.url; - const { guardFromHeaders } = window.__bootstrap.headers; - const { mixinBody, extractBody, InnerBody } = window.__bootstrap.fetchBody; - const { getLocationHref } = window.__bootstrap.location; - const { extractMimeType } = window.__bootstrap.mimesniff; - const { blobFromObjectUrl } = window.__bootstrap.file; - const { - headersFromHeaderList, - headerListFromHeaders, - fillHeaders, - getDecodeSplitHeader, - } = window.__bootstrap.headers; - const { HttpClientPrototype } = window.__bootstrap.fetch; - const abortSignal = window.__bootstrap.abortSignal; - const { - ArrayPrototypeMap, - ArrayPrototypeSlice, - ArrayPrototypeSplice, - ObjectKeys, - ObjectPrototypeIsPrototypeOf, - RegExpPrototypeTest, - Symbol, - SymbolFor, - TypeError, - } = window.__bootstrap.primordials; - - const _request = Symbol("request"); - const _headers = Symbol("headers"); - const _getHeaders = Symbol("get headers"); - const _headersCache = Symbol("headers cache"); - const _signal = Symbol("signal"); - const _mimeType = Symbol("mime type"); - const _body = Symbol("body"); - const _flash = Symbol("flash"); - const _url = Symbol("url"); - const _method = Symbol("method"); - /** - * @param {(() => string)[]} urlList - * @param {string[]} urlListProcessed - */ - function processUrlList(urlList, urlListProcessed) { - for (let i = 0; i < urlList.length; i++) { - if (urlListProcessed[i] === undefined) { - urlListProcessed[i] = urlList[i](); - } +import * as webidl from "internal:ext/webidl/00_webidl.js"; +import { createFilteredInspectProxy } from "internal:ext/console/02_console.js"; +import { + byteUpperCase, + HTTP_TOKEN_CODE_POINT_RE, +} from "internal:ext/web/00_infra.js"; +import { URL } from "internal:ext/url/00_url.js"; +import { + extractBody, + InnerBody, + mixinBody, +} from "internal:ext/fetch/22_body.js"; +import { getLocationHref } from "internal:ext/web/12_location.js"; +import { extractMimeType } from "internal:ext/web/01_mimesniff.js"; +import { blobFromObjectUrl } from "internal:ext/web/09_file.js"; +import { + fillHeaders, + getDecodeSplitHeader, + guardFromHeaders, + headerListFromHeaders, + headersFromHeaderList, +} from "internal:ext/fetch/20_headers.js"; +import { HttpClientPrototype } from "internal:ext/fetch/22_http_client.js"; +import * as abortSignal from "internal:ext/web/03_abort_signal.js"; +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayPrototypeMap, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + ObjectKeys, + ObjectPrototypeIsPrototypeOf, + RegExpPrototypeTest, + Symbol, + SymbolFor, + TypeError, +} = primordials; + +const _request = Symbol("request"); +const _headers = Symbol("headers"); +const _getHeaders = Symbol("get headers"); +const _headersCache = Symbol("headers cache"); +const _signal = Symbol("signal"); +const _mimeType = Symbol("mime type"); +const _body = Symbol("body"); +const _flash = Symbol("flash"); +const _url = Symbol("url"); +const _method = Symbol("method"); + +/** + * @param {(() => string)[]} urlList + * @param {string[]} urlListProcessed + */ +function processUrlList(urlList, urlListProcessed) { + for (let i = 0; i < urlList.length; i++) { + if (urlListProcessed[i] === undefined) { + urlListProcessed[i] = urlList[i](); } - return urlListProcessed; } + return urlListProcessed; +} + +/** + * @typedef InnerRequest + * @property {() => string} method + * @property {() => string} url + * @property {() => string} currentUrl + * @property {() => [string, string][]} headerList + * @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body + * @property {"follow" | "error" | "manual"} redirectMode + * @property {number} redirectCount + * @property {(() => string)[]} urlList + * @property {string[]} urlListProcessed + * @property {number | null} clientRid NOTE: non standard extension for `Deno.HttpClient`. + * @property {Blob | null} blobUrlEntry + */ + +/** + * @param {() => string} method + * @param {string | () => string} url + * @param {() => [string, string][]} headerList + * @param {typeof __window.bootstrap.fetchBody.InnerBody} body + * @param {boolean} maybeBlob + * @returns {InnerRequest} + */ +function newInnerRequest(method, url, headerList, body, maybeBlob) { + let blobUrlEntry = null; + if (maybeBlob && typeof url === "string" && url.startsWith("blob:")) { + blobUrlEntry = blobFromObjectUrl(url); + } + return { + methodInner: null, + get method() { + if (this.methodInner === null) { + try { + this.methodInner = method(); + } catch { + throw new TypeError("cannot read method: request closed"); + } + } + return this.methodInner; + }, + set method(value) { + this.methodInner = value; + }, + headerListInner: null, + get headerList() { + if (this.headerListInner === null) { + try { + this.headerListInner = headerList(); + } catch { + throw new TypeError("cannot read headers: request closed"); + } + } + return this.headerListInner; + }, + set headerList(value) { + this.headerListInner = value; + }, + body, + redirectMode: "follow", + redirectCount: 0, + urlList: [typeof url === "string" ? () => url : url], + urlListProcessed: [], + clientRid: null, + blobUrlEntry, + url() { + if (this.urlListProcessed[0] === undefined) { + try { + this.urlListProcessed[0] = this.urlList[0](); + } catch { + throw new TypeError("cannot read url: request closed"); + } + } + return this.urlListProcessed[0]; + }, + currentUrl() { + const currentIndex = this.urlList.length - 1; + if (this.urlListProcessed[currentIndex] === undefined) { + try { + this.urlListProcessed[currentIndex] = this.urlList[currentIndex](); + } catch { + throw new TypeError("cannot read url: request closed"); + } + } + return this.urlListProcessed[currentIndex]; + }, + }; +} + +/** + * https://fetch.spec.whatwg.org/#concept-request-clone + * @param {InnerRequest} request + * @param {boolean} skipBody + * @param {boolean} flash + * @returns {InnerRequest} + */ +function cloneInnerRequest(request, skipBody = false, flash = false) { + const headerList = ArrayPrototypeMap( + request.headerList, + (x) => [x[0], x[1]], + ); - /** - * @typedef InnerRequest - * @property {() => string} method - * @property {() => string} url - * @property {() => string} currentUrl - * @property {() => [string, string][]} headerList - * @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body - * @property {"follow" | "error" | "manual"} redirectMode - * @property {number} redirectCount - * @property {(() => string)[]} urlList - * @property {string[]} urlListProcessed - * @property {number | null} clientRid NOTE: non standard extension for `Deno.HttpClient`. - * @property {Blob | null} blobUrlEntry - */ + let body = null; + if (request.body !== null && !skipBody) { + body = request.body.clone(); + } - /** - * @param {() => string} method - * @param {string | () => string} url - * @param {() => [string, string][]} headerList - * @param {typeof __window.bootstrap.fetchBody.InnerBody} body - * @param {boolean} maybeBlob - * @returns {InnerRequest} - */ - function newInnerRequest(method, url, headerList, body, maybeBlob) { - let blobUrlEntry = null; - if (maybeBlob && typeof url === "string" && url.startsWith("blob:")) { - blobUrlEntry = blobFromObjectUrl(url); - } + if (flash) { return { - methodInner: null, - get method() { - if (this.methodInner === null) { - try { - this.methodInner = method(); - } catch { - throw new TypeError("cannot read method: request closed"); - } - } - return this.methodInner; - }, - set method(value) { - this.methodInner = value; - }, - headerListInner: null, - get headerList() { - if (this.headerListInner === null) { - try { - this.headerListInner = headerList(); - } catch { - throw new TypeError("cannot read headers: request closed"); - } - } - return this.headerListInner; - }, - set headerList(value) { - this.headerListInner = value; - }, body, + methodCb: request.methodCb, + urlCb: request.urlCb, + headerList: request.headerList, + streamRid: request.streamRid, + serverId: request.serverId, redirectMode: "follow", redirectCount: 0, - urlList: [typeof url === "string" ? () => url : url], - urlListProcessed: [], - clientRid: null, - blobUrlEntry, - url() { - if (this.urlListProcessed[0] === undefined) { - try { - this.urlListProcessed[0] = this.urlList[0](); - } catch { - throw new TypeError("cannot read url: request closed"); - } - } - return this.urlListProcessed[0]; - }, - currentUrl() { - const currentIndex = this.urlList.length - 1; - if (this.urlListProcessed[currentIndex] === undefined) { - try { - this.urlListProcessed[currentIndex] = this.urlList[currentIndex](); - } catch { - throw new TypeError("cannot read url: request closed"); - } - } - return this.urlListProcessed[currentIndex]; - }, }; } - /** - * https://fetch.spec.whatwg.org/#concept-request-clone - * @param {InnerRequest} request - * @param {boolean} skipBody - * @param {boolean} flash - * @returns {InnerRequest} - */ - function cloneInnerRequest(request, skipBody = false, flash = false) { - const headerList = ArrayPrototypeMap( - request.headerList, - (x) => [x[0], x[1]], - ); - - let body = null; - if (request.body !== null && !skipBody) { - body = request.body.clone(); - } - - if (flash) { - return { - body, - methodCb: request.methodCb, - urlCb: request.urlCb, - headerList: request.headerList, - streamRid: request.streamRid, - serverId: request.serverId, - redirectMode: "follow", - redirectCount: 0, - }; - } - - return { - method: request.method, - headerList, - body, - redirectMode: request.redirectMode, - redirectCount: request.redirectCount, - urlList: request.urlList, - urlListProcessed: request.urlListProcessed, - clientRid: request.clientRid, - blobUrlEntry: request.blobUrlEntry, - url() { - if (this.urlListProcessed[0] === undefined) { - try { - this.urlListProcessed[0] = this.urlList[0](); - } catch { - throw new TypeError("cannot read url: request closed"); - } + return { + method: request.method, + headerList, + body, + redirectMode: request.redirectMode, + redirectCount: request.redirectCount, + urlList: request.urlList, + urlListProcessed: request.urlListProcessed, + clientRid: request.clientRid, + blobUrlEntry: request.blobUrlEntry, + url() { + if (this.urlListProcessed[0] === undefined) { + try { + this.urlListProcessed[0] = this.urlList[0](); + } catch { + throw new TypeError("cannot read url: request closed"); } - return this.urlListProcessed[0]; - }, - currentUrl() { - const currentIndex = this.urlList.length - 1; - if (this.urlListProcessed[currentIndex] === undefined) { - try { - this.urlListProcessed[currentIndex] = this.urlList[currentIndex](); - } catch { - throw new TypeError("cannot read url: request closed"); - } + } + return this.urlListProcessed[0]; + }, + currentUrl() { + const currentIndex = this.urlList.length - 1; + if (this.urlListProcessed[currentIndex] === undefined) { + try { + this.urlListProcessed[currentIndex] = this.urlList[currentIndex](); + } catch { + throw new TypeError("cannot read url: request closed"); } - return this.urlListProcessed[currentIndex]; - }, - }; + } + return this.urlListProcessed[currentIndex]; + }, + }; +} + +/** + * @param {string} m + * @returns {boolean} + */ +function isKnownMethod(m) { + return ( + m === "DELETE" || + m === "GET" || + m === "HEAD" || + m === "OPTIONS" || + m === "POST" || + m === "PUT" + ); +} +/** + * @param {string} m + * @returns {string} + */ +function validateAndNormalizeMethod(m) { + // Fast path for well-known methods + if (isKnownMethod(m)) { + return m; } - /** - * @param {string} m - * @returns {boolean} - */ - function isKnownMethod(m) { - return ( - m === "DELETE" || - m === "GET" || - m === "HEAD" || - m === "OPTIONS" || - m === "POST" || - m === "PUT" - ); + // Regular path + if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, m)) { + throw new TypeError("Method is not valid."); } - /** - * @param {string} m - * @returns {string} - */ - function validateAndNormalizeMethod(m) { - // Fast path for well-known methods - if (isKnownMethod(m)) { - return m; + const upperCase = byteUpperCase(m); + if ( + upperCase === "CONNECT" || upperCase === "TRACE" || upperCase === "TRACK" + ) { + throw new TypeError("Method is forbidden."); + } + return upperCase; +} + +class Request { + /** @type {InnerRequest} */ + [_request]; + /** @type {Headers} */ + [_headersCache]; + [_getHeaders]; + + /** @type {Headers} */ + get [_headers]() { + if (this[_headersCache] === undefined) { + this[_headersCache] = this[_getHeaders](); } + return this[_headersCache]; + } - // Regular path - if (!RegExpPrototypeTest(HTTP_TOKEN_CODE_POINT_RE, m)) { - throw new TypeError("Method is not valid."); - } - const upperCase = byteUpperCase(m); - if ( - upperCase === "CONNECT" || upperCase === "TRACE" || upperCase === "TRACK" - ) { - throw new TypeError("Method is forbidden."); + set [_headers](value) { + this[_headersCache] = value; + } + + /** @type {AbortSignal} */ + [_signal]; + get [_mimeType]() { + const values = getDecodeSplitHeader( + headerListFromHeaders(this[_headers]), + "Content-Type", + ); + return extractMimeType(values); + } + get [_body]() { + if (this[_flash]) { + return this[_flash].body; + } else { + return this[_request].body; } - return upperCase; } - class Request { + /** + * https://fetch.spec.whatwg.org/#dom-request + * @param {RequestInfo} input + * @param {RequestInit} init + */ + constructor(input, init = {}) { + const prefix = "Failed to construct 'Request'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + input = webidl.converters["RequestInfo_DOMString"](input, { + prefix, + context: "Argument 1", + }); + init = webidl.converters["RequestInit"](init, { + prefix, + context: "Argument 2", + }); + + this[webidl.brand] = webidl.brand; + /** @type {InnerRequest} */ - [_request]; - /** @type {Headers} */ - [_headersCache]; - [_getHeaders]; - - /** @type {Headers} */ - get [_headers]() { - if (this[_headersCache] === undefined) { - this[_headersCache] = this[_getHeaders](); + let request; + const baseURL = getLocationHref(); + + // 4. + let signal = null; + + // 5. + if (typeof input === "string") { + const parsedURL = new URL(input, baseURL); + request = newInnerRequest( + () => "GET", + parsedURL.href, + () => [], + null, + true, + ); + } else { // 6. + if (!ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) { + throw new TypeError("Unreachable"); } - return this[_headersCache]; + const originalReq = input[_request]; + // fold in of step 12 from below + request = cloneInnerRequest(originalReq, true); + request.redirectCount = 0; // reset to 0 - cloneInnerRequest copies the value + signal = input[_signal]; } - set [_headers](value) { - this[_headersCache] = value; + // 12. is folded into the else statement of step 6 above. + + // 22. + if (init.redirect !== undefined) { + request.redirectMode = init.redirect; } - /** @type {AbortSignal} */ - [_signal]; - get [_mimeType]() { - const values = getDecodeSplitHeader( - headerListFromHeaders(this[_headers]), - "Content-Type", - ); - return extractMimeType(values); + // 25. + if (init.method !== undefined) { + let method = init.method; + method = validateAndNormalizeMethod(method); + request.method = method; } - get [_body]() { - if (this[_flash]) { - return this[_flash].body; - } else { - return this[_request].body; - } + + // 26. + if (init.signal !== undefined) { + signal = init.signal; } - /** - * https://fetch.spec.whatwg.org/#dom-request - * @param {RequestInfo} input - * @param {RequestInit} init - */ - constructor(input, init = {}) { - const prefix = "Failed to construct 'Request'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); - input = webidl.converters["RequestInfo_DOMString"](input, { - prefix, - context: "Argument 1", - }); - init = webidl.converters["RequestInit"](init, { - prefix, - context: "Argument 2", - }); - - this[webidl.brand] = webidl.brand; - - /** @type {InnerRequest} */ - let request; - const baseURL = getLocationHref(); - - // 4. - let signal = null; - - // 5. - if (typeof input === "string") { - const parsedURL = new URL(input, baseURL); - request = newInnerRequest( - () => "GET", - parsedURL.href, - () => [], - null, - true, + // NOTE: non standard extension. This handles Deno.HttpClient parameter + if (init.client !== undefined) { + if ( + init.client !== null && + !ObjectPrototypeIsPrototypeOf(HttpClientPrototype, init.client) + ) { + throw webidl.makeException( + TypeError, + "`client` must be a Deno.HttpClient", + { prefix, context: "Argument 2" }, ); - } else { // 6. - if (!ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) { - throw new TypeError("Unreachable"); - } - const originalReq = input[_request]; - // fold in of step 12 from below - request = cloneInnerRequest(originalReq, true); - request.redirectCount = 0; // reset to 0 - cloneInnerRequest copies the value - signal = input[_signal]; } + request.clientRid = init.client?.rid ?? null; + } - // 12. is folded into the else statement of step 6 above. + // 27. + this[_request] = request; - // 22. - if (init.redirect !== undefined) { - request.redirectMode = init.redirect; - } + // 28. + this[_signal] = abortSignal.newSignal(); - // 25. - if (init.method !== undefined) { - let method = init.method; - method = validateAndNormalizeMethod(method); - request.method = method; - } + // 29. + if (signal !== null) { + abortSignal.follow(this[_signal], signal); + } - // 26. - if (init.signal !== undefined) { - signal = init.signal; - } + // 30. + this[_headers] = headersFromHeaderList(request.headerList, "request"); - // NOTE: non standard extension. This handles Deno.HttpClient parameter - if (init.client !== undefined) { - if ( - init.client !== null && - !ObjectPrototypeIsPrototypeOf(HttpClientPrototype, init.client) - ) { - throw webidl.makeException( - TypeError, - "`client` must be a Deno.HttpClient", - { prefix, context: "Argument 2" }, - ); - } - request.clientRid = init.client?.rid ?? null; + // 32. + if (ObjectKeys(init).length > 0) { + let headers = ArrayPrototypeSlice( + headerListFromHeaders(this[_headers]), + 0, + headerListFromHeaders(this[_headers]).length, + ); + if (init.headers !== undefined) { + headers = init.headers; } + ArrayPrototypeSplice( + headerListFromHeaders(this[_headers]), + 0, + headerListFromHeaders(this[_headers]).length, + ); + fillHeaders(this[_headers], headers); + } - // 27. - this[_request] = request; - - // 28. - this[_signal] = abortSignal.newSignal(); - - // 29. - if (signal !== null) { - abortSignal.follow(this[_signal], signal); - } + // 33. + let inputBody = null; + if (ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) { + inputBody = input[_body]; + } - // 30. - this[_headers] = headersFromHeaderList(request.headerList, "request"); + // 34. + if ( + (request.method === "GET" || request.method === "HEAD") && + ((init.body !== undefined && init.body !== null) || + inputBody !== null) + ) { + throw new TypeError("Request with GET/HEAD method cannot have body."); + } - // 32. - if (ObjectKeys(init).length > 0) { - let headers = ArrayPrototypeSlice( - headerListFromHeaders(this[_headers]), - 0, - headerListFromHeaders(this[_headers]).length, - ); - if (init.headers !== undefined) { - headers = init.headers; - } - ArrayPrototypeSplice( - headerListFromHeaders(this[_headers]), - 0, - headerListFromHeaders(this[_headers]).length, - ); - fillHeaders(this[_headers], headers); - } + // 35. + let initBody = null; - // 33. - let inputBody = null; - if (ObjectPrototypeIsPrototypeOf(RequestPrototype, input)) { - inputBody = input[_body]; + // 36. + if (init.body !== undefined && init.body !== null) { + const res = extractBody(init.body); + initBody = res.body; + if (res.contentType !== null && !this[_headers].has("content-type")) { + this[_headers].append("Content-Type", res.contentType); } + } - // 34. - if ( - (request.method === "GET" || request.method === "HEAD") && - ((init.body !== undefined && init.body !== null) || - inputBody !== null) - ) { - throw new TypeError("Request with GET/HEAD method cannot have body."); - } + // 37. + const inputOrInitBody = initBody ?? inputBody; - // 35. - let initBody = null; + // 39. + let finalBody = inputOrInitBody; - // 36. - if (init.body !== undefined && init.body !== null) { - const res = extractBody(init.body); - initBody = res.body; - if (res.contentType !== null && !this[_headers].has("content-type")) { - this[_headers].append("Content-Type", res.contentType); - } + // 40. + if (initBody === null && inputBody !== null) { + if (input[_body] && input[_body].unusable()) { + throw new TypeError("Input request's body is unusable."); } + finalBody = inputBody.createProxy(); + } - // 37. - const inputOrInitBody = initBody ?? inputBody; - - // 39. - let finalBody = inputOrInitBody; - - // 40. - if (initBody === null && inputBody !== null) { - if (input[_body] && input[_body].unusable()) { - throw new TypeError("Input request's body is unusable."); - } - finalBody = inputBody.createProxy(); - } + // 41. + request.body = finalBody; + } - // 41. - request.body = finalBody; + get method() { + webidl.assertBranded(this, RequestPrototype); + if (this[_method]) { + return this[_method]; } - - get method() { - webidl.assertBranded(this, RequestPrototype); - if (this[_method]) { - return this[_method]; - } - if (this[_flash]) { - this[_method] = this[_flash].methodCb(); - return this[_method]; - } else { - this[_method] = this[_request].method; - return this[_method]; - } + if (this[_flash]) { + this[_method] = this[_flash].methodCb(); + return this[_method]; + } else { + this[_method] = this[_request].method; + return this[_method]; } + } - get url() { - webidl.assertBranded(this, RequestPrototype); - if (this[_url]) { - return this[_url]; - } - - if (this[_flash]) { - this[_url] = this[_flash].urlCb(); - return this[_url]; - } else { - this[_url] = this[_request].url(); - return this[_url]; - } + get url() { + webidl.assertBranded(this, RequestPrototype); + if (this[_url]) { + return this[_url]; } - get headers() { - webidl.assertBranded(this, RequestPrototype); - return this[_headers]; + if (this[_flash]) { + this[_url] = this[_flash].urlCb(); + return this[_url]; + } else { + this[_url] = this[_request].url(); + return this[_url]; } + } - get redirect() { - webidl.assertBranded(this, RequestPrototype); - if (this[_flash]) { - return this[_flash].redirectMode; - } - return this[_request].redirectMode; - } + get headers() { + webidl.assertBranded(this, RequestPrototype); + return this[_headers]; + } - get signal() { - webidl.assertBranded(this, RequestPrototype); - return this[_signal]; + get redirect() { + webidl.assertBranded(this, RequestPrototype); + if (this[_flash]) { + return this[_flash].redirectMode; } + return this[_request].redirectMode; + } - clone() { - webidl.assertBranded(this, RequestPrototype); - if (this[_body] && this[_body].unusable()) { - throw new TypeError("Body is unusable."); - } - let newReq; - if (this[_flash]) { - newReq = cloneInnerRequest(this[_flash], false, true); - } else { - newReq = cloneInnerRequest(this[_request]); - } - const newSignal = abortSignal.newSignal(); + get signal() { + webidl.assertBranded(this, RequestPrototype); + return this[_signal]; + } - if (this[_signal]) { - abortSignal.follow(newSignal, this[_signal]); - } + clone() { + webidl.assertBranded(this, RequestPrototype); + if (this[_body] && this[_body].unusable()) { + throw new TypeError("Body is unusable."); + } + let newReq; + if (this[_flash]) { + newReq = cloneInnerRequest(this[_flash], false, true); + } else { + newReq = cloneInnerRequest(this[_request]); + } + const newSignal = abortSignal.newSignal(); - if (this[_flash]) { - return fromInnerRequest( - newReq, - newSignal, - guardFromHeaders(this[_headers]), - true, - ); - } + if (this[_signal]) { + abortSignal.follow(newSignal, this[_signal]); + } + if (this[_flash]) { return fromInnerRequest( newReq, newSignal, guardFromHeaders(this[_headers]), - false, + true, ); } - [SymbolFor("Deno.customInspect")](inspect) { - return inspect(consoleInternal.createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(RequestPrototype, this), - keys: [ - "bodyUsed", - "headers", - "method", - "redirect", - "url", - ], - })); - } + return fromInnerRequest( + newReq, + newSignal, + guardFromHeaders(this[_headers]), + false, + ); } - webidl.configurePrototype(Request); - const RequestPrototype = Request.prototype; - mixinBody(RequestPrototype, _body, _mimeType); - - webidl.converters["Request"] = webidl.createInterfaceConverter( - "Request", - RequestPrototype, - ); - webidl.converters["RequestInfo_DOMString"] = (V, opts) => { - // Union for (Request or USVString) - if (typeof V == "object") { - if (ObjectPrototypeIsPrototypeOf(RequestPrototype, V)) { - return webidl.converters["Request"](V, opts); - } - } - // Passed to new URL(...) which implicitly converts DOMString -> USVString - return webidl.converters["DOMString"](V, opts); - }; - webidl.converters["RequestRedirect"] = webidl.createEnumConverter( - "RequestRedirect", - [ - "follow", - "error", - "manual", - ], - ); - webidl.converters["RequestInit"] = webidl.createDictionaryConverter( - "RequestInit", - [ - { key: "method", converter: webidl.converters["ByteString"] }, - { key: "headers", converter: webidl.converters["HeadersInit"] }, - { - key: "body", - converter: webidl.createNullableConverter( - webidl.converters["BodyInit_DOMString"], - ), - }, - { key: "redirect", converter: webidl.converters["RequestRedirect"] }, - { - key: "signal", - converter: webidl.createNullableConverter( - webidl.converters["AbortSignal"], - ), - }, - { key: "client", converter: webidl.converters.any }, - ], - ); - - /** - * @param {Request} request - * @returns {InnerRequest} - */ - function toInnerRequest(request) { - return request[_request]; + [SymbolFor("Deno.customInspect")](inspect) { + return inspect(createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(RequestPrototype, this), + keys: [ + "bodyUsed", + "headers", + "method", + "redirect", + "url", + ], + })); } - - /** - * @param {InnerRequest} inner - * @param {AbortSignal} signal - * @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard - * @param {boolean} flash - * @returns {Request} - */ - function fromInnerRequest(inner, signal, guard, flash) { - const request = webidl.createBranded(Request); - if (flash) { - request[_flash] = inner; - } else { - request[_request] = inner; +} + +webidl.configurePrototype(Request); +const RequestPrototype = Request.prototype; +mixinBody(RequestPrototype, _body, _mimeType); + +webidl.converters["Request"] = webidl.createInterfaceConverter( + "Request", + RequestPrototype, +); +webidl.converters["RequestInfo_DOMString"] = (V, opts) => { + // Union for (Request or USVString) + if (typeof V == "object") { + if (ObjectPrototypeIsPrototypeOf(RequestPrototype, V)) { + return webidl.converters["Request"](V, opts); } - request[_signal] = signal; - request[_getHeaders] = flash - ? () => headersFromHeaderList(inner.headerList(), guard) - : () => headersFromHeaderList(inner.headerList, guard); - return request; } - - /** - * @param {number} serverId - * @param {number} streamRid - * @param {ReadableStream} body - * @param {() => string} methodCb - * @param {() => string} urlCb - * @param {() => [string, string][]} headersCb - * @returns {Request} - */ - function fromFlashRequest( - serverId, - streamRid, - body, + // Passed to new URL(...) which implicitly converts DOMString -> USVString + return webidl.converters["DOMString"](V, opts); +}; +webidl.converters["RequestRedirect"] = webidl.createEnumConverter( + "RequestRedirect", + [ + "follow", + "error", + "manual", + ], +); +webidl.converters["RequestInit"] = webidl.createDictionaryConverter( + "RequestInit", + [ + { key: "method", converter: webidl.converters["ByteString"] }, + { key: "headers", converter: webidl.converters["HeadersInit"] }, + { + key: "body", + converter: webidl.createNullableConverter( + webidl.converters["BodyInit_DOMString"], + ), + }, + { key: "redirect", converter: webidl.converters["RequestRedirect"] }, + { + key: "signal", + converter: webidl.createNullableConverter( + webidl.converters["AbortSignal"], + ), + }, + { key: "client", converter: webidl.converters.any }, + ], +); + +/** + * @param {Request} request + * @returns {InnerRequest} + */ +function toInnerRequest(request) { + return request[_request]; +} + +/** + * @param {InnerRequest} inner + * @param {AbortSignal} signal + * @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard + * @param {boolean} flash + * @returns {Request} + */ +function fromInnerRequest(inner, signal, guard, flash) { + const request = webidl.createBranded(Request); + if (flash) { + request[_flash] = inner; + } else { + request[_request] = inner; + } + request[_signal] = signal; + request[_getHeaders] = flash + ? () => headersFromHeaderList(inner.headerList(), guard) + : () => headersFromHeaderList(inner.headerList, guard); + return request; +} + +/** + * @param {number} serverId + * @param {number} streamRid + * @param {ReadableStream} body + * @param {() => string} methodCb + * @param {() => string} urlCb + * @param {() => [string, string][]} headersCb + * @returns {Request} + */ +function fromFlashRequest( + serverId, + streamRid, + body, + methodCb, + urlCb, + headersCb, +) { + const request = webidl.createBranded(Request); + request[_flash] = { + body: body !== null ? new InnerBody(body) : null, methodCb, urlCb, - headersCb, - ) { - const request = webidl.createBranded(Request); - request[_flash] = { - body: body !== null ? new InnerBody(body) : null, - methodCb, - urlCb, - headerList: headersCb, - streamRid, - serverId, - redirectMode: "follow", - redirectCount: 0, - }; - request[_getHeaders] = () => headersFromHeaderList(headersCb(), "request"); - return request; - } - - window.__bootstrap.fetch ??= {}; - window.__bootstrap.fetch.Request = Request; - window.__bootstrap.fetch.toInnerRequest = toInnerRequest; - window.__bootstrap.fetch.fromFlashRequest = fromFlashRequest; - window.__bootstrap.fetch.fromInnerRequest = fromInnerRequest; - window.__bootstrap.fetch.newInnerRequest = newInnerRequest; - window.__bootstrap.fetch.processUrlList = processUrlList; - window.__bootstrap.fetch._flash = _flash; -})(globalThis); + headerList: headersCb, + streamRid, + serverId, + redirectMode: "follow", + redirectCount: 0, + }; + request[_getHeaders] = () => headersFromHeaderList(headersCb(), "request"); + return request; +} + +export { + _flash, + fromFlashRequest, + fromInnerRequest, + newInnerRequest, + processUrlList, + Request, + RequestPrototype, + toInnerRequest, +}; diff --git a/ext/fetch/23_response.js b/ext/fetch/23_response.js index 070068d28..46912135a 100644 --- a/ext/fetch/23_response.js +++ b/ext/fetch/23_response.js @@ -9,510 +9,510 @@ /// <reference path="../web/06_streams_types.d.ts" /> /// <reference path="./lib.deno_fetch.d.ts" /> /// <reference lib="esnext" /> -"use strict"; - -((window) => { - const { isProxy } = Deno.core; - const webidl = window.__bootstrap.webidl; - const consoleInternal = window.__bootstrap.console; - const { - byteLowerCase, - } = window.__bootstrap.infra; - const { HTTP_TAB_OR_SPACE, regexMatcher, serializeJSValueToJSONString } = - window.__bootstrap.infra; - const { extractBody, mixinBody } = window.__bootstrap.fetchBody; - const { getLocationHref } = window.__bootstrap.location; - const { extractMimeType } = window.__bootstrap.mimesniff; - const { URL } = window.__bootstrap.url; - const { - getDecodeSplitHeader, - headerListFromHeaders, - headersFromHeaderList, - guardFromHeaders, - fillHeaders, - } = window.__bootstrap.headers; - const { - ArrayPrototypeMap, - ArrayPrototypePush, - ObjectDefineProperties, - ObjectPrototypeIsPrototypeOf, - RangeError, - RegExp, - RegExpPrototypeTest, - SafeArrayIterator, - Symbol, - SymbolFor, - TypeError, - } = window.__bootstrap.primordials; - - const VCHAR = ["\x21-\x7E"]; - const OBS_TEXT = ["\x80-\xFF"]; - - const REASON_PHRASE = [ - ...new SafeArrayIterator(HTTP_TAB_OR_SPACE), - ...new SafeArrayIterator(VCHAR), - ...new SafeArrayIterator(OBS_TEXT), - ]; - const REASON_PHRASE_MATCHER = regexMatcher(REASON_PHRASE); - const REASON_PHRASE_RE = new RegExp(`^[${REASON_PHRASE_MATCHER}]*$`); - - const _response = Symbol("response"); - const _headers = Symbol("headers"); - const _mimeType = Symbol("mime type"); - const _body = Symbol("body"); + +const core = globalThis.Deno.core; +import * as webidl from "internal:ext/webidl/00_webidl.js"; +import { createFilteredInspectProxy } from "internal:ext/console/02_console.js"; +import { + byteLowerCase, + HTTP_TAB_OR_SPACE, + regexMatcher, + serializeJSValueToJSONString, +} from "internal:ext/web/00_infra.js"; +import { extractBody, mixinBody } from "internal:ext/fetch/22_body.js"; +import { getLocationHref } from "internal:ext/web/12_location.js"; +import { extractMimeType } from "internal:ext/web/01_mimesniff.js"; +import { URL } from "internal:ext/url/00_url.js"; +import { + fillHeaders, + getDecodeSplitHeader, + guardFromHeaders, + headerListFromHeaders, + headersFromHeaderList, +} from "internal:ext/fetch/20_headers.js"; +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayPrototypeMap, + ArrayPrototypePush, + ObjectDefineProperties, + ObjectPrototypeIsPrototypeOf, + RangeError, + RegExp, + RegExpPrototypeTest, + SafeArrayIterator, + Symbol, + SymbolFor, + TypeError, +} = primordials; + +const VCHAR = ["\x21-\x7E"]; +const OBS_TEXT = ["\x80-\xFF"]; + +const REASON_PHRASE = [ + ...new SafeArrayIterator(HTTP_TAB_OR_SPACE), + ...new SafeArrayIterator(VCHAR), + ...new SafeArrayIterator(OBS_TEXT), +]; +const REASON_PHRASE_MATCHER = regexMatcher(REASON_PHRASE); +const REASON_PHRASE_RE = new RegExp(`^[${REASON_PHRASE_MATCHER}]*$`); + +const _response = Symbol("response"); +const _headers = Symbol("headers"); +const _mimeType = Symbol("mime type"); +const _body = Symbol("body"); + +/** + * @typedef InnerResponse + * @property {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"} type + * @property {() => string | null} url + * @property {string[]} urlList + * @property {number} status + * @property {string} statusMessage + * @property {[string, string][]} headerList + * @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body + * @property {boolean} aborted + * @property {string} [error] + */ + +/** + * @param {number} status + * @returns {boolean} + */ +function nullBodyStatus(status) { + return status === 101 || status === 204 || status === 205 || status === 304; +} + +/** + * @param {number} status + * @returns {boolean} + */ +function redirectStatus(status) { + return status === 301 || status === 302 || status === 303 || + status === 307 || status === 308; +} + +/** + * https://fetch.spec.whatwg.org/#concept-response-clone + * @param {InnerResponse} response + * @returns {InnerResponse} + */ +function cloneInnerResponse(response) { + const urlList = [...new SafeArrayIterator(response.urlList)]; + const headerList = ArrayPrototypeMap( + response.headerList, + (x) => [x[0], x[1]], + ); + + let body = null; + if (response.body !== null) { + body = response.body.clone(); + } + + return { + type: response.type, + body, + headerList, + urlList, + status: response.status, + statusMessage: response.statusMessage, + aborted: response.aborted, + url() { + if (this.urlList.length == 0) return null; + return this.urlList[this.urlList.length - 1]; + }, + }; +} + +/** + * @returns {InnerResponse} + */ +function newInnerResponse(status = 200, statusMessage = "") { + return { + type: "default", + body: null, + headerList: [], + urlList: [], + status, + statusMessage, + aborted: false, + url() { + if (this.urlList.length == 0) return null; + return this.urlList[this.urlList.length - 1]; + }, + }; +} + +/** + * @param {string} error + * @returns {InnerResponse} + */ +function networkError(error) { + const resp = newInnerResponse(0); + resp.type = "error"; + resp.error = error; + return resp; +} + +/** + * @returns {InnerResponse} + */ +function abortedNetworkError() { + const resp = networkError("aborted"); + resp.aborted = true; + return resp; +} + +/** + * https://fetch.spec.whatwg.org#initialize-a-response + * @param {Response} response + * @param {ResponseInit} init + * @param {{ body: fetchBody.InnerBody, contentType: string | null } | null} bodyWithType + */ +function initializeAResponse(response, init, bodyWithType) { + // 1. + if ((init.status < 200 || init.status > 599) && init.status != 101) { + throw new RangeError( + `The status provided (${init.status}) is not equal to 101 and outside the range [200, 599].`, + ); + } + + // 2. + if ( + init.statusText && + !RegExpPrototypeTest(REASON_PHRASE_RE, init.statusText) + ) { + throw new TypeError("Status text is not valid."); + } + + // 3. + response[_response].status = init.status; + + // 4. + response[_response].statusMessage = init.statusText; + // 5. + /** @type {headers.Headers} */ + const headers = response[_headers]; + if (init.headers) { + fillHeaders(headers, init.headers); + } + + // 6. + if (bodyWithType !== null) { + if (nullBodyStatus(response[_response].status)) { + throw new TypeError( + "Response with null body status cannot have body", + ); + } + + const { body, contentType } = bodyWithType; + response[_response].body = body; + + if (contentType !== null) { + let hasContentType = false; + const list = headerListFromHeaders(headers); + for (let i = 0; i < list.length; i++) { + if (byteLowerCase(list[i][0]) === "content-type") { + hasContentType = true; + break; + } + } + if (!hasContentType) { + ArrayPrototypePush(list, ["Content-Type", contentType]); + } + } + } +} + +class Response { + get [_mimeType]() { + const values = getDecodeSplitHeader( + headerListFromHeaders(this[_headers]), + "Content-Type", + ); + return extractMimeType(values); + } + get [_body]() { + return this[_response].body; + } /** - * @typedef InnerResponse - * @property {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"} type - * @property {() => string | null} url - * @property {string[]} urlList - * @property {number} status - * @property {string} statusMessage - * @property {[string, string][]} headerList - * @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body - * @property {boolean} aborted - * @property {string} [error] + * @returns {Response} */ + static error() { + const inner = newInnerResponse(0); + inner.type = "error"; + const response = webidl.createBranded(Response); + response[_response] = inner; + response[_headers] = headersFromHeaderList( + response[_response].headerList, + "immutable", + ); + return response; + } /** + * @param {string} url * @param {number} status - * @returns {boolean} + * @returns {Response} */ - function nullBodyStatus(status) { - return status === 101 || status === 204 || status === 205 || status === 304; + static redirect(url, status = 302) { + const prefix = "Failed to call 'Response.redirect'"; + url = webidl.converters["USVString"](url, { + prefix, + context: "Argument 1", + }); + status = webidl.converters["unsigned short"](status, { + prefix, + context: "Argument 2", + }); + + const baseURL = getLocationHref(); + const parsedURL = new URL(url, baseURL); + if (!redirectStatus(status)) { + throw new RangeError("Invalid redirect status code."); + } + const inner = newInnerResponse(status); + inner.type = "default"; + ArrayPrototypePush(inner.headerList, ["Location", parsedURL.href]); + const response = webidl.createBranded(Response); + response[_response] = inner; + response[_headers] = headersFromHeaderList( + response[_response].headerList, + "immutable", + ); + return response; } /** - * @param {number} status - * @returns {boolean} + * @param {any} data + * @param {ResponseInit} init + * @returns {Response} */ - function redirectStatus(status) { - return status === 301 || status === 302 || status === 303 || - status === 307 || status === 308; + static json(data = undefined, init = {}) { + const prefix = "Failed to call 'Response.json'"; + data = webidl.converters.any(data); + init = webidl.converters["ResponseInit_fast"](init, { + prefix, + context: "Argument 2", + }); + + const str = serializeJSValueToJSONString(data); + const res = extractBody(str); + res.contentType = "application/json"; + const response = webidl.createBranded(Response); + response[_response] = newInnerResponse(); + response[_headers] = headersFromHeaderList( + response[_response].headerList, + "response", + ); + initializeAResponse(response, init, res); + return response; } /** - * https://fetch.spec.whatwg.org/#concept-response-clone - * @param {InnerResponse} response - * @returns {InnerResponse} + * @param {BodyInit | null} body + * @param {ResponseInit} init */ - function cloneInnerResponse(response) { - const urlList = [...new SafeArrayIterator(response.urlList)]; - const headerList = ArrayPrototypeMap( - response.headerList, - (x) => [x[0], x[1]], + constructor(body = null, init = undefined) { + const prefix = "Failed to construct 'Response'"; + body = webidl.converters["BodyInit_DOMString?"](body, { + prefix, + context: "Argument 1", + }); + init = webidl.converters["ResponseInit_fast"](init, { + prefix, + context: "Argument 2", + }); + + this[_response] = newInnerResponse(); + this[_headers] = headersFromHeaderList( + this[_response].headerList, + "response", ); - let body = null; - if (response.body !== null) { - body = response.body.clone(); + let bodyWithType = null; + if (body !== null) { + bodyWithType = extractBody(body); } - - return { - type: response.type, - body, - headerList, - urlList, - status: response.status, - statusMessage: response.statusMessage, - aborted: response.aborted, - url() { - if (this.urlList.length == 0) return null; - return this.urlList[this.urlList.length - 1]; - }, - }; + initializeAResponse(this, init, bodyWithType); + this[webidl.brand] = webidl.brand; } /** - * @returns {InnerResponse} + * @returns {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"} */ - function newInnerResponse(status = 200, statusMessage = "") { - return { - type: "default", - body: null, - headerList: [], - urlList: [], - status, - statusMessage, - aborted: false, - url() { - if (this.urlList.length == 0) return null; - return this.urlList[this.urlList.length - 1]; - }, - }; + get type() { + webidl.assertBranded(this, ResponsePrototype); + return this[_response].type; } /** - * @param {string} error - * @returns {InnerResponse} + * @returns {string} */ - function networkError(error) { - const resp = newInnerResponse(0); - resp.type = "error"; - resp.error = error; - return resp; + get url() { + webidl.assertBranded(this, ResponsePrototype); + const url = this[_response].url(); + if (url === null) return ""; + const newUrl = new URL(url); + newUrl.hash = ""; + return newUrl.href; } /** - * @returns {InnerResponse} + * @returns {boolean} */ - function abortedNetworkError() { - const resp = networkError("aborted"); - resp.aborted = true; - return resp; + get redirected() { + webidl.assertBranded(this, ResponsePrototype); + return this[_response].urlList.length > 1; } /** - * https://fetch.spec.whatwg.org#initialize-a-response - * @param {Response} response - * @param {ResponseInit} init - * @param {{ body: __bootstrap.fetchBody.InnerBody, contentType: string | null } | null} bodyWithType + * @returns {number} */ - function initializeAResponse(response, init, bodyWithType) { - // 1. - if ((init.status < 200 || init.status > 599) && init.status != 101) { - throw new RangeError( - `The status provided (${init.status}) is not equal to 101 and outside the range [200, 599].`, - ); - } - - // 2. - if ( - init.statusText && - !RegExpPrototypeTest(REASON_PHRASE_RE, init.statusText) - ) { - throw new TypeError("Status text is not valid."); - } - - // 3. - response[_response].status = init.status; - - // 4. - response[_response].statusMessage = init.statusText; - // 5. - /** @type {__bootstrap.headers.Headers} */ - const headers = response[_headers]; - if (init.headers) { - fillHeaders(headers, init.headers); - } - - // 6. - if (bodyWithType !== null) { - if (nullBodyStatus(response[_response].status)) { - throw new TypeError( - "Response with null body status cannot have body", - ); - } - - const { body, contentType } = bodyWithType; - response[_response].body = body; - - if (contentType !== null) { - let hasContentType = false; - const list = headerListFromHeaders(headers); - for (let i = 0; i < list.length; i++) { - if (byteLowerCase(list[i][0]) === "content-type") { - hasContentType = true; - break; - } - } - if (!hasContentType) { - ArrayPrototypePush(list, ["Content-Type", contentType]); - } - } - } + get status() { + webidl.assertBranded(this, ResponsePrototype); + return this[_response].status; } - class Response { - get [_mimeType]() { - const values = getDecodeSplitHeader( - headerListFromHeaders(this[_headers]), - "Content-Type", - ); - return extractMimeType(values); - } - get [_body]() { - return this[_response].body; - } - - /** - * @returns {Response} - */ - static error() { - const inner = newInnerResponse(0); - inner.type = "error"; - const response = webidl.createBranded(Response); - response[_response] = inner; - response[_headers] = headersFromHeaderList( - response[_response].headerList, - "immutable", - ); - return response; - } - - /** - * @param {string} url - * @param {number} status - * @returns {Response} - */ - static redirect(url, status = 302) { - const prefix = "Failed to call 'Response.redirect'"; - url = webidl.converters["USVString"](url, { - prefix, - context: "Argument 1", - }); - status = webidl.converters["unsigned short"](status, { - prefix, - context: "Argument 2", - }); - - const baseURL = getLocationHref(); - const parsedURL = new URL(url, baseURL); - if (!redirectStatus(status)) { - throw new RangeError("Invalid redirect status code."); - } - const inner = newInnerResponse(status); - inner.type = "default"; - ArrayPrototypePush(inner.headerList, ["Location", parsedURL.href]); - const response = webidl.createBranded(Response); - response[_response] = inner; - response[_headers] = headersFromHeaderList( - response[_response].headerList, - "immutable", - ); - return response; - } - - /** - * @param {any} data - * @param {ResponseInit} init - * @returns {Response} - */ - static json(data = undefined, init = {}) { - const prefix = "Failed to call 'Response.json'"; - data = webidl.converters.any(data); - init = webidl.converters["ResponseInit_fast"](init, { - prefix, - context: "Argument 2", - }); - - const str = serializeJSValueToJSONString(data); - const res = extractBody(str); - res.contentType = "application/json"; - const response = webidl.createBranded(Response); - response[_response] = newInnerResponse(); - response[_headers] = headersFromHeaderList( - response[_response].headerList, - "response", - ); - initializeAResponse(response, init, res); - return response; - } - - /** - * @param {BodyInit | null} body - * @param {ResponseInit} init - */ - constructor(body = null, init = undefined) { - const prefix = "Failed to construct 'Response'"; - body = webidl.converters["BodyInit_DOMString?"](body, { - prefix, - context: "Argument 1", - }); - init = webidl.converters["ResponseInit_fast"](init, { - prefix, - context: "Argument 2", - }); - - this[_response] = newInnerResponse(); - this[_headers] = headersFromHeaderList( - this[_response].headerList, - "response", - ); - - let bodyWithType = null; - if (body !== null) { - bodyWithType = extractBody(body); - } - initializeAResponse(this, init, bodyWithType); - this[webidl.brand] = webidl.brand; - } - - /** - * @returns {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"} - */ - get type() { - webidl.assertBranded(this, ResponsePrototype); - return this[_response].type; - } - - /** - * @returns {string} - */ - get url() { - webidl.assertBranded(this, ResponsePrototype); - const url = this[_response].url(); - if (url === null) return ""; - const newUrl = new URL(url); - newUrl.hash = ""; - return newUrl.href; - } - - /** - * @returns {boolean} - */ - get redirected() { - webidl.assertBranded(this, ResponsePrototype); - return this[_response].urlList.length > 1; - } - - /** - * @returns {number} - */ - get status() { - webidl.assertBranded(this, ResponsePrototype); - return this[_response].status; - } - - /** - * @returns {boolean} - */ - get ok() { - webidl.assertBranded(this, ResponsePrototype); - const status = this[_response].status; - return status >= 200 && status <= 299; - } - - /** - * @returns {string} - */ - get statusText() { - webidl.assertBranded(this, ResponsePrototype); - return this[_response].statusMessage; - } - - /** - * @returns {Headers} - */ - get headers() { - webidl.assertBranded(this, ResponsePrototype); - return this[_headers]; - } - - /** - * @returns {Response} - */ - clone() { - webidl.assertBranded(this, ResponsePrototype); - if (this[_body] && this[_body].unusable()) { - throw new TypeError("Body is unusable."); - } - const second = webidl.createBranded(Response); - const newRes = cloneInnerResponse(this[_response]); - second[_response] = newRes; - second[_headers] = headersFromHeaderList( - newRes.headerList, - guardFromHeaders(this[_headers]), - ); - return second; - } - - [SymbolFor("Deno.customInspect")](inspect) { - return inspect(consoleInternal.createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(ResponsePrototype, this), - keys: [ - "body", - "bodyUsed", - "headers", - "ok", - "redirected", - "status", - "statusText", - "url", - ], - })); - } + /** + * @returns {boolean} + */ + get ok() { + webidl.assertBranded(this, ResponsePrototype); + const status = this[_response].status; + return status >= 200 && status <= 299; } - webidl.configurePrototype(Response); - ObjectDefineProperties(Response, { - json: { enumerable: true }, - redirect: { enumerable: true }, - error: { enumerable: true }, - }); - const ResponsePrototype = Response.prototype; - mixinBody(ResponsePrototype, _body, _mimeType); - - webidl.converters["Response"] = webidl.createInterfaceConverter( - "Response", - ResponsePrototype, - ); - webidl.converters["ResponseInit"] = webidl.createDictionaryConverter( - "ResponseInit", - [{ - key: "status", - defaultValue: 200, - converter: webidl.converters["unsigned short"], - }, { - key: "statusText", - defaultValue: "", - converter: webidl.converters["ByteString"], - }, { - key: "headers", - converter: webidl.converters["HeadersInit"], - }], - ); - webidl.converters["ResponseInit_fast"] = function (init, opts) { - if (init === undefined || init === null) { - return { status: 200, statusText: "", headers: undefined }; - } - // Fast path, if not a proxy - if (typeof init === "object" && !isProxy(init)) { - // Not a proxy fast path - const status = init.status !== undefined - ? webidl.converters["unsigned short"](init.status) - : 200; - const statusText = init.statusText !== undefined - ? webidl.converters["ByteString"](init.statusText) - : ""; - const headers = init.headers !== undefined - ? webidl.converters["HeadersInit"](init.headers) - : undefined; - return { status, statusText, headers }; - } - // Slow default path - return webidl.converters["ResponseInit"](init, opts); - }; + /** + * @returns {string} + */ + get statusText() { + webidl.assertBranded(this, ResponsePrototype); + return this[_response].statusMessage; + } /** - * @param {Response} response - * @returns {InnerResponse} + * @returns {Headers} */ - function toInnerResponse(response) { - return response[_response]; + get headers() { + webidl.assertBranded(this, ResponsePrototype); + return this[_headers]; } /** - * @param {InnerResponse} inner - * @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard * @returns {Response} */ - function fromInnerResponse(inner, guard) { - const response = webidl.createBranded(Response); - response[_response] = inner; - response[_headers] = headersFromHeaderList(inner.headerList, guard); - return response; + clone() { + webidl.assertBranded(this, ResponsePrototype); + if (this[_body] && this[_body].unusable()) { + throw new TypeError("Body is unusable."); + } + const second = webidl.createBranded(Response); + const newRes = cloneInnerResponse(this[_response]); + second[_response] = newRes; + second[_headers] = headersFromHeaderList( + newRes.headerList, + guardFromHeaders(this[_headers]), + ); + return second; } - window.__bootstrap.fetch ??= {}; - window.__bootstrap.fetch.Response = Response; - window.__bootstrap.fetch.ResponsePrototype = ResponsePrototype; - window.__bootstrap.fetch.newInnerResponse = newInnerResponse; - window.__bootstrap.fetch.toInnerResponse = toInnerResponse; - window.__bootstrap.fetch.fromInnerResponse = fromInnerResponse; - window.__bootstrap.fetch.redirectStatus = redirectStatus; - window.__bootstrap.fetch.nullBodyStatus = nullBodyStatus; - window.__bootstrap.fetch.networkError = networkError; - window.__bootstrap.fetch.abortedNetworkError = abortedNetworkError; -})(globalThis); + [SymbolFor("Deno.customInspect")](inspect) { + return inspect(createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(ResponsePrototype, this), + keys: [ + "body", + "bodyUsed", + "headers", + "ok", + "redirected", + "status", + "statusText", + "url", + ], + })); + } +} + +webidl.configurePrototype(Response); +ObjectDefineProperties(Response, { + json: { enumerable: true }, + redirect: { enumerable: true }, + error: { enumerable: true }, +}); +const ResponsePrototype = Response.prototype; +mixinBody(ResponsePrototype, _body, _mimeType); + +webidl.converters["Response"] = webidl.createInterfaceConverter( + "Response", + ResponsePrototype, +); +webidl.converters["ResponseInit"] = webidl.createDictionaryConverter( + "ResponseInit", + [{ + key: "status", + defaultValue: 200, + converter: webidl.converters["unsigned short"], + }, { + key: "statusText", + defaultValue: "", + converter: webidl.converters["ByteString"], + }, { + key: "headers", + converter: webidl.converters["HeadersInit"], + }], +); +webidl.converters["ResponseInit_fast"] = function (init, opts) { + if (init === undefined || init === null) { + return { status: 200, statusText: "", headers: undefined }; + } + // Fast path, if not a proxy + if (typeof init === "object" && !core.isProxy(init)) { + // Not a proxy fast path + const status = init.status !== undefined + ? webidl.converters["unsigned short"](init.status) + : 200; + const statusText = init.statusText !== undefined + ? webidl.converters["ByteString"](init.statusText) + : ""; + const headers = init.headers !== undefined + ? webidl.converters["HeadersInit"](init.headers) + : undefined; + return { status, statusText, headers }; + } + // Slow default path + return webidl.converters["ResponseInit"](init, opts); +}; + +/** + * @param {Response} response + * @returns {InnerResponse} + */ +function toInnerResponse(response) { + return response[_response]; +} + +/** + * @param {InnerResponse} inner + * @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard + * @returns {Response} + */ +function fromInnerResponse(inner, guard) { + const response = webidl.createBranded(Response); + response[_response] = inner; + response[_headers] = headersFromHeaderList(inner.headerList, guard); + return response; +} + +export { + abortedNetworkError, + fromInnerResponse, + networkError, + newInnerResponse, + nullBodyStatus, + redirectStatus, + Response, + ResponsePrototype, + toInnerResponse, +}; diff --git a/ext/fetch/26_fetch.js b/ext/fetch/26_fetch.js index ddb023a37..9c136f242 100644 --- a/ext/fetch/26_fetch.js +++ b/ext/fetch/26_fetch.js @@ -9,578 +9,577 @@ /// <reference path="./internal.d.ts" /> /// <reference path="./lib.deno_fetch.d.ts" /> /// <reference lib="esnext" /> -"use strict"; - -((window) => { - const core = window.Deno.core; - const ops = core.ops; - const webidl = window.__bootstrap.webidl; - const { byteLowerCase } = window.__bootstrap.infra; - const { BlobPrototype } = window.__bootstrap.file; - const { errorReadableStream, ReadableStreamPrototype, readableStreamForRid } = - window.__bootstrap.streams; - const { InnerBody, extractBody } = window.__bootstrap.fetchBody; - const { - toInnerRequest, - toInnerResponse, - fromInnerResponse, - redirectStatus, - nullBodyStatus, - networkError, - abortedNetworkError, - processUrlList, - } = window.__bootstrap.fetch; - const abortSignal = window.__bootstrap.abortSignal; - const { - ArrayPrototypePush, - ArrayPrototypeSplice, - ArrayPrototypeFilter, - ArrayPrototypeIncludes, - ObjectPrototypeIsPrototypeOf, - Promise, - PromisePrototypeThen, - PromisePrototypeCatch, - SafeArrayIterator, - String, - StringPrototypeStartsWith, - StringPrototypeToLowerCase, - TypeError, - Uint8Array, - Uint8ArrayPrototype, - WeakMap, - WeakMapPrototypeDelete, - WeakMapPrototypeGet, - WeakMapPrototypeHas, - WeakMapPrototypeSet, - } = window.__bootstrap.primordials; - - const REQUEST_BODY_HEADER_NAMES = [ - "content-encoding", - "content-language", - "content-location", - "content-type", - ]; - - const requestBodyReaders = new WeakMap(); - - /** - * @param {{ method: string, url: string, headers: [string, string][], clientRid: number | null, hasBody: boolean }} args - * @param {Uint8Array | null} body - * @returns {{ requestRid: number, requestBodyRid: number | null }} - */ - function opFetch(method, url, headers, clientRid, hasBody, bodyLength, body) { - return ops.op_fetch( - method, - url, - headers, - clientRid, - hasBody, - bodyLength, - body, - ); - } - /** - * @param {number} rid - * @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number }>} - */ - function opFetchSend(rid) { - return core.opAsync("op_fetch_send", rid); +const core = globalThis.Deno.core; +const ops = core.ops; +import * as webidl from "internal:ext/webidl/00_webidl.js"; +import { byteLowerCase } from "internal:ext/web/00_infra.js"; +import { BlobPrototype } from "internal:ext/web/09_file.js"; +import { + errorReadableStream, + readableStreamForRid, + ReadableStreamPrototype, +} from "internal:ext/web/06_streams.js"; +import { extractBody, InnerBody } from "internal:ext/fetch/22_body.js"; +import { + processUrlList, + toInnerRequest, +} from "internal:ext/fetch/23_request.js"; +import { + abortedNetworkError, + fromInnerResponse, + networkError, + nullBodyStatus, + redirectStatus, + toInnerResponse, +} from "internal:ext/fetch/23_response.js"; +import * as abortSignal from "internal:ext/web/03_abort_signal.js"; +const primordials = globalThis.__bootstrap.primordials; +const { + ArrayPrototypePush, + ArrayPrototypeSplice, + ArrayPrototypeFilter, + ArrayPrototypeIncludes, + ObjectPrototypeIsPrototypeOf, + Promise, + PromisePrototypeThen, + PromisePrototypeCatch, + SafeArrayIterator, + String, + StringPrototypeStartsWith, + StringPrototypeToLowerCase, + TypeError, + Uint8Array, + Uint8ArrayPrototype, + WeakMap, + WeakMapPrototypeDelete, + WeakMapPrototypeGet, + WeakMapPrototypeHas, + WeakMapPrototypeSet, +} = primordials; + +const REQUEST_BODY_HEADER_NAMES = [ + "content-encoding", + "content-language", + "content-location", + "content-type", +]; + +const requestBodyReaders = new WeakMap(); + +/** + * @param {{ method: string, url: string, headers: [string, string][], clientRid: number | null, hasBody: boolean }} args + * @param {Uint8Array | null} body + * @returns {{ requestRid: number, requestBodyRid: number | null }} + */ +function opFetch(method, url, headers, clientRid, hasBody, bodyLength, body) { + return ops.op_fetch( + method, + url, + headers, + clientRid, + hasBody, + bodyLength, + body, + ); +} + +/** + * @param {number} rid + * @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number }>} + */ +function opFetchSend(rid) { + return core.opAsync("op_fetch_send", rid); +} + +/** + * @param {number} responseBodyRid + * @param {AbortSignal} [terminator] + * @returns {ReadableStream<Uint8Array>} + */ +function createResponseBodyStream(responseBodyRid, terminator) { + const readable = readableStreamForRid(responseBodyRid); + + function onAbort() { + errorReadableStream(readable, terminator.reason); + core.tryClose(responseBodyRid); } - /** - * @param {number} responseBodyRid - * @param {AbortSignal} [terminator] - * @returns {ReadableStream<Uint8Array>} - */ - function createResponseBodyStream(responseBodyRid, terminator) { - const readable = readableStreamForRid(responseBodyRid); - - function onAbort() { - errorReadableStream(readable, terminator.reason); - core.tryClose(responseBodyRid); + // TODO(lucacasonato): clean up registration + terminator[abortSignal.add](onAbort); + + return readable; +} + +/** + * @param {InnerRequest} req + * @param {boolean} recursive + * @param {AbortSignal} terminator + * @returns {Promise<InnerResponse>} + */ +async function mainFetch(req, recursive, terminator) { + if (req.blobUrlEntry !== null) { + if (req.method !== "GET") { + throw new TypeError("Blob URL fetch only supports GET method."); } - // TODO(lucacasonato): clean up registration - terminator[abortSignal.add](onAbort); + const body = new InnerBody(req.blobUrlEntry.stream()); + terminator[abortSignal.add](() => body.error(terminator.reason)); + processUrlList(req.urlList, req.urlListProcessed); - return readable; + return { + headerList: [ + ["content-length", String(req.blobUrlEntry.size)], + ["content-type", req.blobUrlEntry.type], + ], + status: 200, + statusMessage: "OK", + body, + type: "basic", + url() { + if (this.urlList.length == 0) return null; + return this.urlList[this.urlList.length - 1]; + }, + urlList: recursive + ? [] + : [...new SafeArrayIterator(req.urlListProcessed)], + }; } - /** - * @param {InnerRequest} req - * @param {boolean} recursive - * @param {AbortSignal} terminator - * @returns {Promise<InnerResponse>} - */ - async function mainFetch(req, recursive, terminator) { - if (req.blobUrlEntry !== null) { - if (req.method !== "GET") { - throw new TypeError("Blob URL fetch only supports GET method."); - } - - const body = new InnerBody(req.blobUrlEntry.stream()); - terminator[abortSignal.add](() => body.error(terminator.reason)); - processUrlList(req.urlList, req.urlListProcessed); - - return { - headerList: [ - ["content-length", String(req.blobUrlEntry.size)], - ["content-type", req.blobUrlEntry.type], - ], - status: 200, - statusMessage: "OK", - body, - type: "basic", - url() { - if (this.urlList.length == 0) return null; - return this.urlList[this.urlList.length - 1]; - }, - urlList: recursive - ? [] - : [...new SafeArrayIterator(req.urlListProcessed)], - }; - } + /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */ + let reqBody = null; - /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */ - let reqBody = null; - - if (req.body !== null) { + if (req.body !== null) { + if ( + ObjectPrototypeIsPrototypeOf( + ReadableStreamPrototype, + req.body.streamOrStatic, + ) + ) { if ( - ObjectPrototypeIsPrototypeOf( - ReadableStreamPrototype, - req.body.streamOrStatic, - ) + req.body.length === null || + ObjectPrototypeIsPrototypeOf(BlobPrototype, req.body.source) ) { - if ( - req.body.length === null || - ObjectPrototypeIsPrototypeOf(BlobPrototype, req.body.source) - ) { - reqBody = req.body.stream; + reqBody = req.body.stream; + } else { + const reader = req.body.stream.getReader(); + WeakMapPrototypeSet(requestBodyReaders, req, reader); + const r1 = await reader.read(); + if (r1.done) { + reqBody = new Uint8Array(0); } else { - const reader = req.body.stream.getReader(); - WeakMapPrototypeSet(requestBodyReaders, req, reader); - const r1 = await reader.read(); - if (r1.done) { - reqBody = new Uint8Array(0); - } else { - reqBody = r1.value; - const r2 = await reader.read(); - if (!r2.done) throw new TypeError("Unreachable"); - } - WeakMapPrototypeDelete(requestBodyReaders, req); + reqBody = r1.value; + const r2 = await reader.read(); + if (!r2.done) throw new TypeError("Unreachable"); } - } else { - req.body.streamOrStatic.consumed = true; - reqBody = req.body.streamOrStatic.body; - // TODO(@AaronO): plumb support for StringOrBuffer all the way - reqBody = typeof reqBody === "string" ? core.encode(reqBody) : reqBody; + WeakMapPrototypeDelete(requestBodyReaders, req); } + } else { + req.body.streamOrStatic.consumed = true; + reqBody = req.body.streamOrStatic.body; + // TODO(@AaronO): plumb support for StringOrBuffer all the way + reqBody = typeof reqBody === "string" ? core.encode(reqBody) : reqBody; } + } - const { requestRid, requestBodyRid, cancelHandleRid } = opFetch( - req.method, - req.currentUrl(), - req.headerList, - req.clientRid, - reqBody !== null, - req.body?.length, - ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, reqBody) - ? reqBody - : null, - ); - - function onAbort() { - if (cancelHandleRid !== null) { - core.tryClose(cancelHandleRid); - } - if (requestBodyRid !== null) { - core.tryClose(requestBodyRid); - } + const { requestRid, requestBodyRid, cancelHandleRid } = opFetch( + req.method, + req.currentUrl(), + req.headerList, + req.clientRid, + reqBody !== null, + req.body?.length, + ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, reqBody) ? reqBody : null, + ); + + function onAbort() { + if (cancelHandleRid !== null) { + core.tryClose(cancelHandleRid); } - terminator[abortSignal.add](onAbort); - - let requestSendError; - let requestSendErrorSet = false; if (requestBodyRid !== null) { - if ( - reqBody === null || - !ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, reqBody) - ) { - throw new TypeError("Unreachable"); + core.tryClose(requestBodyRid); + } + } + terminator[abortSignal.add](onAbort); + + let requestSendError; + let requestSendErrorSet = false; + if (requestBodyRid !== null) { + if ( + reqBody === null || + !ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, reqBody) + ) { + throw new TypeError("Unreachable"); + } + const reader = reqBody.getReader(); + WeakMapPrototypeSet(requestBodyReaders, req, reader); + (async () => { + let done = false; + while (!done) { + let val; + try { + const res = await reader.read(); + done = res.done; + val = res.value; + } catch (err) { + if (terminator.aborted) break; + // TODO(lucacasonato): propagate error into response body stream + requestSendError = err; + requestSendErrorSet = true; + break; + } + if (done) break; + if (!ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, val)) { + const error = new TypeError( + "Item in request body ReadableStream is not a Uint8Array", + ); + await reader.cancel(error); + // TODO(lucacasonato): propagate error into response body stream + requestSendError = error; + requestSendErrorSet = true; + break; + } + try { + await core.writeAll(requestBodyRid, val); + } catch (err) { + if (terminator.aborted) break; + await reader.cancel(err); + // TODO(lucacasonato): propagate error into response body stream + requestSendError = err; + requestSendErrorSet = true; + break; + } } - const reader = reqBody.getReader(); - WeakMapPrototypeSet(requestBodyReaders, req, reader); - (async () => { - let done = false; - while (!done) { - let val; - try { - const res = await reader.read(); - done = res.done; - val = res.value; - } catch (err) { - if (terminator.aborted) break; - // TODO(lucacasonato): propagate error into response body stream + if (done && !terminator.aborted) { + try { + await core.shutdown(requestBodyRid); + } catch (err) { + if (!terminator.aborted) { requestSendError = err; requestSendErrorSet = true; - break; - } - if (done) break; - if (!ObjectPrototypeIsPrototypeOf(Uint8ArrayPrototype, val)) { - const error = new TypeError( - "Item in request body ReadableStream is not a Uint8Array", - ); - await reader.cancel(error); - // TODO(lucacasonato): propagate error into response body stream - requestSendError = error; - requestSendErrorSet = true; - break; - } - try { - await core.writeAll(requestBodyRid, val); - } catch (err) { - if (terminator.aborted) break; - await reader.cancel(err); - // TODO(lucacasonato): propagate error into response body stream - requestSendError = err; - requestSendErrorSet = true; - break; } } - if (done && !terminator.aborted) { - try { - await core.shutdown(requestBodyRid); - } catch (err) { - if (!terminator.aborted) { - requestSendError = err; - requestSendErrorSet = true; - } - } - } - WeakMapPrototypeDelete(requestBodyReaders, req); - core.tryClose(requestBodyRid); - })(); - } - let resp; - try { - resp = await opFetchSend(requestRid); - } catch (err) { - if (terminator.aborted) return; - if (requestSendErrorSet) { - // if the request body stream errored, we want to propagate that error - // instead of the original error from opFetchSend - throw new TypeError("Failed to fetch: request body stream errored", { - cause: requestSendError, - }); - } - throw err; - } finally { - if (cancelHandleRid !== null) { - core.tryClose(cancelHandleRid); } + WeakMapPrototypeDelete(requestBodyReaders, req); + core.tryClose(requestBodyRid); + })(); + } + let resp; + try { + resp = await opFetchSend(requestRid); + } catch (err) { + if (terminator.aborted) return; + if (requestSendErrorSet) { + // if the request body stream errored, we want to propagate that error + // instead of the original error from opFetchSend + throw new TypeError("Failed to fetch: request body stream errored", { + cause: requestSendError, + }); } - if (terminator.aborted) return abortedNetworkError(); - - processUrlList(req.urlList, req.urlListProcessed); - - /** @type {InnerResponse} */ - const response = { - headerList: resp.headers, - status: resp.status, - body: null, - statusMessage: resp.statusText, - type: "basic", - url() { - if (this.urlList.length == 0) return null; - return this.urlList[this.urlList.length - 1]; - }, - urlList: req.urlListProcessed, - }; - if (redirectStatus(resp.status)) { - switch (req.redirectMode) { - case "error": - core.close(resp.responseRid); - return networkError( - "Encountered redirect while redirect mode is set to 'error'", - ); - case "follow": - core.close(resp.responseRid); - return httpRedirectFetch(req, response, terminator); - case "manual": - break; - } + throw err; + } finally { + if (cancelHandleRid !== null) { + core.tryClose(cancelHandleRid); + } + } + if (terminator.aborted) return abortedNetworkError(); + + processUrlList(req.urlList, req.urlListProcessed); + + /** @type {InnerResponse} */ + const response = { + headerList: resp.headers, + status: resp.status, + body: null, + statusMessage: resp.statusText, + type: "basic", + url() { + if (this.urlList.length == 0) return null; + return this.urlList[this.urlList.length - 1]; + }, + urlList: req.urlListProcessed, + }; + if (redirectStatus(resp.status)) { + switch (req.redirectMode) { + case "error": + core.close(resp.responseRid); + return networkError( + "Encountered redirect while redirect mode is set to 'error'", + ); + case "follow": + core.close(resp.responseRid); + return httpRedirectFetch(req, response, terminator); + case "manual": + break; } + } - if (nullBodyStatus(response.status)) { + if (nullBodyStatus(response.status)) { + core.close(resp.responseRid); + } else { + if (req.method === "HEAD" || req.method === "CONNECT") { + response.body = null; core.close(resp.responseRid); } else { - if (req.method === "HEAD" || req.method === "CONNECT") { - response.body = null; - core.close(resp.responseRid); - } else { - response.body = new InnerBody( - createResponseBodyStream(resp.responseRid, terminator), - ); - } + response.body = new InnerBody( + createResponseBodyStream(resp.responseRid, terminator), + ); } + } - if (recursive) return response; + if (recursive) return response; - if (response.urlList.length === 0) { - processUrlList(req.urlList, req.urlListProcessed); - response.urlList = [...new SafeArrayIterator(req.urlListProcessed)]; - } + if (response.urlList.length === 0) { + processUrlList(req.urlList, req.urlListProcessed); + response.urlList = [...new SafeArrayIterator(req.urlListProcessed)]; + } + return response; +} + +/** + * @param {InnerRequest} request + * @param {InnerResponse} response + * @param {AbortSignal} terminator + * @returns {Promise<InnerResponse>} + */ +function httpRedirectFetch(request, response, terminator) { + const locationHeaders = ArrayPrototypeFilter( + response.headerList, + (entry) => byteLowerCase(entry[0]) === "location", + ); + if (locationHeaders.length === 0) { return response; } - - /** - * @param {InnerRequest} request - * @param {InnerResponse} response - * @param {AbortSignal} terminator - * @returns {Promise<InnerResponse>} - */ - function httpRedirectFetch(request, response, terminator) { - const locationHeaders = ArrayPrototypeFilter( - response.headerList, - (entry) => byteLowerCase(entry[0]) === "location", - ); - if (locationHeaders.length === 0) { - return response; - } - const locationURL = new URL( - locationHeaders[0][1], - response.url() ?? undefined, + const locationURL = new URL( + locationHeaders[0][1], + response.url() ?? undefined, + ); + if (locationURL.hash === "") { + locationURL.hash = request.currentUrl().hash; + } + if (locationURL.protocol !== "https:" && locationURL.protocol !== "http:") { + return networkError("Can not redirect to a non HTTP(s) url"); + } + if (request.redirectCount === 20) { + return networkError("Maximum number of redirects (20) reached"); + } + request.redirectCount++; + if ( + response.status !== 303 && + request.body !== null && + request.body.source === null + ) { + return networkError( + "Can not redeliver a streaming request body after a redirect", ); - if (locationURL.hash === "") { - locationURL.hash = request.currentUrl().hash; - } - if (locationURL.protocol !== "https:" && locationURL.protocol !== "http:") { - return networkError("Can not redirect to a non HTTP(s) url"); - } - if (request.redirectCount === 20) { - return networkError("Maximum number of redirects (20) reached"); - } - request.redirectCount++; - if ( - response.status !== 303 && - request.body !== null && - request.body.source === null - ) { - return networkError( - "Can not redeliver a streaming request body after a redirect", - ); - } - if ( - ((response.status === 301 || response.status === 302) && - request.method === "POST") || - (response.status === 303 && - request.method !== "GET" && - request.method !== "HEAD") - ) { - request.method = "GET"; - request.body = null; - for (let i = 0; i < request.headerList.length; i++) { - if ( - ArrayPrototypeIncludes( - REQUEST_BODY_HEADER_NAMES, - byteLowerCase(request.headerList[i][0]), - ) - ) { - ArrayPrototypeSplice(request.headerList, i, 1); - i--; - } + } + if ( + ((response.status === 301 || response.status === 302) && + request.method === "POST") || + (response.status === 303 && + request.method !== "GET" && + request.method !== "HEAD") + ) { + request.method = "GET"; + request.body = null; + for (let i = 0; i < request.headerList.length; i++) { + if ( + ArrayPrototypeIncludes( + REQUEST_BODY_HEADER_NAMES, + byteLowerCase(request.headerList[i][0]), + ) + ) { + ArrayPrototypeSplice(request.headerList, i, 1); + i--; } } - if (request.body !== null) { - const res = extractBody(request.body.source); - request.body = res.body; - } - ArrayPrototypePush(request.urlList, () => locationURL.href); - return mainFetch(request, true, terminator); } + if (request.body !== null) { + const res = extractBody(request.body.source); + request.body = res.body; + } + ArrayPrototypePush(request.urlList, () => locationURL.href); + return mainFetch(request, true, terminator); +} + +/** + * @param {RequestInfo} input + * @param {RequestInit} init + */ +function fetch(input, init = {}) { + // There is an async dispatch later that causes a stack trace disconnect. + // We reconnect it by assigning the result of that dispatch to `opPromise`, + // awaiting `opPromise` in an inner function also named `fetch()` and + // returning the result from that. + let opPromise = undefined; + // 1. + const result = new Promise((resolve, reject) => { + const prefix = "Failed to call 'fetch'"; + webidl.requiredArguments(arguments.length, 1, { prefix }); + // 2. + const requestObject = new Request(input, init); + // 3. + const request = toInnerRequest(requestObject); + // 4. + if (requestObject.signal.aborted) { + reject(abortFetch(request, null, requestObject.signal.reason)); + return; + } - /** - * @param {RequestInfo} input - * @param {RequestInit} init - */ - function fetch(input, init = {}) { - // There is an async dispatch later that causes a stack trace disconnect. - // We reconnect it by assigning the result of that dispatch to `opPromise`, - // awaiting `opPromise` in an inner function also named `fetch()` and - // returning the result from that. - let opPromise = undefined; - // 1. - const result = new Promise((resolve, reject) => { - const prefix = "Failed to call 'fetch'"; - webidl.requiredArguments(arguments.length, 1, { prefix }); - // 2. - const requestObject = new Request(input, init); - // 3. - const request = toInnerRequest(requestObject); - // 4. - if (requestObject.signal.aborted) { - reject(abortFetch(request, null, requestObject.signal.reason)); - return; - } - - // 7. - let responseObject = null; - // 9. - let locallyAborted = false; - // 10. - function onabort() { - locallyAborted = true; - reject( - abortFetch(request, responseObject, requestObject.signal.reason), - ); - } - requestObject.signal[abortSignal.add](onabort); + // 7. + let responseObject = null; + // 9. + let locallyAborted = false; + // 10. + function onabort() { + locallyAborted = true; + reject( + abortFetch(request, responseObject, requestObject.signal.reason), + ); + } + requestObject.signal[abortSignal.add](onabort); - if (!requestObject.headers.has("Accept")) { - ArrayPrototypePush(request.headerList, ["Accept", "*/*"]); - } + if (!requestObject.headers.has("Accept")) { + ArrayPrototypePush(request.headerList, ["Accept", "*/*"]); + } - if (!requestObject.headers.has("Accept-Language")) { - ArrayPrototypePush(request.headerList, ["Accept-Language", "*"]); - } + if (!requestObject.headers.has("Accept-Language")) { + ArrayPrototypePush(request.headerList, ["Accept-Language", "*"]); + } - // 12. - opPromise = PromisePrototypeCatch( - PromisePrototypeThen( - mainFetch(request, false, requestObject.signal), - (response) => { - // 12.1. - if (locallyAborted) return; - // 12.2. - if (response.aborted) { - reject( - abortFetch( - request, - responseObject, - requestObject.signal.reason, - ), - ); - requestObject.signal[abortSignal.remove](onabort); - return; - } - // 12.3. - if (response.type === "error") { - const err = new TypeError( - "Fetch failed: " + (response.error ?? "unknown error"), - ); - reject(err); - requestObject.signal[abortSignal.remove](onabort); - return; - } - responseObject = fromInnerResponse(response, "immutable"); - resolve(responseObject); + // 12. + opPromise = PromisePrototypeCatch( + PromisePrototypeThen( + mainFetch(request, false, requestObject.signal), + (response) => { + // 12.1. + if (locallyAborted) return; + // 12.2. + if (response.aborted) { + reject( + abortFetch( + request, + responseObject, + requestObject.signal.reason, + ), + ); requestObject.signal[abortSignal.remove](onabort); - }, - ), - (err) => { - reject(err); + return; + } + // 12.3. + if (response.type === "error") { + const err = new TypeError( + "Fetch failed: " + (response.error ?? "unknown error"), + ); + reject(err); + requestObject.signal[abortSignal.remove](onabort); + return; + } + responseObject = fromInnerResponse(response, "immutable"); + resolve(responseObject); requestObject.signal[abortSignal.remove](onabort); }, - ); - }); - if (opPromise) { - PromisePrototypeCatch(result, () => {}); - return (async function fetch() { - await opPromise; - return result; - })(); - } - return result; + ), + (err) => { + reject(err); + requestObject.signal[abortSignal.remove](onabort); + }, + ); + }); + if (opPromise) { + PromisePrototypeCatch(result, () => {}); + return (async function fetch() { + await opPromise; + return result; + })(); } + return result; +} - function abortFetch(request, responseObject, error) { - if (request.body !== null) { - if (WeakMapPrototypeHas(requestBodyReaders, request)) { - WeakMapPrototypeGet(requestBodyReaders, request).cancel(error); - } else { - request.body.cancel(error); - } - } - if (responseObject !== null) { - const response = toInnerResponse(responseObject); - if (response.body !== null) response.body.error(error); +function abortFetch(request, responseObject, error) { + if (request.body !== null) { + if (WeakMapPrototypeHas(requestBodyReaders, request)) { + WeakMapPrototypeGet(requestBodyReaders, request).cancel(error); + } else { + request.body.cancel(error); } - return error; } + if (responseObject !== null) { + const response = toInnerResponse(responseObject); + if (response.body !== null) response.body.error(error); + } + return error; +} + +/** + * Handle the Response argument to the WebAssembly streaming APIs, after + * resolving if it was passed as a promise. This function should be registered + * through `Deno.core.setWasmStreamingCallback`. + * + * @param {any} source The source parameter that the WebAssembly streaming API + * was called with. If it was called with a Promise, `source` is the resolved + * value of that promise. + * @param {number} rid An rid that represents the wasm streaming resource. + */ +function handleWasmStreaming(source, rid) { + // This implements part of + // https://webassembly.github.io/spec/web-api/#compile-a-potential-webassembly-response + try { + const res = webidl.converters["Response"](source, { + prefix: "Failed to call 'WebAssembly.compileStreaming'", + context: "Argument 1", + }); - /** - * Handle the Response argument to the WebAssembly streaming APIs, after - * resolving if it was passed as a promise. This function should be registered - * through `Deno.core.setWasmStreamingCallback`. - * - * @param {any} source The source parameter that the WebAssembly streaming API - * was called with. If it was called with a Promise, `source` is the resolved - * value of that promise. - * @param {number} rid An rid that represents the wasm streaming resource. - */ - function handleWasmStreaming(source, rid) { - // This implements part of - // https://webassembly.github.io/spec/web-api/#compile-a-potential-webassembly-response - try { - const res = webidl.converters["Response"](source, { - prefix: "Failed to call 'WebAssembly.compileStreaming'", - context: "Argument 1", - }); - - // 2.3. - // The spec is ambiguous here, see - // https://github.com/WebAssembly/spec/issues/1138. The WPT tests expect - // the raw value of the Content-Type attribute lowercased. We ignore this - // for file:// because file fetches don't have a Content-Type. - if (!StringPrototypeStartsWith(res.url, "file://")) { - const contentType = res.headers.get("Content-Type"); - if ( - typeof contentType !== "string" || - StringPrototypeToLowerCase(contentType) !== "application/wasm" - ) { - throw new TypeError("Invalid WebAssembly content type."); - } + // 2.3. + // The spec is ambiguous here, see + // https://github.com/WebAssembly/spec/issues/1138. The WPT tests expect + // the raw value of the Content-Type attribute lowercased. We ignore this + // for file:// because file fetches don't have a Content-Type. + if (!StringPrototypeStartsWith(res.url, "file://")) { + const contentType = res.headers.get("Content-Type"); + if ( + typeof contentType !== "string" || + StringPrototypeToLowerCase(contentType) !== "application/wasm" + ) { + throw new TypeError("Invalid WebAssembly content type."); } + } - // 2.5. - if (!res.ok) { - throw new TypeError(`HTTP status code ${res.status}`); - } + // 2.5. + if (!res.ok) { + throw new TypeError(`HTTP status code ${res.status}`); + } - // Pass the resolved URL to v8. - ops.op_wasm_streaming_set_url(rid, res.url); - - if (res.body !== null) { - // 2.6. - // Rather than consuming the body as an ArrayBuffer, this passes each - // chunk to the feed as soon as it's available. - PromisePrototypeThen( - (async () => { - const reader = res.body.getReader(); - while (true) { - const { value: chunk, done } = await reader.read(); - if (done) break; - ops.op_wasm_streaming_feed(rid, chunk); - } - })(), - // 2.7 - () => core.close(rid), - // 2.8 - (err) => core.abortWasmStreaming(rid, err), - ); - } else { + // Pass the resolved URL to v8. + ops.op_wasm_streaming_set_url(rid, res.url); + + if (res.body !== null) { + // 2.6. + // Rather than consuming the body as an ArrayBuffer, this passes each + // chunk to the feed as soon as it's available. + PromisePrototypeThen( + (async () => { + const reader = res.body.getReader(); + while (true) { + const { value: chunk, done } = await reader.read(); + if (done) break; + ops.op_wasm_streaming_feed(rid, chunk); + } + })(), // 2.7 - core.close(rid); - } - } catch (err) { - // 2.8 - core.abortWasmStreaming(rid, err); + () => core.close(rid), + // 2.8 + (err) => core.abortWasmStreaming(rid, err), + ); + } else { + // 2.7 + core.close(rid); } + } catch (err) { + // 2.8 + core.abortWasmStreaming(rid, err); } +} - window.__bootstrap.fetch ??= {}; - window.__bootstrap.fetch.fetch = fetch; - window.__bootstrap.fetch.handleWasmStreaming = handleWasmStreaming; -})(this); +export { fetch, handleWasmStreaming }; diff --git a/ext/fetch/internal.d.ts b/ext/fetch/internal.d.ts index 13a91d2d0..596e3ffcb 100644 --- a/ext/fetch/internal.d.ts +++ b/ext/fetch/internal.d.ts @@ -5,106 +5,98 @@ /// <reference no-default-lib="true" /> /// <reference lib="esnext" /> -declare namespace globalThis { - declare namespace __bootstrap { - declare var fetchUtil: { - requiredArguments(name: string, length: number, required: number): void; - }; +declare var domIterable: { + DomIterableMixin(base: any, dataSymbol: symbol): any; +}; - declare var domIterable: { - DomIterableMixin(base: any, dataSymbol: symbol): any; - }; - - declare namespace headers { - class Headers { - } - type HeaderList = [string, string][]; - function headersFromHeaderList( - list: HeaderList, - guard: - | "immutable" - | "request" - | "request-no-cors" - | "response" - | "none", - ): Headers; - function headerListFromHeaders(headers: Headers): HeaderList; - function fillHeaders(headers: Headers, object: HeadersInit): void; - function getDecodeSplitHeader( - list: HeaderList, - name: string, - ): string[] | null; - function guardFromHeaders( - headers: Headers, - ): "immutable" | "request" | "request-no-cors" | "response" | "none"; - } - - declare namespace formData { - declare type FormData = typeof FormData; - declare function formDataToBlob( - formData: globalThis.FormData, - ): Blob; - declare function parseFormData( - body: Uint8Array, - boundary: string | undefined, - ): FormData; - declare function formDataFromEntries(entries: FormDataEntry[]): FormData; - } +declare module "internal:ext/fetch/20_headers.js" { + class Headers { + } + type HeaderList = [string, string][]; + function headersFromHeaderList( + list: HeaderList, + guard: + | "immutable" + | "request" + | "request-no-cors" + | "response" + | "none", + ): Headers; + function headerListFromHeaders(headers: Headers): HeaderList; + function fillHeaders(headers: Headers, object: HeadersInit): void; + function getDecodeSplitHeader( + list: HeaderList, + name: string, + ): string[] | null; + function guardFromHeaders( + headers: Headers, + ): "immutable" | "request" | "request-no-cors" | "response" | "none"; +} - declare namespace fetchBody { - function mixinBody( - prototype: any, - bodySymbol: symbol, - mimeTypeSymbol: symbol, - ): void; - class InnerBody { - constructor(stream?: ReadableStream<Uint8Array>); - stream: ReadableStream<Uint8Array>; - source: null | Uint8Array | Blob | FormData; - length: null | number; - unusable(): boolean; - consume(): Promise<Uint8Array>; - clone(): InnerBody; - } - function extractBody(object: BodyInit): { - body: InnerBody; - contentType: string | null; - }; - } +declare module "internal:ext/fetch/21_formdata.js" { + type FormData = typeof FormData; + function formDataToBlob( + formData: FormData, + ): Blob; + function parseFormData( + body: Uint8Array, + boundary: string | undefined, + ): FormData; + function formDataFromEntries(entries: FormDataEntry[]): FormData; +} - declare namespace fetch { - function toInnerRequest(request: Request): InnerRequest; - function fromInnerRequest( - inner: InnerRequest, - signal: AbortSignal | null, - guard: - | "request" - | "immutable" - | "request-no-cors" - | "response" - | "none", - skipBody: boolean, - flash: boolean, - ): Request; - function redirectStatus(status: number): boolean; - function nullBodyStatus(status: number): boolean; - function newInnerRequest( - method: string, - url: any, - headerList?: [string, string][], - body?: globalThis.__bootstrap.fetchBody.InnerBody, - ): InnerResponse; - function toInnerResponse(response: Response): InnerResponse; - function fromInnerResponse( - inner: InnerResponse, - guard: - | "request" - | "immutable" - | "request-no-cors" - | "response" - | "none", - ): Response; - function networkError(error: string): InnerResponse; - } +declare module "internal:ext/fetch/22_body.js" { + function mixinBody( + prototype: any, + bodySymbol: symbol, + mimeTypeSymbol: symbol, + ): void; + class InnerBody { + constructor(stream?: ReadableStream<Uint8Array>); + stream: ReadableStream<Uint8Array>; + source: null | Uint8Array | Blob | FormData; + length: null | number; + unusable(): boolean; + consume(): Promise<Uint8Array>; + clone(): InnerBody; } + function extractBody(object: BodyInit): { + body: InnerBody; + contentType: string | null; + }; +} + +declare module "internal:ext/fetch/26_fetch.js" { + function toInnerRequest(request: Request): InnerRequest; + function fromInnerRequest( + inner: InnerRequest, + signal: AbortSignal | null, + guard: + | "request" + | "immutable" + | "request-no-cors" + | "response" + | "none", + skipBody: boolean, + flash: boolean, + ): Request; + function redirectStatus(status: number): boolean; + function nullBodyStatus(status: number): boolean; + function newInnerRequest( + method: string, + url: any, + headerList?: [string, string][], + body?: fetchBody.InnerBody, + ): InnerResponse; + function toInnerResponse(response: Response): InnerResponse; + function fromInnerResponse( + inner: InnerResponse, + guard: + | "request" + | "immutable" + | "request-no-cors" + | "response" + | "none", + ): Response; + function networkError(error: string): InnerResponse; } diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs index 78a42cd84..93c624dd6 100644 --- a/ext/fetch/lib.rs +++ b/ext/fetch/lib.rs @@ -97,9 +97,8 @@ where { Extension::builder(env!("CARGO_PKG_NAME")) .dependencies(vec!["deno_webidl", "deno_web", "deno_url", "deno_console"]) - .js(include_js_files!( + .esm(include_js_files!( prefix "internal:ext/fetch", - "01_fetch_util.js", "20_headers.js", "21_formdata.js", "22_body.js", |