summaryrefslogtreecommitdiff
path: root/ext/fetch
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fetch')
-rw-r--r--ext/fetch/01_fetch_util.js22
-rw-r--r--ext/fetch/20_headers.js479
-rw-r--r--ext/fetch/21_formdata.js507
-rw-r--r--ext/fetch/22_body.js403
-rw-r--r--ext/fetch/22_http_client.js40
-rw-r--r--ext/fetch/23_request.js484
-rw-r--r--ext/fetch/23_response.js451
-rw-r--r--ext/fetch/26_fetch.js542
-rw-r--r--ext/fetch/Cargo.toml28
-rw-r--r--ext/fetch/README.md5
-rw-r--r--ext/fetch/internal.d.ts108
-rw-r--r--ext/fetch/lib.deno_fetch.d.ts437
-rw-r--r--ext/fetch/lib.rs567
13 files changed, 4073 insertions, 0 deletions
diff --git a/ext/fetch/01_fetch_util.js b/ext/fetch/01_fetch_util.js
new file mode 100644
index 000000000..9cf19588b
--- /dev/null
+++ b/ext/fetch/01_fetch_util.js
@@ -0,0 +1,22 @@
+// Copyright 2018-2021 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
new file mode 100644
index 000000000..91154d958
--- /dev/null
+++ b/ext/fetch/20_headers.js
@@ -0,0 +1,479 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <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_WHITESPACE_PREFIX_RE,
+ HTTP_WHITESPACE_SUFFIX_RE,
+ HTTP_TOKEN_CODE_POINT_RE,
+ byteLowerCase,
+ collectSequenceOfCodepoints,
+ collectHttpQuotedString,
+ } = window.__bootstrap.infra;
+ const {
+ ArrayIsArray,
+ ArrayPrototypeMap,
+ ArrayPrototypePush,
+ ArrayPrototypeSort,
+ ArrayPrototypeJoin,
+ ArrayPrototypeSplice,
+ ArrayPrototypeFilter,
+ ObjectKeys,
+ ObjectEntries,
+ RegExpPrototypeTest,
+ Symbol,
+ SymbolFor,
+ SymbolIterator,
+ SymbolToStringTag,
+ StringPrototypeReplaceAll,
+ StringPrototypeIncludes,
+ 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[]}
+ */
+
+ /**
+ * @param {string} potentialValue
+ * @returns {string}
+ */
+ function normalizeHeaderValue(potentialValue) {
+ potentialValue = StringPrototypeReplaceAll(
+ potentialValue,
+ HTTP_WHITESPACE_PREFIX_RE,
+ "",
+ );
+ potentialValue = StringPrototypeReplaceAll(
+ potentialValue,
+ HTTP_WHITESPACE_SUFFIX_RE,
+ "",
+ );
+ return potentialValue;
+ }
+
+ /**
+ * @param {Headers} headers
+ * @param {HeadersInit} object
+ */
+ function fillHeaders(headers, object) {
+ if (ArrayIsArray(object)) {
+ for (const header of object) {
+ 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 of ObjectKeys(object)) {
+ appendHeader(headers, key, object[key]);
+ }
+ }
+ }
+
+ /**
+ * 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 (
+ StringPrototypeIncludes(value, "\x00") ||
+ StringPrototypeIncludes(value, "\x0A") ||
+ StringPrototypeIncludes(value, "\x0D")
+ ) {
+ throw new TypeError("Header value is not valid.");
+ }
+
+ // 3.
+ if (headers[_guard] == "immutable") {
+ throw new TypeError("Headers are immutable.");
+ }
+
+ // 7.
+ const list = headers[_headerList];
+ name = byteLowerCase(name);
+ 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) => 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;
+ }
+ } 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, "");
+
+ 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 (const entry of list) {
+ const name = 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 concatentated,
+ // 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;
+ }
+ }
+
+ return ArrayPrototypeSort(
+ [...ObjectEntries(headers), ...cookies],
+ (a, b) => {
+ const akey = a[0];
+ const bkey = b[0];
+ if (akey > bkey) return 1;
+ if (akey < bkey) return -1;
+ return 0;
+ },
+ );
+ }
+
+ /** @param {HeadersInit} [init] */
+ constructor(init = undefined) {
+ const prefix = "Failed to construct 'Event'";
+ 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);
+ }
+ }
+
+ /**
+ * @param {string} name
+ * @param {string} value
+ */
+ append(name, value) {
+ webidl.assertBranded(this, Headers);
+ 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",
+ });
+
+ 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];
+ name = byteLowerCase(name);
+ for (let i = 0; i < list.length; i++) {
+ if (list[i][0] === name) {
+ ArrayPrototypeSplice(list, i, 1);
+ i--;
+ }
+ }
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @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];
+ name = byteLowerCase(name);
+ for (let i = 0; i < list.length; i++) {
+ if (list[i][0] === name) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param {string} name
+ * @param {string} value
+ */
+ set(name, value) {
+ webidl.assertBranded(this, Headers);
+ 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 (
+ StringPrototypeIncludes(value, "\x00") ||
+ StringPrototypeIncludes(value, "\x0A") ||
+ StringPrototypeIncludes(value, "\x0D")
+ ) {
+ throw new TypeError("Header value is not valid.");
+ }
+
+ if (this[_guard] == "immutable") {
+ throw new TypeError("Headers are immutable.");
+ }
+
+ const list = this[_headerList];
+ name = byteLowerCase(name);
+ let added = false;
+ for (let i = 0; i < list.length; i++) {
+ if (list[i][0] === name) {
+ if (!added) {
+ list[i][1] = value;
+ added = true;
+ } else {
+ ArrayPrototypeSplice(list, i, 1);
+ i--;
+ }
+ }
+ }
+ if (!added) {
+ ArrayPrototypePush(list, [name, value]);
+ }
+ }
+
+ [SymbolFor("Deno.privateCustomInspect")](inspect) {
+ const headers = {};
+ for (const header of this) {
+ headers[header[0]] = header[1];
+ }
+ return `Headers ${inspect(headers)}`;
+ }
+
+ get [SymbolToStringTag]() {
+ return "Headers";
+ }
+ }
+
+ webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1);
+
+ webidl.configurePrototype(Headers);
+
+ 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);
+ }
+ 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,
+ );
+
+ /**
+ * @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}
+ * @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 = {
+ Headers,
+ headersFromHeaderList,
+ headerListFromHeaders,
+ fillHeaders,
+ getDecodeSplitHeader,
+ guardFromHeaders,
+ };
+})(this);
diff --git a/ext/fetch/21_formdata.js b/ext/fetch/21_formdata.js
new file mode 100644
index 000000000..25ed32c2d
--- /dev/null
+++ b/ext/fetch/21_formdata.js
@@ -0,0 +1,507 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <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, File } = globalThis.__bootstrap.file;
+ const {
+ ArrayPrototypeMap,
+ ArrayPrototypePush,
+ ArrayPrototypeSlice,
+ ArrayPrototypeSplice,
+ ArrayPrototypeFilter,
+ ArrayPrototypeForEach,
+ Map,
+ MapPrototypeGet,
+ MapPrototypeSet,
+ MathRandom,
+ Symbol,
+ SymbolToStringTag,
+ 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 (value instanceof Blob && !(value instanceof File)) {
+ value = new File([value], "blob", { type: value.type });
+ }
+ if (value instanceof File && 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 {
+ get [SymbolToStringTag]() {
+ return "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, FormData);
+ const prefix = "Failed to execute 'append' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ if (valueOrBlobValue instanceof Blob) {
+ 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, {
+ prefix,
+ context: "Argument 2",
+ });
+ }
+
+ const entry = createEntry(name, valueOrBlobValue, filename);
+
+ ArrayPrototypePush(this[entryList], entry);
+ }
+
+ /**
+ * @param {string} name
+ * @returns {void}
+ */
+ delete(name) {
+ webidl.assertBranded(this, FormData);
+ 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--;
+ }
+ }
+ }
+
+ /**
+ * @param {string} name
+ * @returns {FormDataEntryValue | null}
+ */
+ get(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'get' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ for (const entry of this[entryList]) {
+ if (entry.name === name) return entry.value;
+ }
+ return null;
+ }
+
+ /**
+ * @param {string} name
+ * @returns {FormDataEntryValue[]}
+ */
+ getAll(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'getAll' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ const returnList = [];
+ for (const entry of this[entryList]) {
+ if (entry.name === name) ArrayPrototypePush(returnList, entry.value);
+ }
+ return returnList;
+ }
+
+ /**
+ * @param {string} name
+ * @returns {boolean}
+ */
+ has(name) {
+ webidl.assertBranded(this, FormData);
+ const prefix = "Failed to execute 'has' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+
+ for (const entry of this[entryList]) {
+ 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, FormData);
+ const prefix = "Failed to execute 'set' on 'FormData'";
+ webidl.requiredArguments(arguments.length, 2, { prefix });
+
+ name = webidl.converters["USVString"](name, {
+ prefix,
+ context: "Argument 1",
+ });
+ if (valueOrBlobValue instanceof Blob) {
+ 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, {
+ prefix,
+ context: "Argument 2",
+ });
+ }
+
+ 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--;
+ }
+ }
+ }
+ if (!added) {
+ ArrayPrototypePush(list, entry);
+ }
+ }
+ }
+
+ webidl.mixinPairIterable("FormData", FormData, entryList, "name", "value");
+
+ webidl.configurePrototype(FormData);
+
+ const escape = (str, isFilename) =>
+ StringPrototypeReplace(
+ StringPrototypeReplace(
+ StringPrototypeReplace(
+ (isFilename ? str : StringPrototypeReplace(str, /\r?\n|\r/g, "\r\n")),
+ /\n/g,
+ "%0A",
+ ),
+ /\r/g,
+ "%0D",
+ ),
+ /"/g,
+ "%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="`;
+
+ for (const [name, 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,
+ });
+ }
+
+ /**
+ * @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
+ ArrayPrototypeForEach(
+ ArrayPrototypeMap(
+ ArrayPrototypeFilter(
+ ArrayPrototypeMap(
+ ArrayPrototypeSlice(StringPrototypeSplit(value, ";"), 1),
+ (s) => StringPrototypeSplit(StringPrototypeTrim(s), "="),
+ ),
+ (arr) => arr.length > 1,
+ ),
+ ([k, v]) => [k, StringPrototypeReplace(v, /^"([^"]*)"$/, "$1")],
+ ),
+ ([k, v]) => MapPrototypeSet(params, k, v),
+ );
+
+ return params;
+ }
+
+ 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");
+ }
+
+ this.boundary = `--${boundary}`;
+ this.body = body;
+ this.boundaryChars = core.encode(this.boundary);
+ }
+
+ /**
+ * @param {string} headersText
+ * @returns {{ headers: Headers, disposition: Map<string, string> }}
+ */
+ #parseHeaders(headersText) {
+ const headers = new Headers();
+ const rawHeaders = StringPrototypeSplit(headersText, "\r\n");
+ for (const rawHeader of rawHeaders) {
+ 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") ?? "",
+ );
+
+ return { headers, disposition };
+ }
+
+ /**
+ * @returns {FormData}
+ */
+ parse() {
+ // Body must be at least 2 boundaries + \r\n + -- on the last boundary.
+ if (this.body.length < (this.boundary.length * 2) + 4) {
+ throw new TypeError("Form data too short to be valid.");
+ }
+
+ 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;
+
+ 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) {
+ 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 (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;
+ }
+ }
+
+ 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;
+ }
+
+ webidl.converters["FormData"] = webidl
+ .createInterfaceConverter("FormData", FormData);
+
+ globalThis.__bootstrap.formData = {
+ FormData,
+ formDataToBlob,
+ parseFormData,
+ formDataFromEntries,
+ };
+})(globalThis);
diff --git a/ext/fetch/22_body.js b/ext/fetch/22_body.js
new file mode 100644
index 000000000..49da149c2
--- /dev/null
+++ b/ext/fetch/22_body.js
@@ -0,0 +1,403 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
+/// <reference path="../url/lib.deno_url.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <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 { parseFormData, formDataFromEntries, formDataToBlob } =
+ globalThis.__bootstrap.formData;
+ const mimesniff = globalThis.__bootstrap.mimesniff;
+ const { isReadableStreamDisturbed, errorReadableStream, createProxy } =
+ globalThis.__bootstrap.streams;
+ const {
+ ArrayBuffer,
+ ArrayBufferIsView,
+ ArrayPrototypePush,
+ ArrayPrototypeMap,
+ JSONParse,
+ ObjectDefineProperties,
+ PromiseResolve,
+ TypedArrayPrototypeSet,
+ TypedArrayPrototypeSlice,
+ TypeError,
+ Uint8Array,
+ } = window.__bootstrap.primordials;
+
+ class InnerBody {
+ /** @type {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} */
+ streamOrStatic;
+ /** @type {null | Uint8Array | Blob | FormData} */
+ source = null;
+ /** @type {null | number} */
+ length = null;
+
+ /**
+ * @param {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} stream
+ */
+ constructor(stream) {
+ this.streamOrStatic = stream ??
+ { body: new Uint8Array(), consumed: false };
+ }
+
+ get stream() {
+ if (!(this.streamOrStatic instanceof ReadableStream)) {
+ const { body, consumed } = this.streamOrStatic;
+ if (consumed) {
+ this.streamOrStatic = new ReadableStream();
+ this.streamOrStatic.getReader();
+ } else {
+ this.streamOrStatic = new ReadableStream({
+ start(controller) {
+ controller.enqueue(body);
+ controller.close();
+ },
+ });
+ }
+ }
+ return this.streamOrStatic;
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#body-unusable
+ * @returns {boolean}
+ */
+ unusable() {
+ if (this.streamOrStatic instanceof ReadableStream) {
+ return this.streamOrStatic.locked ||
+ isReadableStreamDisturbed(this.streamOrStatic);
+ }
+ return this.streamOrStatic.consumed;
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ consumed() {
+ if (this.streamOrStatic instanceof ReadableStream) {
+ return isReadableStreamDisturbed(this.streamOrStatic);
+ }
+ return this.streamOrStatic.consumed;
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#concept-body-consume-body
+ * @returns {Promise<Uint8Array>}
+ */
+ async consume() {
+ if (this.unusable()) throw new TypeError("Body already consumed.");
+ if (this.streamOrStatic instanceof ReadableStream) {
+ const reader = this.stream.getReader();
+ /** @type {Uint8Array[]} */
+ const chunks = [];
+ let totalLength = 0;
+ while (true) {
+ const { value: chunk, done } = await reader.read();
+ if (done) break;
+ ArrayPrototypePush(chunks, chunk);
+ totalLength += chunk.byteLength;
+ }
+ const finalBuffer = new Uint8Array(totalLength);
+ let i = 0;
+ for (const chunk of chunks) {
+ TypedArrayPrototypeSet(finalBuffer, chunk, i);
+ i += chunk.byteLength;
+ }
+ return finalBuffer;
+ } else {
+ this.streamOrStatic.consumed = true;
+ return this.streamOrStatic.body;
+ }
+ }
+
+ cancel(error) {
+ if (this.streamOrStatic instanceof ReadableStream) {
+ this.streamOrStatic.cancel(error);
+ } else {
+ this.streamOrStatic.consumed = true;
+ }
+ }
+
+ error(error) {
+ if (this.streamOrStatic instanceof ReadableStream) {
+ errorReadableStream(this.streamOrStatic, error);
+ } else {
+ this.streamOrStatic.consumed = true;
+ }
+ }
+
+ /**
+ * @returns {InnerBody}
+ */
+ clone() {
+ const [out1, 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;
+ }
+
+ /**
+ * @returns {InnerBody}
+ */
+ createProxy() {
+ let proxyStreamOrStatic;
+ if (this.streamOrStatic instanceof ReadableStream) {
+ 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;
+ }
+ }
+
+ /**
+ * @param {any} prototype
+ * @param {symbol} bodySymbol
+ * @param {symbol} mimeTypeSymbol
+ * @returns {void}
+ */
+ function mixinBody(prototype, bodySymbol, mimeTypeSymbol) {
+ function consumeBody(object) {
+ if (object[bodySymbol] !== null) {
+ return object[bodySymbol].consume();
+ }
+ return PromiseResolve(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,
+ },
+ bodyUsed: {
+ /**
+ * @returns {boolean}
+ */
+ get() {
+ webidl.assertBranded(this, prototype);
+ if (this[bodySymbol] !== null) {
+ return this[bodySymbol].consumed();
+ }
+ return false;
+ },
+ configurable: true,
+ enumerable: true,
+ },
+ arrayBuffer: {
+ /** @returns {Promise<ArrayBuffer>} */
+ value: async function arrayBuffer() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "ArrayBuffer");
+ },
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ },
+ blob: {
+ /** @returns {Promise<Blob>} */
+ value: async function blob() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "Blob", this[mimeTypeSymbol]);
+ },
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ },
+ formData: {
+ /** @returns {Promise<FormData>} */
+ value: async function formData() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "FormData", this[mimeTypeSymbol]);
+ },
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ },
+ json: {
+ /** @returns {Promise<any>} */
+ value: async function json() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "JSON");
+ },
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ },
+ text: {
+ /** @returns {Promise<string>} */
+ value: async function text() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "text");
+ },
+ writable: true,
+ configurable: true,
+ enumerable: true,
+ },
+ };
+ return ObjectDefineProperties(prototype.prototype, mixin);
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#concept-body-package-data
+ * @param {Uint8Array} bytes
+ * @param {"ArrayBuffer" | "Blob" | "FormData" | "JSON" | "text"} type
+ * @param {MimeType | null} [mimeType]
+ */
+ function packageData(bytes, type, mimeType) {
+ switch (type) {
+ case "ArrayBuffer":
+ return 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(bytes, boundary);
+ } else if (essence === "application/x-www-form-urlencoded") {
+ const entries = parseUrlEncoded(bytes);
+ return formDataFromEntries(
+ ArrayPrototypeMap(
+ entries,
+ (x) => ({ name: x[0], value: x[1] }),
+ ),
+ );
+ }
+ throw new TypeError("Body can not be decoded as form data");
+ }
+ throw new TypeError("Missing content type");
+ }
+ case "JSON":
+ return JSONParse(core.decode(bytes));
+ case "text":
+ return core.decode(bytes);
+ }
+ }
+
+ /**
+ * @param {BodyInit} object
+ * @returns {{body: InnerBody, contentType: string | null}}
+ */
+ function extractBody(object) {
+ /** @type {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} */
+ let stream;
+ let source = null;
+ let length = null;
+ let contentType = null;
+ if (object instanceof Blob) {
+ stream = object.stream();
+ source = object;
+ length = object.size;
+ if (object.type.length !== 0) {
+ contentType = object.type;
+ }
+ } else if (ArrayBufferIsView(object) || object instanceof ArrayBuffer) {
+ 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 (object instanceof FormData) {
+ const res = formDataToBlob(object);
+ stream = res.stream();
+ source = res;
+ length = res.size;
+ contentType = res.type;
+ } else if (object instanceof URLSearchParams) {
+ // TODO(@satyarohith): not sure what primordial here.
+ source = core.encode(object.toString());
+ contentType = "application/x-www-form-urlencoded;charset=UTF-8";
+ } else if (typeof object === "string") {
+ source = core.encode(object);
+ contentType = "text/plain;charset=UTF-8";
+ } else if (object instanceof ReadableStream) {
+ stream = object;
+ if (object.locked || isReadableStreamDisturbed(object)) {
+ throw new TypeError("ReadableStream is locked or disturbed");
+ }
+ }
+ if (source instanceof Uint8Array) {
+ 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"] = (V, opts) => {
+ // Union for (ReadableStream or Blob or ArrayBufferView or ArrayBuffer or FormData or URLSearchParams or USVString)
+ if (V instanceof ReadableStream) {
+ // TODO(lucacasonato): ReadableStream is not branded
+ return V;
+ } else if (V instanceof Blob) {
+ return webidl.converters["Blob"](V, opts);
+ } else if (V instanceof FormData) {
+ return webidl.converters["FormData"](V, opts);
+ } else if (V instanceof URLSearchParams) {
+ // TODO(lucacasonato): URLSearchParams is not branded
+ return V;
+ }
+ if (typeof V === "object") {
+ if (V instanceof ArrayBuffer || V instanceof SharedArrayBuffer) {
+ return webidl.converters["ArrayBuffer"](V, opts);
+ }
+ if (ArrayBufferIsView(V)) {
+ return webidl.converters["ArrayBufferView"](V, opts);
+ }
+ }
+ return webidl.converters["USVString"](V, opts);
+ };
+ webidl.converters["BodyInit?"] = webidl.createNullableConverter(
+ webidl.converters["BodyInit"],
+ );
+
+ window.__bootstrap.fetchBody = { mixinBody, InnerBody, extractBody };
+})(globalThis);
diff --git a/ext/fetch/22_http_client.js b/ext/fetch/22_http_client.js
new file mode 100644
index 000000000..60b069aa7
--- /dev/null
+++ b/ext/fetch/22_http_client.js
@@ -0,0 +1,40 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <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;
+
+ /**
+ * @param {Deno.CreateHttpClientOptions} options
+ * @returns {HttpClient}
+ */
+ function createHttpClient(options) {
+ return new HttpClient(core.opSync("op_create_http_client", options));
+ }
+
+ class HttpClient {
+ /**
+ * @param {number} rid
+ */
+ constructor(rid) {
+ this.rid = rid;
+ }
+ close() {
+ core.close(this.rid);
+ }
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.createHttpClient = createHttpClient;
+ window.__bootstrap.fetch.HttpClient = HttpClient;
+})(globalThis);
diff --git a/ext/fetch/23_request.js b/ext/fetch/23_request.js
new file mode 100644
index 000000000..1372125c1
--- /dev/null
+++ b/ext/fetch/23_request.js
@@ -0,0 +1,484 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <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 } = window.__bootstrap.fetchBody;
+ const { getLocationHref } = window.__bootstrap.location;
+ const mimesniff = window.__bootstrap.mimesniff;
+ const {
+ headersFromHeaderList,
+ headerListFromHeaders,
+ fillHeaders,
+ getDecodeSplitHeader,
+ } = window.__bootstrap.headers;
+ const { HttpClient } = window.__bootstrap.fetch;
+ const abortSignal = window.__bootstrap.abortSignal;
+ const {
+ ArrayPrototypeMap,
+ ArrayPrototypeSlice,
+ ArrayPrototypeSplice,
+ MapPrototypeHas,
+ MapPrototypeGet,
+ MapPrototypeSet,
+ ObjectKeys,
+ RegExpPrototypeTest,
+ Symbol,
+ SymbolFor,
+ SymbolToStringTag,
+ TypeError,
+ } = window.__bootstrap.primordials;
+
+ const _request = Symbol("request");
+ const _headers = Symbol("headers");
+ const _signal = Symbol("signal");
+ const _mimeType = Symbol("mime type");
+ const _body = Symbol("body");
+
+ /**
+ * @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 {number | null} clientRid NOTE: non standard extension for `Deno.HttpClient`.
+ */
+
+ const defaultInnerRequest = {
+ url() {
+ return this.urlList[0];
+ },
+ currentUrl() {
+ return this.urlList[this.urlList.length - 1];
+ },
+ redirectMode: "follow",
+ redirectCount: 0,
+ clientRid: null,
+ };
+
+ /**
+ * @param {string} method
+ * @param {string} url
+ * @param {[string, string][]} headerList
+ * @param {typeof __window.bootstrap.fetchBody.InnerBody} body
+ * @returns
+ */
+ function newInnerRequest(method, url, headerList = [], body = null) {
+ return {
+ method: method,
+ headerList,
+ body,
+ urlList: [url],
+ ...defaultInnerRequest,
+ };
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#concept-request-clone
+ * @param {InnerRequest} request
+ * @returns {InnerRequest}
+ */
+ function cloneInnerRequest(request) {
+ const headerList = [
+ ...ArrayPrototypeMap(request.headerList, (x) => [x[0], x[1]]),
+ ];
+ let body = null;
+ if (request.body !== null) {
+ body = request.body.clone();
+ }
+
+ return {
+ method: request.method,
+ url() {
+ return this.urlList[0];
+ },
+ currentUrl() {
+ return this.urlList[this.urlList.length - 1];
+ },
+ headerList,
+ body,
+ redirectMode: request.redirectMode,
+ redirectCount: request.redirectCount,
+ urlList: request.urlList,
+ clientRid: request.clientRid,
+ };
+ }
+
+ /**
+ * @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;
+ }
+
+ // 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.");
+ }
+ return upperCase;
+ }
+
+ class Request {
+ /** @type {InnerRequest} */
+ [_request];
+ /** @type {Headers} */
+ [_headers];
+ /** @type {AbortSignal} */
+ [_signal];
+ get [_mimeType]() {
+ let charset = null;
+ let essence = null;
+ let mimeType = null;
+ const headerList = headerListFromHeaders(this[_headers]);
+ const values = getDecodeSplitHeader(headerList, "content-type");
+ if (values === null) return null;
+ for (const value of values) {
+ const temporaryMimeType = mimesniff.parseMimeType(value);
+ if (
+ temporaryMimeType === null ||
+ mimesniff.essence(temporaryMimeType) == "*/*"
+ ) {
+ continue;
+ }
+ mimeType = temporaryMimeType;
+ if (mimesniff.essence(mimeType) !== essence) {
+ charset = null;
+ const newCharset = MapPrototypeGet(mimeType.parameters, "charset");
+ if (newCharset !== undefined) {
+ charset = newCharset;
+ }
+ essence = mimesniff.essence(mimeType);
+ } else {
+ if (
+ MapPrototypeHas(mimeType.parameters, "charset") === null &&
+ charset !== null
+ ) {
+ MapPrototypeSet(mimeType.parameters, "charset", charset);
+ }
+ }
+ }
+ if (mimeType === null) return null;
+ return mimeType;
+ }
+ get [_body]() {
+ return this[_request].body;
+ }
+
+ /**
+ * 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"](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);
+ } else { // 6.
+ if (!(input instanceof Request)) throw new TypeError("Unreachable");
+ request = input[_request];
+ signal = input[_signal];
+ }
+
+ // 12.
+ // TODO(lucacasonato): create a copy of `request`
+
+ // 22.
+ if (init.redirect !== undefined) {
+ request.redirectMode = init.redirect;
+ }
+
+ // 25.
+ if (init.method !== undefined) {
+ let method = init.method;
+ method = validateAndNormalizeMethod(method);
+ request.method = method;
+ }
+
+ // 26.
+ if (init.signal !== undefined) {
+ signal = init.signal;
+ }
+
+ // NOTE: non standard extension. This handles Deno.HttpClient parameter
+ if (init.client !== undefined) {
+ if (init.client !== null && !(init.client instanceof HttpClient)) {
+ throw webidl.makeException(
+ TypeError,
+ "`client` must be a Deno.HttpClient",
+ { prefix, context: "Argument 2" },
+ );
+ }
+ request.clientRid = init.client?.rid ?? null;
+ }
+
+ // 27.
+ this[_request] = request;
+
+ // 28.
+ this[_signal] = abortSignal.newSignal();
+
+ // 29.
+ if (signal !== null) {
+ abortSignal.follow(this[_signal], signal);
+ }
+
+ // 30.
+ this[_headers] = headersFromHeaderList(request.headerList, "request");
+
+ // 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);
+ }
+
+ // 33.
+ let inputBody = null;
+ if (input instanceof Request) {
+ inputBody = input[_body];
+ }
+
+ // 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.");
+ }
+
+ // 35.
+ let initBody = null;
+
+ // 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);
+ }
+ }
+
+ // 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;
+ }
+
+ get method() {
+ webidl.assertBranded(this, Request);
+ return this[_request].method;
+ }
+
+ get url() {
+ webidl.assertBranded(this, Request);
+ return this[_request].url();
+ }
+
+ get headers() {
+ webidl.assertBranded(this, Request);
+ return this[_headers];
+ }
+
+ get redirect() {
+ webidl.assertBranded(this, Request);
+ return this[_request].redirectMode;
+ }
+
+ get signal() {
+ webidl.assertBranded(this, Request);
+ return this[_signal];
+ }
+
+ clone() {
+ webidl.assertBranded(this, Request);
+ if (this[_body] && this[_body].unusable()) {
+ throw new TypeError("Body is unusable.");
+ }
+ const newReq = cloneInnerRequest(this[_request]);
+ const newSignal = abortSignal.newSignal();
+ abortSignal.follow(newSignal, this[_signal]);
+ return fromInnerRequest(
+ newReq,
+ newSignal,
+ guardFromHeaders(this[_headers]),
+ );
+ }
+
+ get [SymbolToStringTag]() {
+ return "Request";
+ }
+
+ [SymbolFor("Deno.customInspect")](inspect) {
+ return inspect(consoleInternal.createFilteredInspectProxy({
+ object: this,
+ evaluate: this instanceof Request,
+ keys: [
+ "bodyUsed",
+ "headers",
+ "method",
+ "redirect",
+ "url",
+ ],
+ }));
+ }
+ }
+
+ mixinBody(Request, _body, _mimeType);
+
+ webidl.configurePrototype(Request);
+
+ webidl.converters["Request"] = webidl.createInterfaceConverter(
+ "Request",
+ Request,
+ );
+ webidl.converters["RequestInfo"] = (V, opts) => {
+ // Union for (Request or USVString)
+ if (typeof V == "object") {
+ if (V instanceof Request) {
+ return webidl.converters["Request"](V, opts);
+ }
+ }
+ return webidl.converters["USVString"](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"],
+ ),
+ },
+ { 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 {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard
+ * @returns {Request}
+ */
+ function fromInnerRequest(inner, signal, guard) {
+ const request = webidl.createBranded(Request);
+ request[_request] = inner;
+ request[_signal] = signal;
+ request[_headers] = headersFromHeaderList(inner.headerList, guard);
+ return request;
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.Request = Request;
+ window.__bootstrap.fetch.toInnerRequest = toInnerRequest;
+ window.__bootstrap.fetch.fromInnerRequest = fromInnerRequest;
+ window.__bootstrap.fetch.newInnerRequest = newInnerRequest;
+})(globalThis);
diff --git a/ext/fetch/23_response.js b/ext/fetch/23_response.js
new file mode 100644
index 000000000..0db20e90e
--- /dev/null
+++ b/ext/fetch/23_response.js
@@ -0,0 +1,451 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <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_TAB_OR_SPACE, regexMatcher } = window.__bootstrap.infra;
+ const { extractBody, mixinBody } = window.__bootstrap.fetchBody;
+ const { getLocationHref } = window.__bootstrap.location;
+ const mimesniff = window.__bootstrap.mimesniff;
+ const { URL } = window.__bootstrap.url;
+ const {
+ getDecodeSplitHeader,
+ headerListFromHeaders,
+ headersFromHeaderList,
+ guardFromHeaders,
+ fillHeaders,
+ } = window.__bootstrap.headers;
+ const {
+ ArrayPrototypeMap,
+ ArrayPrototypePush,
+ MapPrototypeHas,
+ MapPrototypeGet,
+ MapPrototypeSet,
+ RangeError,
+ RegExp,
+ RegExpPrototypeTest,
+ Symbol,
+ SymbolFor,
+ SymbolToStringTag,
+ TypeError,
+ } = window.__bootstrap.primordials;
+
+ const VCHAR = ["\x21-\x7E"];
+ const OBS_TEXT = ["\x80-\xFF"];
+
+ const REASON_PHRASE = [...HTTP_TAB_OR_SPACE, ...VCHAR, ...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 = [...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,
+ url() {
+ if (this.urlList.length == 0) return null;
+ return this.urlList[this.urlList.length - 1];
+ },
+ urlList,
+ status: response.status,
+ statusMessage: response.statusMessage,
+ aborted: response.aborted,
+ };
+ }
+
+ const defaultInnerResponse = {
+ type: "default",
+ body: null,
+ aborted: false,
+ url() {
+ if (this.urlList.length == 0) return null;
+ return this.urlList[this.urlList.length - 1];
+ },
+ };
+
+ /**
+ * @returns {InnerResponse}
+ */
+ function newInnerResponse(status = 200, statusMessage = "") {
+ return {
+ headerList: [],
+ urlList: [],
+ status,
+ statusMessage,
+ ...defaultInnerResponse,
+ };
+ }
+
+ /**
+ * @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;
+ }
+
+ class Response {
+ /** @type {InnerResponse} */
+ [_response];
+ /** @type {Headers} */
+ [_headers];
+ get [_mimeType]() {
+ let charset = null;
+ let essence = null;
+ let mimeType = null;
+ const headerList = headerListFromHeaders(this[_headers]);
+ const values = getDecodeSplitHeader(headerList, "content-type");
+ if (values === null) return null;
+ for (const value of values) {
+ const temporaryMimeType = mimesniff.parseMimeType(value);
+ if (
+ temporaryMimeType === null ||
+ mimesniff.essence(temporaryMimeType) == "*/*"
+ ) {
+ continue;
+ }
+ mimeType = temporaryMimeType;
+ if (mimesniff.essence(mimeType) !== essence) {
+ charset = null;
+ const newCharset = MapPrototypeGet(mimeType.parameters, "charset");
+ if (newCharset !== undefined) {
+ charset = newCharset;
+ }
+ essence = mimesniff.essence(mimeType);
+ } else {
+ if (
+ MapPrototypeHas(mimeType.parameters, "charset") === null &&
+ charset !== null
+ ) {
+ MapPrototypeSet(mimeType.parameters, "charset", charset);
+ }
+ }
+ }
+ if (mimeType === null) return null;
+ return mimeType;
+ }
+ 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 {BodyInit | null} body
+ * @param {ResponseInit} init
+ */
+ constructor(body = null, init = {}) {
+ const prefix = "Failed to construct 'Response'";
+ body = webidl.converters["BodyInit?"](body, {
+ prefix,
+ context: "Argument 1",
+ });
+ init = webidl.converters["ResponseInit"](init, {
+ prefix,
+ context: "Argument 2",
+ });
+
+ if (init.status < 200 || init.status > 599) {
+ throw new RangeError(
+ `The status provided (${init.status}) is outside the range [200, 599].`,
+ );
+ }
+
+ if (!RegExpPrototypeTest(REASON_PHRASE_RE, init.statusText)) {
+ throw new TypeError("Status text is not valid.");
+ }
+
+ this[webidl.brand] = webidl.brand;
+ const response = newInnerResponse(init.status, init.statusText);
+ this[_response] = response;
+ this[_headers] = headersFromHeaderList(response.headerList, "response");
+ if (init.headers !== undefined) {
+ fillHeaders(this[_headers], init.headers);
+ }
+ if (body !== null) {
+ if (nullBodyStatus(response.status)) {
+ throw new TypeError(
+ "Response with null body status cannot have body",
+ );
+ }
+ const res = extractBody(body);
+ response.body = res.body;
+ if (res.contentType !== null && !this[_headers].has("content-type")) {
+ this[_headers].append("Content-Type", res.contentType);
+ }
+ }
+ }
+
+ /**
+ * @returns {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"}
+ */
+ get type() {
+ webidl.assertBranded(this, Response);
+ return this[_response].type;
+ }
+
+ /**
+ * @returns {string}
+ */
+ get url() {
+ webidl.assertBranded(this, Response);
+ 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, Response);
+ return this[_response].urlList.length > 1;
+ }
+
+ /**
+ * @returns {number}
+ */
+ get status() {
+ webidl.assertBranded(this, Response);
+ return this[_response].status;
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ get ok() {
+ webidl.assertBranded(this, Response);
+ const status = this[_response].status;
+ return status >= 200 && status <= 299;
+ }
+
+ /**
+ * @returns {string}
+ */
+ get statusText() {
+ webidl.assertBranded(this, Response);
+ return this[_response].statusMessage;
+ }
+
+ /**
+ * @returns {Headers}
+ */
+ get headers() {
+ webidl.assertBranded(this, Response);
+ return this[_headers];
+ }
+
+ /**
+ * @returns {Response}
+ */
+ clone() {
+ webidl.assertBranded(this, Response);
+ 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;
+ }
+
+ get [SymbolToStringTag]() {
+ return "Response";
+ }
+
+ [SymbolFor("Deno.customInspect")](inspect) {
+ return inspect(consoleInternal.createFilteredInspectProxy({
+ object: this,
+ evaluate: this instanceof Response,
+ keys: [
+ "body",
+ "bodyUsed",
+ "headers",
+ "ok",
+ "redirected",
+ "status",
+ "statusText",
+ "url",
+ ],
+ }));
+ }
+ }
+
+ mixinBody(Response, _body, _mimeType);
+
+ webidl.configurePrototype(Response);
+
+ webidl.converters["Response"] = webidl.createInterfaceConverter(
+ "Response",
+ Response,
+ );
+ 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"],
+ }],
+ );
+
+ /**
+ * @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;
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.Response = Response;
+ 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);
diff --git a/ext/fetch/26_fetch.js b/ext/fetch/26_fetch.js
new file mode 100644
index 000000000..f7166001e
--- /dev/null
+++ b/ext/fetch/26_fetch.js
@@ -0,0 +1,542 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../../core/lib.deno_core.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="../web/06_streams_types.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <reference path="./lib.deno_fetch.d.ts" />
+/// <reference lib="esnext" />
+"use strict";
+
+((window) => {
+ const core = window.Deno.core;
+ const webidl = window.__bootstrap.webidl;
+ const { errorReadableStream } = window.__bootstrap.streams;
+ const { InnerBody, extractBody } = window.__bootstrap.fetchBody;
+ const {
+ toInnerRequest,
+ toInnerResponse,
+ fromInnerResponse,
+ redirectStatus,
+ nullBodyStatus,
+ networkError,
+ abortedNetworkError,
+ } = window.__bootstrap.fetch;
+ const abortSignal = window.__bootstrap.abortSignal;
+ const { DOMException } = window.__bootstrap.domException;
+ const {
+ ArrayPrototypePush,
+ ArrayPrototypeSplice,
+ ArrayPrototypeFilter,
+ ArrayPrototypeIncludes,
+ Promise,
+ PromisePrototypeThen,
+ PromisePrototypeCatch,
+ StringPrototypeToLowerCase,
+ TypedArrayPrototypeSubarray,
+ TypeError,
+ Uint8Array,
+ } = window.__bootstrap.primordials;
+
+ const REQUEST_BODY_HEADER_NAMES = [
+ "content-encoding",
+ "content-language",
+ "content-location",
+ "content-type",
+ ];
+
+ /**
+ * @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(args, body) {
+ return core.opSync("op_fetch", args, 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} rid
+ * @param {Uint8Array} body
+ * @returns {Promise<void>}
+ */
+ function opFetchRequestWrite(rid, body) {
+ return core.opAsync("op_fetch_request_write", rid, body);
+ }
+
+ /**
+ * @param {number} rid
+ * @param {Uint8Array} body
+ * @returns {Promise<number>}
+ */
+ function opFetchResponseRead(rid, body) {
+ return core.opAsync("op_fetch_response_read", rid, body);
+ }
+
+ // A finalization registry to clean up underlying fetch resources that are GC'ed.
+ const RESOURCE_REGISTRY = new FinalizationRegistry((rid) => {
+ try {
+ core.close(rid);
+ } catch {
+ // might have already been closed
+ }
+ });
+
+ /**
+ * @param {number} responseBodyRid
+ * @param {AbortSignal} [terminator]
+ * @returns {ReadableStream<Uint8Array>}
+ */
+ function createResponseBodyStream(responseBodyRid, terminator) {
+ function onAbort() {
+ if (readable) {
+ errorReadableStream(
+ readable,
+ new DOMException("Ongoing fetch was aborted.", "AbortError"),
+ );
+ }
+ try {
+ core.close(responseBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ // TODO(lucacasonato): clean up registration
+ terminator[abortSignal.add](onAbort);
+ const readable = new ReadableStream({
+ type: "bytes",
+ async pull(controller) {
+ try {
+ // This is the largest possible size for a single packet on a TLS
+ // stream.
+ const chunk = new Uint8Array(16 * 1024 + 256);
+ const read = await opFetchResponseRead(
+ responseBodyRid,
+ chunk,
+ );
+ if (read > 0) {
+ // We read some data. Enqueue it onto the stream.
+ controller.enqueue(TypedArrayPrototypeSubarray(chunk, 0, read));
+ } else {
+ RESOURCE_REGISTRY.unregister(readable);
+ // We have reached the end of the body, so we close the stream.
+ controller.close();
+ try {
+ core.close(responseBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ } catch (err) {
+ RESOURCE_REGISTRY.unregister(readable);
+ if (terminator.aborted) {
+ controller.error(
+ new DOMException("Ongoing fetch was aborted.", "AbortError"),
+ );
+ } else {
+ // There was an error while reading a chunk of the body, so we
+ // error.
+ controller.error(err);
+ }
+ try {
+ core.close(responseBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ },
+ cancel() {
+ if (!terminator.aborted) {
+ terminator[abortSignal.signalAbort]();
+ }
+ },
+ });
+ RESOURCE_REGISTRY.register(readable, responseBodyRid, readable);
+ return readable;
+ }
+
+ /**
+ * @param {InnerRequest} req
+ * @param {boolean} recursive
+ * @param {AbortSignal} terminator
+ * @returns {Promise<InnerResponse>}
+ */
+ async function mainFetch(req, recursive, terminator) {
+ /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
+ let reqBody = null;
+
+ if (req.body !== null) {
+ if (req.body.streamOrStatic instanceof ReadableStream) {
+ if (req.body.length === null || req.body.source instanceof Blob) {
+ reqBody = req.body.stream;
+ } else {
+ const reader = req.body.stream.getReader();
+ 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");
+ }
+ }
+ } else {
+ req.body.streamOrStatic.consumed = true;
+ reqBody = req.body.streamOrStatic.body;
+ }
+ }
+
+ const { requestRid, requestBodyRid, cancelHandleRid } = opFetch({
+ method: req.method,
+ url: req.currentUrl(),
+ headers: req.headerList,
+ clientRid: req.clientRid,
+ hasBody: reqBody !== null,
+ bodyLength: req.body?.length,
+ }, reqBody instanceof Uint8Array ? reqBody : null);
+
+ function onAbort() {
+ try {
+ core.close(cancelHandleRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ try {
+ core.close(requestBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ terminator[abortSignal.add](onAbort);
+
+ if (requestBodyRid !== null) {
+ if (reqBody === null || !(reqBody instanceof ReadableStream)) {
+ throw new TypeError("Unreachable");
+ }
+ const reader = reqBody.getReader();
+ (async () => {
+ while (true) {
+ const { value, done } = await PromisePrototypeCatch(
+ reader.read(),
+ (err) => {
+ if (terminator.aborted) return { done: true, value: undefined };
+ throw err;
+ },
+ );
+ if (done) break;
+ if (!(value instanceof Uint8Array)) {
+ await reader.cancel("value not a Uint8Array");
+ break;
+ }
+ try {
+ await PromisePrototypeCatch(
+ opFetchRequestWrite(requestBodyRid, value),
+ (err) => {
+ if (terminator.aborted) return;
+ throw err;
+ },
+ );
+ if (terminator.aborted) break;
+ } catch (err) {
+ await reader.cancel(err);
+ break;
+ }
+ }
+ try {
+ core.close(requestBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ })();
+ }
+
+ let resp;
+ try {
+ resp = await PromisePrototypeCatch(opFetchSend(requestRid), (err) => {
+ if (terminator.aborted) return;
+ throw err;
+ });
+ } finally {
+ try {
+ core.close(cancelHandleRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ if (terminator.aborted) return abortedNetworkError();
+
+ /** @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.urlList,
+ };
+ 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)) {
+ 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),
+ );
+ }
+ }
+
+ if (recursive) return response;
+
+ if (response.urlList.length === 0) {
+ response.urlList = [...req.urlList];
+ }
+
+ return response;
+ }
+
+ /**
+ * @param {InnerRequest} request
+ * @param {InnerResponse} response
+ * @returns {Promise<InnerResponse>}
+ */
+ function httpRedirectFetch(request, response, terminator) {
+ const locationHeaders = ArrayPrototypeFilter(
+ response.headerList,
+ (entry) => entry[0] === "location",
+ );
+ if (locationHeaders.length === 0) {
+ return response;
+ }
+ 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 (
+ ((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,
+ 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);
+ }
+
+ /**
+ * @param {RequestInfo} input
+ * @param {RequestInit} init
+ */
+ function fetch(input, init = {}) {
+ // 1.
+ const p = new Promise((resolve, reject) => {
+ const prefix = "Failed to call 'fetch'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ input = webidl.converters["RequestInfo"](input, {
+ prefix,
+ context: "Argument 1",
+ });
+ init = webidl.converters["RequestInit"](init, {
+ prefix,
+ context: "Argument 2",
+ });
+
+ // 2.
+ const requestObject = new Request(input, init);
+ // 3.
+ const request = toInnerRequest(requestObject);
+ // 4.
+ if (requestObject.signal.aborted) {
+ reject(abortFetch(request, null));
+ return;
+ }
+
+ // 7.
+ let responseObject = null;
+ // 9.
+ let locallyAborted = false;
+ // 10.
+ function onabort() {
+ locallyAborted = true;
+ reject(abortFetch(request, responseObject));
+ }
+ requestObject.signal[abortSignal.add](onabort);
+
+ if (!requestObject.headers.has("accept")) {
+ ArrayPrototypePush(request.headerList, ["accept", "*/*"]);
+ }
+
+ // 12.
+ PromisePrototypeCatch(
+ PromisePrototypeThen(
+ mainFetch(request, false, requestObject.signal),
+ (response) => {
+ // 12.1.
+ if (locallyAborted) return;
+ // 12.2.
+ if (response.aborted) {
+ reject(request, responseObject);
+ 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);
+ requestObject.signal[abortSignal.remove](onabort);
+ },
+ ),
+ (err) => {
+ reject(err);
+ requestObject.signal[abortSignal.remove](onabort);
+ },
+ );
+ });
+ return p;
+ }
+
+ function abortFetch(request, responseObject) {
+ const error = new DOMException("Ongoing fetch was aborted.", "AbortError");
+ if (request.body !== null) request.body.cancel(error);
+ if (responseObject !== null) {
+ const response = toInnerResponse(responseObject);
+ if (response.body !== null) response.body.error(error);
+ }
+ return error;
+ }
+
+ /**
+ * Handle the Promise<Response> argument to the WebAssembly streaming
+ * APIs. This function should be registered through
+ * `Deno.core.setWasmStreamingCallback`.
+ *
+ * @param {any} source The source parameter that the WebAssembly
+ * streaming API was called with.
+ * @param {number} rid An rid that can be used with
+ * `Deno.core.wasmStreamingFeed`.
+ */
+ function handleWasmStreaming(source, rid) {
+ // This implements part of
+ // https://webassembly.github.io/spec/web-api/#compile-a-potential-webassembly-response
+ (async () => {
+ try {
+ const res = webidl.converters["Response"](await 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.
+ if (
+ StringPrototypeToLowerCase(res.headers.get("Content-Type")) !==
+ "application/wasm"
+ ) {
+ throw new TypeError("Invalid WebAssembly content type.");
+ }
+
+ // 2.5.
+ if (!res.ok) {
+ throw new TypeError(`HTTP status code ${res.status}`);
+ }
+
+ // 2.6.
+ // Rather than consuming the body as an ArrayBuffer, this passes each
+ // chunk to the feed as soon as it's available.
+ if (res.body !== null) {
+ const reader = res.body.getReader();
+ while (true) {
+ const { value: chunk, done } = await reader.read();
+ if (done) break;
+ Deno.core.wasmStreamingFeed(rid, "bytes", chunk);
+ }
+ }
+
+ // 2.7.
+ Deno.core.wasmStreamingFeed(rid, "finish");
+ } catch (err) {
+ // 2.8 and 3
+ Deno.core.wasmStreamingFeed(rid, "abort", err);
+ }
+ })();
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.fetch = fetch;
+ window.__bootstrap.fetch.handleWasmStreaming = handleWasmStreaming;
+})(this);
diff --git a/ext/fetch/Cargo.toml b/ext/fetch/Cargo.toml
new file mode 100644
index 000000000..80d0cb2e1
--- /dev/null
+++ b/ext/fetch/Cargo.toml
@@ -0,0 +1,28 @@
+# Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+[package]
+name = "deno_fetch"
+version = "0.37.0"
+authors = ["the Deno authors"]
+edition = "2018"
+license = "MIT"
+readme = "README.md"
+repository = "https://github.com/denoland/deno"
+description = "Fetch API implementation for Deno"
+
+[lib]
+path = "lib.rs"
+
+[dependencies]
+bytes = "1.0.1"
+data-url = "0.1.0"
+deno_core = { version = "0.96.0", path = "../../core" }
+deno_tls = { version = "0.1.0", path = "../tls" }
+deno_web = { version = "0.45.0", path = "../web" }
+http = "0.2.4"
+lazy_static = "1.4.0"
+reqwest = { version = "0.11.4", default-features = false, features = ["rustls-tls", "stream", "gzip", "brotli"] }
+serde = { version = "1.0.126", features = ["derive"] }
+tokio = { version = "1.8.1", features = ["full"] }
+tokio-stream = "0.1.7"
+tokio-util = "0.6.7"
diff --git a/ext/fetch/README.md b/ext/fetch/README.md
new file mode 100644
index 000000000..2c946197e
--- /dev/null
+++ b/ext/fetch/README.md
@@ -0,0 +1,5 @@
+# deno_fetch
+
+This crate implements the Fetch API.
+
+Spec: https://fetch.spec.whatwg.org/
diff --git a/ext/fetch/internal.d.ts b/ext/fetch/internal.d.ts
new file mode 100644
index 000000000..a84e0bcce
--- /dev/null
+++ b/ext/fetch/internal.d.ts
@@ -0,0 +1,108 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file no-explicit-any
+
+/// <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 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 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 namespace fetch {
+ function toInnerRequest(request: Request): InnerRequest;
+ function fromInnerRequest(
+ inner: InnerRequest,
+ signal: AbortSignal | null,
+ guard:
+ | "request"
+ | "immutable"
+ | "request-no-cors"
+ | "response"
+ | "none",
+ ): 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;
+ }
+ }
+}
diff --git a/ext/fetch/lib.deno_fetch.d.ts b/ext/fetch/lib.deno_fetch.d.ts
new file mode 100644
index 000000000..7fe7d9453
--- /dev/null
+++ b/ext/fetch/lib.deno_fetch.d.ts
@@ -0,0 +1,437 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// deno-lint-ignore-file no-explicit-any
+
+/// <reference no-default-lib="true" />
+/// <reference lib="esnext" />
+
+interface DomIterable<K, V> {
+ keys(): IterableIterator<K>;
+ values(): IterableIterator<V>;
+ entries(): IterableIterator<[K, V]>;
+ [Symbol.iterator](): IterableIterator<[K, V]>;
+ forEach(
+ callback: (value: V, key: K, parent: this) => void,
+ thisArg?: any,
+ ): void;
+}
+
+type FormDataEntryValue = File | string;
+
+/** Provides a way to easily construct a set of key/value pairs representing
+ * form fields and their values, which can then be easily sent using the
+ * XMLHttpRequest.send() method. It uses the same format a form would use if the
+ * encoding type were set to "multipart/form-data". */
+declare class FormData implements DomIterable<string, FormDataEntryValue> {
+ // TODO(ry) FormData constructor is non-standard.
+ // new(form?: HTMLFormElement): FormData;
+ constructor();
+
+ append(name: string, value: string | Blob, fileName?: string): void;
+ delete(name: string): void;
+ get(name: string): FormDataEntryValue | null;
+ getAll(name: string): FormDataEntryValue[];
+ has(name: string): boolean;
+ set(name: string, value: string | Blob, fileName?: string): void;
+ keys(): IterableIterator<string>;
+ values(): IterableIterator<string>;
+ entries(): IterableIterator<[string, FormDataEntryValue]>;
+ [Symbol.iterator](): IterableIterator<[string, FormDataEntryValue]>;
+ forEach(
+ callback: (value: FormDataEntryValue, key: string, parent: this) => void,
+ thisArg?: any,
+ ): void;
+}
+
+interface Body {
+ /** A simple getter used to expose a `ReadableStream` of the body contents. */
+ readonly body: ReadableStream<Uint8Array> | null;
+ /** Stores a `Boolean` that declares whether the body has been used in a
+ * response yet.
+ */
+ readonly bodyUsed: boolean;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with an `ArrayBuffer`.
+ */
+ arrayBuffer(): Promise<ArrayBuffer>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `Blob`.
+ */
+ blob(): Promise<Blob>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `FormData` object.
+ */
+ formData(): Promise<FormData>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with the result of parsing the body text as JSON.
+ */
+ json(): Promise<any>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `USVString` (text).
+ */
+ text(): Promise<string>;
+}
+
+type HeadersInit = Headers | string[][] | Record<string, string>;
+
+/** This Fetch API interface allows you to perform various actions on HTTP
+ * request and response headers. These actions include retrieving, setting,
+ * adding to, and removing. A Headers object has an associated header list,
+ * which is initially empty and consists of zero or more name and value pairs.
+ * You can add to this using methods like append() (see Examples). In all
+ * methods of this interface, header names are matched by case-insensitive byte
+ * sequence. */
+interface Headers {
+ append(name: string, value: string): void;
+ delete(name: string): void;
+ get(name: string): string | null;
+ has(name: string): boolean;
+ set(name: string, value: string): void;
+ forEach(
+ callbackfn: (value: string, key: string, parent: Headers) => void,
+ thisArg?: any,
+ ): void;
+}
+
+declare class Headers implements DomIterable<string, string> {
+ constructor(init?: HeadersInit);
+
+ /** Appends a new value onto an existing header inside a `Headers` object, or
+ * adds the header if it does not already exist.
+ */
+ append(name: string, value: string): void;
+ /** Deletes a header from a `Headers` object. */
+ delete(name: string): void;
+ /** Returns an iterator allowing to go through all key/value pairs
+ * contained in this Headers object. The both the key and value of each pairs
+ * are ByteString objects.
+ */
+ entries(): IterableIterator<[string, string]>;
+ /** Returns a `ByteString` sequence of all the values of a header within a
+ * `Headers` object with a given name.
+ */
+ get(name: string): string | null;
+ /** Returns a boolean stating whether a `Headers` object contains a certain
+ * header.
+ */
+ has(name: string): boolean;
+ /** Returns an iterator allowing to go through all keys contained in
+ * this Headers object. The keys are ByteString objects.
+ */
+ keys(): IterableIterator<string>;
+ /** Sets a new value for an existing header inside a Headers object, or adds
+ * the header if it does not already exist.
+ */
+ set(name: string, value: string): void;
+ /** Returns an iterator allowing to go through all values contained in
+ * this Headers object. The values are ByteString objects.
+ */
+ values(): IterableIterator<string>;
+ forEach(
+ callbackfn: (value: string, key: string, parent: this) => void,
+ thisArg?: any,
+ ): void;
+ /** The Symbol.iterator well-known symbol specifies the default
+ * iterator for this Headers object
+ */
+ [Symbol.iterator](): IterableIterator<[string, string]>;
+}
+
+type RequestInfo = Request | string;
+type RequestCache =
+ | "default"
+ | "force-cache"
+ | "no-cache"
+ | "no-store"
+ | "only-if-cached"
+ | "reload";
+type RequestCredentials = "include" | "omit" | "same-origin";
+type RequestMode = "cors" | "navigate" | "no-cors" | "same-origin";
+type RequestRedirect = "error" | "follow" | "manual";
+type ReferrerPolicy =
+ | ""
+ | "no-referrer"
+ | "no-referrer-when-downgrade"
+ | "origin"
+ | "origin-when-cross-origin"
+ | "same-origin"
+ | "strict-origin"
+ | "strict-origin-when-cross-origin"
+ | "unsafe-url";
+type BodyInit =
+ | Blob
+ | BufferSource
+ | FormData
+ | URLSearchParams
+ | ReadableStream<Uint8Array>
+ | string;
+type RequestDestination =
+ | ""
+ | "audio"
+ | "audioworklet"
+ | "document"
+ | "embed"
+ | "font"
+ | "image"
+ | "manifest"
+ | "object"
+ | "paintworklet"
+ | "report"
+ | "script"
+ | "sharedworker"
+ | "style"
+ | "track"
+ | "video"
+ | "worker"
+ | "xslt";
+
+interface RequestInit {
+ /**
+ * A BodyInit object or null to set request's body.
+ */
+ body?: BodyInit | null;
+ /**
+ * A string indicating how the request will interact with the browser's cache
+ * to set request's cache.
+ */
+ cache?: RequestCache;
+ /**
+ * A string indicating whether credentials will be sent with the request
+ * always, never, or only when sent to a same-origin URL. Sets request's
+ * credentials.
+ */
+ credentials?: RequestCredentials;
+ /**
+ * A Headers object, an object literal, or an array of two-item arrays to set
+ * request's headers.
+ */
+ headers?: HeadersInit;
+ /**
+ * A cryptographic hash of the resource to be fetched by request. Sets
+ * request's integrity.
+ */
+ integrity?: string;
+ /**
+ * A boolean to set request's keepalive.
+ */
+ keepalive?: boolean;
+ /**
+ * A string to set request's method.
+ */
+ method?: string;
+ /**
+ * A string to indicate whether the request will use CORS, or will be
+ * restricted to same-origin URLs. Sets request's mode.
+ */
+ mode?: RequestMode;
+ /**
+ * A string indicating whether request follows redirects, results in an error
+ * upon encountering a redirect, or returns the redirect (in an opaque
+ * fashion). Sets request's redirect.
+ */
+ redirect?: RequestRedirect;
+ /**
+ * A string whose value is a same-origin URL, "about:client", or the empty
+ * string, to set request's referrer.
+ */
+ referrer?: string;
+ /**
+ * A referrer policy to set request's referrerPolicy.
+ */
+ referrerPolicy?: ReferrerPolicy;
+ /**
+ * An AbortSignal to set request's signal.
+ */
+ signal?: AbortSignal | null;
+ /**
+ * Can only be null. Used to disassociate request from any Window.
+ */
+ window?: any;
+}
+
+/** This Fetch API interface represents a resource request. */
+declare class Request implements Body {
+ constructor(input: RequestInfo, init?: RequestInit);
+
+ /**
+ * Returns the cache mode associated with request, which is a string
+ * indicating how the request will interact with the browser's cache when
+ * fetching.
+ */
+ readonly cache: RequestCache;
+ /**
+ * Returns the credentials mode associated with request, which is a string
+ * indicating whether credentials will be sent with the request always, never,
+ * or only when sent to a same-origin URL.
+ */
+ readonly credentials: RequestCredentials;
+ /**
+ * Returns the kind of resource requested by request, e.g., "document" or "script".
+ */
+ readonly destination: RequestDestination;
+ /**
+ * Returns a Headers object consisting of the headers associated with request.
+ * Note that headers added in the network layer by the user agent will not be
+ * accounted for in this object, e.g., the "Host" header.
+ */
+ readonly headers: Headers;
+ /**
+ * Returns request's subresource integrity metadata, which is a cryptographic
+ * hash of the resource being fetched. Its value consists of multiple hashes
+ * separated by whitespace. [SRI]
+ */
+ readonly integrity: string;
+ /**
+ * Returns a boolean indicating whether or not request is for a history
+ * navigation (a.k.a. back-forward navigation).
+ */
+ readonly isHistoryNavigation: boolean;
+ /**
+ * Returns a boolean indicating whether or not request is for a reload
+ * navigation.
+ */
+ readonly isReloadNavigation: boolean;
+ /**
+ * Returns a boolean indicating whether or not request can outlive the global
+ * in which it was created.
+ */
+ readonly keepalive: boolean;
+ /**
+ * Returns request's HTTP method, which is "GET" by default.
+ */
+ readonly method: string;
+ /**
+ * Returns the mode associated with request, which is a string indicating
+ * whether the request will use CORS, or will be restricted to same-origin
+ * URLs.
+ */
+ readonly mode: RequestMode;
+ /**
+ * Returns the redirect mode associated with request, which is a string
+ * indicating how redirects for the request will be handled during fetching. A
+ * request will follow redirects by default.
+ */
+ readonly redirect: RequestRedirect;
+ /**
+ * Returns the referrer of request. Its value can be a same-origin URL if
+ * explicitly set in init, the empty string to indicate no referrer, and
+ * "about:client" when defaulting to the global's default. This is used during
+ * fetching to determine the value of the `Referer` header of the request
+ * being made.
+ */
+ readonly referrer: string;
+ /**
+ * Returns the referrer policy associated with request. This is used during
+ * fetching to compute the value of the request's referrer.
+ */
+ readonly referrerPolicy: ReferrerPolicy;
+ /**
+ * Returns the signal associated with request, which is an AbortSignal object
+ * indicating whether or not request has been aborted, and its abort event
+ * handler.
+ */
+ readonly signal: AbortSignal;
+ /**
+ * Returns the URL of request as a string.
+ */
+ readonly url: string;
+ clone(): Request;
+
+ /** A simple getter used to expose a `ReadableStream` of the body contents. */
+ readonly body: ReadableStream<Uint8Array> | null;
+ /** Stores a `Boolean` that declares whether the body has been used in a
+ * response yet.
+ */
+ readonly bodyUsed: boolean;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with an `ArrayBuffer`.
+ */
+ arrayBuffer(): Promise<ArrayBuffer>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `Blob`.
+ */
+ blob(): Promise<Blob>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `FormData` object.
+ */
+ formData(): Promise<FormData>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with the result of parsing the body text as JSON.
+ */
+ json(): Promise<any>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `USVString` (text).
+ */
+ text(): Promise<string>;
+}
+
+interface ResponseInit {
+ headers?: HeadersInit;
+ status?: number;
+ statusText?: string;
+}
+
+type ResponseType =
+ | "basic"
+ | "cors"
+ | "default"
+ | "error"
+ | "opaque"
+ | "opaqueredirect";
+
+/** This Fetch API interface represents the response to a request. */
+declare class Response implements Body {
+ constructor(body?: BodyInit | null, init?: ResponseInit);
+ static error(): Response;
+ static redirect(url: string, status?: number): Response;
+
+ readonly headers: Headers;
+ readonly ok: boolean;
+ readonly redirected: boolean;
+ readonly status: number;
+ readonly statusText: string;
+ readonly trailer: Promise<Headers>;
+ readonly type: ResponseType;
+ readonly url: string;
+ clone(): Response;
+
+ /** A simple getter used to expose a `ReadableStream` of the body contents. */
+ readonly body: ReadableStream<Uint8Array> | null;
+ /** Stores a `Boolean` that declares whether the body has been used in a
+ * response yet.
+ */
+ readonly bodyUsed: boolean;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with an `ArrayBuffer`.
+ */
+ arrayBuffer(): Promise<ArrayBuffer>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `Blob`.
+ */
+ blob(): Promise<Blob>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `FormData` object.
+ */
+ formData(): Promise<FormData>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with the result of parsing the body text as JSON.
+ */
+ json(): Promise<any>;
+ /** Takes a `Response` stream and reads it to completion. It returns a promise
+ * that resolves with a `USVString` (text).
+ */
+ text(): Promise<string>;
+}
+
+/** Fetch a resource from the network. It returns a Promise that resolves to the
+ * Response to that request, whether it is successful or not.
+ *
+ * const response = await fetch("http://my.json.host/data.json");
+ * console.log(response.status); // e.g. 200
+ * console.log(response.statusText); // e.g. "OK"
+ * const jsonData = await response.json();
+ */
+declare function fetch(
+ input: Request | URL | string,
+ init?: RequestInit,
+): Promise<Response>;
diff --git a/ext/fetch/lib.rs b/ext/fetch/lib.rs
new file mode 100644
index 000000000..e89df470a
--- /dev/null
+++ b/ext/fetch/lib.rs
@@ -0,0 +1,567 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+use data_url::DataUrl;
+use deno_core::error::bad_resource_id;
+use deno_core::error::null_opbuf;
+use deno_core::error::type_error;
+use deno_core::error::AnyError;
+use deno_core::futures::Future;
+use deno_core::futures::Stream;
+use deno_core::futures::StreamExt;
+use deno_core::include_js_files;
+use deno_core::op_async;
+use deno_core::op_sync;
+use deno_core::url::Url;
+use deno_core::AsyncRefCell;
+use deno_core::ByteString;
+use deno_core::CancelFuture;
+use deno_core::CancelHandle;
+use deno_core::CancelTryFuture;
+use deno_core::Canceled;
+use deno_core::Extension;
+use deno_core::OpState;
+use deno_core::RcRef;
+use deno_core::Resource;
+use deno_core::ResourceId;
+use deno_core::ZeroCopyBuf;
+use deno_tls::create_http_client;
+use deno_tls::rustls::RootCertStore;
+use deno_tls::Proxy;
+use deno_web::BlobStore;
+use http::header::CONTENT_LENGTH;
+use reqwest::header::HeaderName;
+use reqwest::header::HeaderValue;
+use reqwest::header::HOST;
+use reqwest::Body;
+use reqwest::Client;
+use reqwest::Method;
+use reqwest::RequestBuilder;
+use reqwest::Response;
+use serde::Deserialize;
+use serde::Serialize;
+use std::borrow::Cow;
+use std::cell::RefCell;
+use std::convert::From;
+use std::fs::File;
+use std::io::Read;
+use std::path::Path;
+use std::path::PathBuf;
+use std::pin::Pin;
+use std::rc::Rc;
+use tokio::io::AsyncReadExt;
+use tokio::sync::mpsc;
+use tokio_stream::wrappers::ReceiverStream;
+use tokio_util::io::StreamReader;
+
+pub use reqwest; // Re-export reqwest
+
+pub fn init<P: FetchPermissions + 'static>(
+ user_agent: String,
+ root_cert_store: Option<RootCertStore>,
+ proxy: Option<Proxy>,
+ request_builder_hook: Option<fn(RequestBuilder) -> RequestBuilder>,
+ unsafely_ignore_certificate_errors: Option<Vec<String>>,
+) -> Extension {
+ Extension::builder()
+ .js(include_js_files!(
+ prefix "deno:ext/fetch",
+ "01_fetch_util.js",
+ "20_headers.js",
+ "21_formdata.js",
+ "22_body.js",
+ "22_http_client.js",
+ "23_request.js",
+ "23_response.js",
+ "26_fetch.js",
+ ))
+ .ops(vec![
+ ("op_fetch", op_sync(op_fetch::<P>)),
+ ("op_fetch_send", op_async(op_fetch_send)),
+ ("op_fetch_request_write", op_async(op_fetch_request_write)),
+ ("op_fetch_response_read", op_async(op_fetch_response_read)),
+ ("op_create_http_client", op_sync(op_create_http_client::<P>)),
+ ])
+ .state(move |state| {
+ state.put::<reqwest::Client>({
+ create_http_client(
+ user_agent.clone(),
+ root_cert_store.clone(),
+ None,
+ proxy.clone(),
+ unsafely_ignore_certificate_errors.clone(),
+ )
+ .unwrap()
+ });
+ state.put::<HttpClientDefaults>(HttpClientDefaults {
+ user_agent: user_agent.clone(),
+ root_cert_store: root_cert_store.clone(),
+ proxy: proxy.clone(),
+ request_builder_hook,
+ unsafely_ignore_certificate_errors: unsafely_ignore_certificate_errors
+ .clone(),
+ });
+ Ok(())
+ })
+ .build()
+}
+
+pub struct HttpClientDefaults {
+ pub user_agent: String,
+ pub root_cert_store: Option<RootCertStore>,
+ pub proxy: Option<Proxy>,
+ pub request_builder_hook: Option<fn(RequestBuilder) -> RequestBuilder>,
+ pub unsafely_ignore_certificate_errors: Option<Vec<String>>,
+}
+
+pub trait FetchPermissions {
+ fn check_net_url(&mut self, _url: &Url) -> Result<(), AnyError>;
+ fn check_read(&mut self, _p: &Path) -> Result<(), AnyError>;
+}
+
+/// For use with `op_fetch` when the user does not want permissions.
+pub struct NoFetchPermissions;
+
+impl FetchPermissions for NoFetchPermissions {
+ fn check_net_url(&mut self, _url: &Url) -> Result<(), AnyError> {
+ Ok(())
+ }
+
+ fn check_read(&mut self, _p: &Path) -> Result<(), AnyError> {
+ Ok(())
+ }
+}
+
+pub fn get_declaration() -> PathBuf {
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("lib.deno_fetch.d.ts")
+}
+
+#[derive(Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FetchArgs {
+ method: ByteString,
+ url: String,
+ headers: Vec<(ByteString, ByteString)>,
+ client_rid: Option<u32>,
+ has_body: bool,
+ body_length: Option<u64>,
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FetchReturn {
+ request_rid: ResourceId,
+ request_body_rid: Option<ResourceId>,
+ cancel_handle_rid: Option<ResourceId>,
+}
+
+pub fn op_fetch<FP>(
+ state: &mut OpState,
+ args: FetchArgs,
+ data: Option<ZeroCopyBuf>,
+) -> Result<FetchReturn, AnyError>
+where
+ FP: FetchPermissions + 'static,
+{
+ let client = if let Some(rid) = args.client_rid {
+ let r = state
+ .resource_table
+ .get::<HttpClientResource>(rid)
+ .ok_or_else(bad_resource_id)?;
+ r.client.clone()
+ } else {
+ let client = state.borrow::<reqwest::Client>();
+ client.clone()
+ };
+
+ let method = Method::from_bytes(&args.method)?;
+ let url = Url::parse(&args.url)?;
+
+ // Check scheme before asking for net permission
+ let scheme = url.scheme();
+ let (request_rid, request_body_rid, cancel_handle_rid) = match scheme {
+ "http" | "https" => {
+ let permissions = state.borrow_mut::<FP>();
+ permissions.check_net_url(&url)?;
+
+ let mut request = client.request(method, url);
+
+ let request_body_rid = if args.has_body {
+ match data {
+ None => {
+ // If no body is passed, we return a writer for streaming the body.
+ let (tx, rx) = mpsc::channel::<std::io::Result<Vec<u8>>>(1);
+
+ // If the size of the body is known, we include a content-length
+ // header explicitly.
+ if let Some(body_size) = args.body_length {
+ request =
+ request.header(CONTENT_LENGTH, HeaderValue::from(body_size))
+ }
+
+ request = request.body(Body::wrap_stream(ReceiverStream::new(rx)));
+
+ let request_body_rid =
+ state.resource_table.add(FetchRequestBodyResource {
+ body: AsyncRefCell::new(tx),
+ cancel: CancelHandle::default(),
+ });
+
+ Some(request_body_rid)
+ }
+ Some(data) => {
+ // If a body is passed, we use it, and don't return a body for streaming.
+ request = request.body(Vec::from(&*data));
+ None
+ }
+ }
+ } else {
+ None
+ };
+
+ for (key, value) in args.headers {
+ let name = HeaderName::from_bytes(&key).unwrap();
+ let v = HeaderValue::from_bytes(&value).unwrap();
+ if name != HOST {
+ request = request.header(name, v);
+ }
+ }
+
+ let defaults = state.borrow::<HttpClientDefaults>();
+ if let Some(request_builder_hook) = defaults.request_builder_hook {
+ request = request_builder_hook(request);
+ }
+
+ let cancel_handle = CancelHandle::new_rc();
+ let cancel_handle_ = cancel_handle.clone();
+
+ let fut = async move {
+ request
+ .send()
+ .or_cancel(cancel_handle_)
+ .await
+ .map(|res| res.map_err(|err| type_error(err.to_string())))
+ };
+
+ let request_rid = state
+ .resource_table
+ .add(FetchRequestResource(Box::pin(fut)));
+
+ let cancel_handle_rid =
+ state.resource_table.add(FetchCancelHandle(cancel_handle));
+
+ (request_rid, request_body_rid, Some(cancel_handle_rid))
+ }
+ "data" => {
+ let data_url = DataUrl::process(url.as_str())
+ .map_err(|e| type_error(format!("{:?}", e)))?;
+
+ let (body, _) = data_url
+ .decode_to_vec()
+ .map_err(|e| type_error(format!("{:?}", e)))?;
+
+ let response = http::Response::builder()
+ .status(http::StatusCode::OK)
+ .header(http::header::CONTENT_TYPE, data_url.mime_type().to_string())
+ .body(reqwest::Body::from(body))?;
+
+ let fut = async move { Ok(Ok(Response::from(response))) };
+
+ let request_rid = state
+ .resource_table
+ .add(FetchRequestResource(Box::pin(fut)));
+
+ (request_rid, None, None)
+ }
+ "blob" => {
+ let blob_store = state.try_borrow::<BlobStore>().ok_or_else(|| {
+ type_error("Blob URLs are not supported in this context.")
+ })?;
+
+ let blob = blob_store
+ .get_object_url(url)?
+ .ok_or_else(|| type_error("Blob for the given URL not found."))?;
+
+ if method != "GET" {
+ return Err(type_error("Blob URL fetch only supports GET method."));
+ }
+
+ let cancel_handle = CancelHandle::new_rc();
+ let cancel_handle_ = cancel_handle.clone();
+
+ let fut = async move {
+ // TODO(lucacsonato): this should be a stream!
+ let chunk = match blob.read_all().or_cancel(cancel_handle_).await? {
+ Ok(chunk) => chunk,
+ Err(err) => return Ok(Err(err)),
+ };
+
+ let res = http::Response::builder()
+ .status(http::StatusCode::OK)
+ .header(http::header::CONTENT_LENGTH, chunk.len())
+ .header(http::header::CONTENT_TYPE, blob.media_type.clone())
+ .body(reqwest::Body::from(chunk))
+ .map_err(|err| type_error(err.to_string()));
+
+ match res {
+ Ok(response) => Ok(Ok(Response::from(response))),
+ Err(err) => Ok(Err(err)),
+ }
+ };
+
+ let request_rid = state
+ .resource_table
+ .add(FetchRequestResource(Box::pin(fut)));
+
+ let cancel_handle_rid =
+ state.resource_table.add(FetchCancelHandle(cancel_handle));
+
+ (request_rid, None, Some(cancel_handle_rid))
+ }
+ _ => return Err(type_error(format!("scheme '{}' not supported", scheme))),
+ };
+
+ Ok(FetchReturn {
+ request_rid,
+ request_body_rid,
+ cancel_handle_rid,
+ })
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct FetchResponse {
+ status: u16,
+ status_text: String,
+ headers: Vec<(ByteString, ByteString)>,
+ url: String,
+ response_rid: ResourceId,
+}
+
+pub async fn op_fetch_send(
+ state: Rc<RefCell<OpState>>,
+ rid: ResourceId,
+ _: (),
+) -> Result<FetchResponse, AnyError> {
+ let request = state
+ .borrow_mut()
+ .resource_table
+ .take::<FetchRequestResource>(rid)
+ .ok_or_else(bad_resource_id)?;
+
+ let request = Rc::try_unwrap(request)
+ .ok()
+ .expect("multiple op_fetch_send ongoing");
+
+ let res = match request.0.await {
+ Ok(Ok(res)) => res,
+ Ok(Err(err)) => return Err(type_error(err.to_string())),
+ Err(_) => return Err(type_error("request was cancelled")),
+ };
+
+ //debug!("Fetch response {}", url);
+ let status = res.status();
+ let url = res.url().to_string();
+ let mut res_headers = Vec::new();
+ for (key, val) in res.headers().iter() {
+ let key_bytes: &[u8] = key.as_ref();
+ res_headers.push((
+ ByteString(key_bytes.to_owned()),
+ ByteString(val.as_bytes().to_owned()),
+ ));
+ }
+
+ let stream: BytesStream = Box::pin(res.bytes_stream().map(|r| {
+ r.map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err))
+ }));
+ let stream_reader = StreamReader::new(stream);
+ let rid = state
+ .borrow_mut()
+ .resource_table
+ .add(FetchResponseBodyResource {
+ reader: AsyncRefCell::new(stream_reader),
+ cancel: CancelHandle::default(),
+ });
+
+ Ok(FetchResponse {
+ status: status.as_u16(),
+ status_text: status.canonical_reason().unwrap_or("").to_string(),
+ headers: res_headers,
+ url,
+ response_rid: rid,
+ })
+}
+
+pub async fn op_fetch_request_write(
+ state: Rc<RefCell<OpState>>,
+ rid: ResourceId,
+ data: Option<ZeroCopyBuf>,
+) -> Result<(), AnyError> {
+ let data = data.ok_or_else(null_opbuf)?;
+ let buf = Vec::from(&*data);
+
+ let resource = state
+ .borrow()
+ .resource_table
+ .get::<FetchRequestBodyResource>(rid)
+ .ok_or_else(bad_resource_id)?;
+ let body = RcRef::map(&resource, |r| &r.body).borrow_mut().await;
+ let cancel = RcRef::map(resource, |r| &r.cancel);
+ body.send(Ok(buf)).or_cancel(cancel).await?.map_err(|_| {
+ type_error("request body receiver not connected (request closed)")
+ })?;
+
+ Ok(())
+}
+
+pub async fn op_fetch_response_read(
+ state: Rc<RefCell<OpState>>,
+ rid: ResourceId,
+ data: Option<ZeroCopyBuf>,
+) -> Result<usize, AnyError> {
+ let data = data.ok_or_else(null_opbuf)?;
+
+ let resource = state
+ .borrow()
+ .resource_table
+ .get::<FetchResponseBodyResource>(rid)
+ .ok_or_else(bad_resource_id)?;
+ let mut reader = RcRef::map(&resource, |r| &r.reader).borrow_mut().await;
+ let cancel = RcRef::map(resource, |r| &r.cancel);
+ let mut buf = data.clone();
+ let read = reader.read(&mut buf).try_or_cancel(cancel).await?;
+ Ok(read)
+}
+
+type CancelableResponseResult = Result<Result<Response, AnyError>, Canceled>;
+
+struct FetchRequestResource(
+ Pin<Box<dyn Future<Output = CancelableResponseResult>>>,
+);
+
+impl Resource for FetchRequestResource {
+ fn name(&self) -> Cow<str> {
+ "fetchRequest".into()
+ }
+}
+
+struct FetchCancelHandle(Rc<CancelHandle>);
+
+impl Resource for FetchCancelHandle {
+ fn name(&self) -> Cow<str> {
+ "fetchCancelHandle".into()
+ }
+
+ fn close(self: Rc<Self>) {
+ self.0.cancel()
+ }
+}
+
+struct FetchRequestBodyResource {
+ body: AsyncRefCell<mpsc::Sender<std::io::Result<Vec<u8>>>>,
+ cancel: CancelHandle,
+}
+
+impl Resource for FetchRequestBodyResource {
+ fn name(&self) -> Cow<str> {
+ "fetchRequestBody".into()
+ }
+
+ fn close(self: Rc<Self>) {
+ self.cancel.cancel()
+ }
+}
+
+type BytesStream =
+ Pin<Box<dyn Stream<Item = Result<bytes::Bytes, std::io::Error>> + Unpin>>;
+
+struct FetchResponseBodyResource {
+ reader: AsyncRefCell<StreamReader<BytesStream, bytes::Bytes>>,
+ cancel: CancelHandle,
+}
+
+impl Resource for FetchResponseBodyResource {
+ fn name(&self) -> Cow<str> {
+ "fetchResponseBody".into()
+ }
+
+ fn close(self: Rc<Self>) {
+ self.cancel.cancel()
+ }
+}
+
+struct HttpClientResource {
+ client: Client,
+}
+
+impl Resource for HttpClientResource {
+ fn name(&self) -> Cow<str> {
+ "httpClient".into()
+ }
+}
+
+impl HttpClientResource {
+ fn new(client: Client) -> Self {
+ Self { client }
+ }
+}
+
+#[derive(Deserialize, Default, Debug)]
+#[serde(rename_all = "camelCase")]
+#[serde(default)]
+pub struct CreateHttpClientOptions {
+ ca_stores: Option<Vec<String>>,
+ ca_file: Option<String>,
+ ca_data: Option<ByteString>,
+ proxy: Option<Proxy>,
+}
+
+pub fn op_create_http_client<FP>(
+ state: &mut OpState,
+ args: CreateHttpClientOptions,
+ _: (),
+) -> Result<ResourceId, AnyError>
+where
+ FP: FetchPermissions + 'static,
+{
+ if let Some(ca_file) = args.ca_file.clone() {
+ let permissions = state.borrow_mut::<FP>();
+ permissions.check_read(&PathBuf::from(ca_file))?;
+ }
+
+ if let Some(proxy) = args.proxy.clone() {
+ let permissions = state.borrow_mut::<FP>();
+ let url = Url::parse(&proxy.url)?;
+ permissions.check_net_url(&url)?;
+ }
+
+ let defaults = state.borrow::<HttpClientDefaults>();
+ let cert_data =
+ get_cert_data(args.ca_file.as_deref(), args.ca_data.as_deref())?;
+
+ let client = create_http_client(
+ defaults.user_agent.clone(),
+ defaults.root_cert_store.clone(),
+ cert_data,
+ args.proxy,
+ defaults.unsafely_ignore_certificate_errors.clone(),
+ )
+ .unwrap();
+
+ let rid = state.resource_table.add(HttpClientResource::new(client));
+ Ok(rid)
+}
+
+fn get_cert_data(
+ ca_file: Option<&str>,
+ ca_data: Option<&[u8]>,
+) -> Result<Option<Vec<u8>>, AnyError> {
+ if let Some(ca_data) = ca_data {
+ Ok(Some(ca_data.to_vec()))
+ } else if let Some(ca_file) = ca_file {
+ let mut buf = Vec::new();
+ File::open(ca_file)?.read_to_end(&mut buf)?;
+ Ok(Some(buf))
+ } else {
+ Ok(None)
+ }
+}