summaryrefslogtreecommitdiff
path: root/ext/fetch/20_headers.js
diff options
context:
space:
mode:
Diffstat (limited to 'ext/fetch/20_headers.js')
-rw-r--r--ext/fetch/20_headers.js479
1 files changed, 479 insertions, 0 deletions
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);