1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-25 15:29:32 -05:00
denoland-deno/op_crates/fetch/20_headers.js
Luca Casonato 0552eaf569
chore: align Headers to spec (#10199)
This commit aligns `Headers` to spec. It also removes the now unused
03_dom_iterable.js file. We now pass all relevant `Headers` WPT. We do
not implement any sort of header filtering, as we are a server side
runtime.

This is likely not the most efficient implementation of `Headers` yet.
It is however spec compliant. Once all the APIs in the `HTTP` hot loop
are correct we can start optimizing them. It is likely that this commit
reduces bench throughput temporarily.
2021-04-19 01:00:13 +02:00

365 lines
10 KiB
JavaScript

// 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="../file/internal.d.ts" />
/// <reference path="../file/lib.deno_file.d.ts" />
/// <reference path="./internal.d.ts" />
/// <reference path="./11_streams_types.d.ts" />
/// <reference path="./lib.deno_fetch.d.ts" />
/// <reference lib="esnext" />
"use strict";
((window) => {
const webidl = window.__bootstrap.webidl;
const {
HTTP_WHITESPACE_PREFIX_RE,
HTTP_WHITESPACE_SUFFIX_RE,
HTTP_TOKEN_CODE_POINT_RE,
byteLowerCase,
} = window.__bootstrap.infra;
const _headerList = Symbol("header list");
const _iterableHeaders = Symbol("iterable headers");
const _guard = Symbol("guard");
/**
* @typedef Header
* @type {[string, string]}
*/
/**
* @typedef HeaderList
* @type {Header[]}
*/
/**
* @typedef {string} potentialValue
* @returns {string}
*/
function normalizeHeaderValue(potentialValue) {
potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_PREFIX_RE, "");
potentialValue = potentialValue.replaceAll(HTTP_WHITESPACE_SUFFIX_RE, "");
return potentialValue;
}
/**
* @param {Headers} headers
* @param {HeadersInit} object
*/
function fillHeaders(headers, object) {
if (Array.isArray(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 Object.keys(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 (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
throw new TypeError("Header name is not valid.");
}
if (
value.includes("\x00") || value.includes("\x0A") || value.includes("\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];
const lowercaseName = byteLowerCase(name);
for (let i = 0; i < list.length; i++) {
if (byteLowerCase(list[i][0]) === lowercaseName) {
name = list[i][0];
break;
}
}
list.push([name, value]);
}
/**
* @param {HeaderList} list
* @param {string} name
*/
function getHeader(list, name) {
const lowercaseName = byteLowerCase(name);
const entries = list.filter((entry) =>
byteLowerCase(entry[0]) === lowercaseName
).map((entry) => entry[1]);
if (entries.length === 0) {
return null;
} else {
return entries.join("\x2C\x20");
}
}
class Headers {
/** @type {HeaderList} */
[_headerList] = [];
/** @type {"immutable"| "request"| "request-no-cors"| "response" | "none"} */
[_guard];
get [_iterableHeaders]() {
const list = this[_headerList];
const headers = [];
const headerNamesSet = new Set();
for (const entry of list) {
headerNamesSet.add(byteLowerCase(entry[0]));
}
const names = [...headerNamesSet].sort();
for (const name of names) {
// The following if statement, and if block of the following statement
// are 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") {
const setCookie = list.filter((entry) =>
byteLowerCase(entry[0]) === "set-cookie"
);
if (setCookie.length === 0) throw new TypeError("Unreachable");
for (const entry of setCookie) {
headers.push([name, entry[1]]);
}
} else {
const value = getHeader(list, name);
if (value === null) throw new TypeError("Unreachable");
headers.push([name, value]);
}
}
return headers;
}
/** @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 (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
throw new TypeError("Header name is not valid.");
}
if (this[_guard] == "immutable") {
throw new TypeError("Headers are immutable.");
}
const list = this[_headerList];
const lowercaseName = byteLowerCase(name);
for (let i = 0; i < list.length; i++) {
if (byteLowerCase(list[i][0]) === lowercaseName) {
list.splice(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 (!HTTP_TOKEN_CODE_POINT_RE.test(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 (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
throw new TypeError("Header name is not valid.");
}
const list = this[_headerList];
const lowercaseName = byteLowerCase(name);
for (let i = 0; i < list.length; i++) {
if (byteLowerCase(list[i][0]) === lowercaseName) {
return true;
}
}
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 (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
throw new TypeError("Header name is not valid.");
}
if (
value.includes("\x00") || value.includes("\x0A") ||
value.includes("\x0D")
) {
throw new TypeError("Header value is not valid.");
}
if (this[_guard] == "immutable") {
throw new TypeError("Headers are immutable.");
}
const list = this[_headerList];
const lowercaseName = byteLowerCase(name);
let added = false;
for (let i = 0; i < list.length; i++) {
if (byteLowerCase(list[i][0]) === lowercaseName) {
if (!added) {
list[i][1] = value;
added = true;
} else {
list.splice(i, 1);
i--;
}
}
}
if (!added) {
list.push([name, value]);
}
}
[Symbol.for("Deno.customInspect")](inspect) {
const headers = {};
for (const header of this) {
headers[header[0]] = header[1];
}
return `Headers ${inspect(headers)}`;
}
get [Symbol.toStringTag]() {
return "Headers";
}
}
webidl.mixinPairIterable("Headers", Headers, _iterableHeaders, 0, 1);
webidl.converters["sequence<ByteString>"] = webidl
.createSequenceConverter(webidl.converters["ByteString"]);
webidl.converters["sequence<sequence<ByteString>>"] = webidl
.createSequenceConverter(webidl.converters["sequence<ByteString>"]);
webidl.converters["record<ByteString, ByteString>"] = webidl
.createRecordConverter(
webidl.converters["ByteString"],
webidl.converters["ByteString"],
);
webidl.converters["HeadersInit"] = (V, opts) => {
// Union for (sequence<sequence<ByteString>> or record<ByteString, ByteString>)
if (typeof V === "object" && V !== null) {
if (V[Symbol.iterator] !== 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,
);
window.__bootstrap.headers = {
Headers,
};
})(this);