1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-22 15:24:46 -05:00

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.
This commit is contained in:
Luca Casonato 2021-04-19 01:00:13 +02:00 committed by GitHub
parent 0c5ecec8f6
commit 0552eaf569
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 552 additions and 514 deletions

View file

@ -1,88 +0,0 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals, unitTest } from "./test_util.ts";
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
function setup() {
const dataSymbol = Symbol("data symbol");
class Base {
[dataSymbol] = new Map<string, number>();
constructor(
data: Array<[string, number]> | IterableIterator<[string, number]>,
) {
for (const [key, value] of data) {
this[dataSymbol].set(key, value);
}
}
}
return {
Base,
// This is using an internal API we don't want published as types, so having
// to cast to any to "trick" TypeScript
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
DomIterable: Deno[Deno.internal].DomIterableMixin(Base, dataSymbol),
};
}
unitTest(function testDomIterable(): void {
const { DomIterable, Base } = setup();
const fixture: Array<[string, number]> = [
["foo", 1],
["bar", 2],
];
const domIterable = new DomIterable(fixture);
assertEquals(Array.from(domIterable.entries()), fixture);
assertEquals(Array.from(domIterable.values()), [1, 2]);
assertEquals(Array.from(domIterable.keys()), ["foo", "bar"]);
let result: Array<[string, number]> = [];
for (const [key, value] of domIterable) {
assert(key != null);
assert(value != null);
result.push([key, value]);
}
assertEquals(fixture, result);
result = [];
const scope = {};
function callback(
this: typeof scope,
value: number,
key: string,
parent: typeof domIterable,
): void {
assertEquals(parent, domIterable);
assert(key != null);
assert(value != null);
assert(this === scope);
result.push([key, value]);
}
domIterable.forEach(callback, scope);
assertEquals(fixture, result);
assertEquals(DomIterable.name, Base.name);
});
unitTest(function testDomIterableScope(): void {
const { DomIterable } = setup();
const domIterable = new DomIterable([["foo", 1]]);
// deno-lint-ignore no-explicit-any
function checkScope(thisArg: any, expected: any): void {
function callback(this: typeof thisArg): void {
assertEquals(this, expected);
}
domIterable.forEach(callback, thisArg);
}
checkScope(0, Object(0));
checkScope("", Object(""));
checkScope(null, window);
checkScope(undefined, window);
});

View file

@ -661,8 +661,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes()); const actual = new TextDecoder().decode(buf.bytes());
const expected = [ const expected = [
"POST /blah HTTP/1.1\r\n", "POST /blah HTTP/1.1\r\n",
"hello: World\r\n",
"foo: Bar\r\n", "foo: Bar\r\n",
"hello: World\r\n",
"accept: */*\r\n", "accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`, `user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n", "accept-encoding: gzip, br\r\n",
@ -695,9 +695,9 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes()); const actual = new TextDecoder().decode(buf.bytes());
const expected = [ const expected = [
"POST /blah HTTP/1.1\r\n", "POST /blah HTTP/1.1\r\n",
"hello: World\r\n",
"foo: Bar\r\n",
"content-type: text/plain;charset=UTF-8\r\n", "content-type: text/plain;charset=UTF-8\r\n",
"foo: Bar\r\n",
"hello: World\r\n",
"accept: */*\r\n", "accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`, `user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n", "accept-encoding: gzip, br\r\n",
@ -733,8 +733,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes()); const actual = new TextDecoder().decode(buf.bytes());
const expected = [ const expected = [
"POST /blah HTTP/1.1\r\n", "POST /blah HTTP/1.1\r\n",
"hello: World\r\n",
"foo: Bar\r\n", "foo: Bar\r\n",
"hello: World\r\n",
"accept: */*\r\n", "accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`, `user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n", "accept-encoding: gzip, br\r\n",
@ -1115,8 +1115,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes()); const actual = new TextDecoder().decode(buf.bytes());
const expected = [ const expected = [
"POST /blah HTTP/1.1\r\n", "POST /blah HTTP/1.1\r\n",
"hello: World\r\n",
"foo: Bar\r\n", "foo: Bar\r\n",
"hello: World\r\n",
"accept: */*\r\n", "accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`, `user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n", "accept-encoding: gzip, br\r\n",

View file

@ -1,10 +1,5 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
import { import { assert, assertEquals, unitTest } from "./test_util.ts";
assert,
assertEquals,
assertStringIncludes,
unitTest,
} from "./test_util.ts";
const { const {
inspectArgs, inspectArgs,
// @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol // @ts-expect-error TypeScript (as of 3.7) does not support indexing namespaces by symbol
@ -25,10 +20,7 @@ unitTest(function newHeaderTest(): void {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
new Headers(null as any); new Headers(null as any);
} catch (e) { } catch (e) {
assertEquals( assert(e instanceof TypeError);
e.message,
"Failed to construct 'Headers'; The provided value was not valid",
);
} }
}); });
@ -271,13 +263,11 @@ unitTest(function headerParamsArgumentsCheck(): void {
methodRequireOneParam.forEach((method): void => { methodRequireOneParam.forEach((method): void => {
const headers = new Headers(); const headers = new Headers();
let hasThrown = 0; let hasThrown = 0;
let errMsg = "";
try { try {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
(headers as any)[method](); (headers as any)[method]();
hasThrown = 1; hasThrown = 1;
} catch (err) { } catch (err) {
errMsg = err.message;
if (err instanceof TypeError) { if (err instanceof TypeError) {
hasThrown = 2; hasThrown = 2;
} else { } else {
@ -285,23 +275,17 @@ unitTest(function headerParamsArgumentsCheck(): void {
} }
} }
assertEquals(hasThrown, 2); assertEquals(hasThrown, 2);
assertStringIncludes(
errMsg,
`${method} requires at least 1 argument, but only 0 present`,
);
}); });
methodRequireTwoParams.forEach((method): void => { methodRequireTwoParams.forEach((method): void => {
const headers = new Headers(); const headers = new Headers();
let hasThrown = 0; let hasThrown = 0;
let errMsg = "";
try { try {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
(headers as any)[method](); (headers as any)[method]();
hasThrown = 1; hasThrown = 1;
} catch (err) { } catch (err) {
errMsg = err.message;
if (err instanceof TypeError) { if (err instanceof TypeError) {
hasThrown = 2; hasThrown = 2;
} else { } else {
@ -309,19 +293,13 @@ unitTest(function headerParamsArgumentsCheck(): void {
} }
} }
assertEquals(hasThrown, 2); assertEquals(hasThrown, 2);
assertStringIncludes(
errMsg,
`${method} requires at least 2 arguments, but only 0 present`,
);
hasThrown = 0; hasThrown = 0;
errMsg = "";
try { try {
// deno-lint-ignore no-explicit-any // deno-lint-ignore no-explicit-any
(headers as any)[method]("foo"); (headers as any)[method]("foo");
hasThrown = 1; hasThrown = 1;
} catch (err) { } catch (err) {
errMsg = err.message;
if (err instanceof TypeError) { if (err instanceof TypeError) {
hasThrown = 2; hasThrown = 2;
} else { } else {
@ -329,10 +307,6 @@ unitTest(function headerParamsArgumentsCheck(): void {
} }
} }
assertEquals(hasThrown, 2); assertEquals(hasThrown, 2);
assertStringIncludes(
errMsg,
`${method} requires at least 2 arguments, but only 1 present`,
);
}); });
}); });
@ -361,8 +335,8 @@ unitTest(function headersAppendMultiple(): void {
const actual = [...headers]; const actual = [...headers];
assertEquals(actual, [ assertEquals(actual, [
["set-cookie", "foo=bar"], ["set-cookie", "foo=bar"],
["x-deno", "foo, bar"],
["set-cookie", "bar=baz"], ["set-cookie", "bar=baz"],
["x-deno", "foo, bar"],
]); ]);
}); });
@ -372,22 +346,12 @@ unitTest(function headersAppendDuplicateSetCookieKey(): void {
headers.append("Set-cookie", "baz=bar"); headers.append("Set-cookie", "baz=bar");
const actual = [...headers]; const actual = [...headers];
assertEquals(actual, [ assertEquals(actual, [
["set-cookie", "foo=bar"],
["set-cookie", "foo=baz"], ["set-cookie", "foo=baz"],
["set-cookie", "baz=bar"], ["set-cookie", "baz=bar"],
]); ]);
}); });
unitTest(function headersSetDuplicateCookieKey(): void {
const headers = new Headers([["Set-Cookie", "foo=bar"]]);
headers.set("set-Cookie", "foo=baz");
headers.set("set-cookie", "bar=qat");
const actual = [...headers];
assertEquals(actual, [
["set-cookie", "foo=baz"],
["set-cookie", "bar=qat"],
]);
});
unitTest(function headersGetSetCookie(): void { unitTest(function headersGetSetCookie(): void {
const headers = new Headers([ const headers = new Headers([
["Set-Cookie", "foo=bar"], ["Set-Cookie", "foo=bar"],
@ -411,7 +375,7 @@ unitTest(function customInspectReturnsCorrectHeadersFormat(): void {
const singleHeader = new Headers([["Content-Type", "application/json"]]); const singleHeader = new Headers([["Content-Type", "application/json"]]);
assertEquals( assertEquals(
stringify(singleHeader), stringify(singleHeader),
"Headers { content-type: application/json }", `Headers { "content-type": "application/json" }`,
); );
const multiParamHeader = new Headers([ const multiParamHeader = new Headers([
["Content-Type", "application/json"], ["Content-Type", "application/json"],
@ -419,6 +383,6 @@ unitTest(function customInspectReturnsCorrectHeadersFormat(): void {
]); ]);
assertEquals( assertEquals(
stringify(multiParamHeader), stringify(multiParamHeader),
"Headers { content-type: application/json, content-length: 1337 }", `Headers { "content-length": "1337", "content-type": "application/json" }`,
); );
}); });

View file

@ -35,7 +35,6 @@ import "./io_test.ts";
import "./link_test.ts"; import "./link_test.ts";
import "./make_temp_test.ts"; import "./make_temp_test.ts";
import "./metrics_test.ts"; import "./metrics_test.ts";
import "./dom_iterable_test.ts";
import "./mkdir_test.ts"; import "./mkdir_test.ts";
import "./net_test.ts"; import "./net_test.ts";
import "./os_test.ts"; import "./os_test.ts";

View file

@ -1,80 +0,0 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
"use strict";
((window) => {
const { requiredArguments } = window.__bootstrap.fetchUtil;
function DomIterableMixin(
Base,
dataSymbol,
) {
// we have to cast `this` as `any` because there is no way to describe the
// Base class in a way where the Symbol `dataSymbol` is defined. So the
// runtime code works, but we do lose a little bit of type safety.
// Additionally, we have to not use .keys() nor .values() since the internal
// slot differs in type - some have a Map, which yields [K, V] in
// Symbol.iterator, and some have an Array, which yields V, in this case
// [K, V] too as they are arrays of tuples.
const DomIterable = class extends Base {
*entries() {
for (const entry of this[dataSymbol]) {
yield entry;
}
}
*keys() {
for (const [key] of this[dataSymbol]) {
yield key;
}
}
*values() {
for (const [, value] of this[dataSymbol]) {
yield value;
}
}
forEach(
callbackfn,
thisArg,
) {
requiredArguments(
`${this.constructor.name}.forEach`,
arguments.length,
1,
);
callbackfn = callbackfn.bind(
thisArg == null ? globalThis : Object(thisArg),
);
for (const [key, value] of this[dataSymbol]) {
callbackfn(value, key, this);
}
}
*[Symbol.iterator]() {
for (const entry of this[dataSymbol]) {
yield entry;
}
}
};
// we want the Base class name to be the name of the class.
Object.defineProperty(DomIterable, "name", {
value: Base.name,
configurable: true,
});
return DomIterable;
}
window.__bootstrap.internals = {
...window.__bootstrap.internals ?? {},
DomIterableMixin,
};
window.__bootstrap.domIterable = {
DomIterableMixin,
};
})(this);

View file

@ -1,247 +1,327 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. // 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"; "use strict";
((window) => { ((window) => {
const { DomIterableMixin } = window.__bootstrap.domIterable; const webidl = window.__bootstrap.webidl;
const { requiredArguments } = window.__bootstrap.fetchUtil; const {
HTTP_WHITESPACE_PREFIX_RE,
HTTP_WHITESPACE_SUFFIX_RE,
HTTP_TOKEN_CODE_POINT_RE,
byteLowerCase,
} = window.__bootstrap.infra;
// From node-fetch const _headerList = Symbol("header list");
// Copyright (c) 2016 David Frank. MIT License. const _iterableHeaders = Symbol("iterable headers");
const invalidTokenRegex = /[^\^_`a-zA-Z\-0-9!#$%&'*+.|~]/; const _guard = Symbol("guard");
const invalidHeaderCharRegex = /[^\t\x20-\x7e\x80-\xff]/;
function isHeaders(value) { /**
// eslint-disable-next-line @typescript-eslint/no-use-before-define * @typedef Header
return value instanceof Headers; * @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;
} }
const headersData = Symbol("headers data"); /**
* @param {Headers} headers
// TODO(bartlomieju): headerGuard? Investigate if it is needed * @param {HeadersInit} object
// node-fetch did not implement this but it is in the spec */
function normalizeParams(name, value) { function fillHeaders(headers, object) {
name = String(name).toLowerCase(); if (Array.isArray(object)) {
value = String(value).trim(); for (const header of object) {
return [name, value]; if (header.length !== 2) {
} throw new TypeError(
`Invalid header. Length must be 2, but is ${header.length}`,
// The following name/value validations are copied from );
// https://github.com/bitinn/node-fetch/blob/master/src/headers.js
// Copyright (c) 2016 David Frank. MIT License.
function validateName(name) {
if (invalidTokenRegex.test(name) || name === "") {
throw new TypeError(`${name} is not a legal HTTP header name`);
}
}
function validateValue(value) {
if (invalidHeaderCharRegex.test(value)) {
throw new TypeError(`${value} is not a legal HTTP header value`);
}
}
/** Appends a key and value to the header list.
*
* The spec indicates that when a key already exists, the append adds the new
* value onto the end of the existing value. The behaviour of this though
* varies when the key is `set-cookie`. In this case, if the key of the cookie
* already exists, the value is replaced, but if the key of the cookie does not
* exist, and additional `set-cookie` header is added.
*
* The browser specification of `Headers` is written for clients, and not
* servers, and Deno is a server, meaning that it needs to follow the patterns
* expected for servers, of which a `set-cookie` header is expected for each
* unique cookie key, but duplicate cookie keys should not exist. */
function dataAppend(
data,
key,
value,
) {
for (let i = 0; i < data.length; i++) {
const [dataKey] = data[i];
if (key === "set-cookie" && dataKey === "set-cookie") {
const [, dataValue] = data[i];
const [dataCookieKey] = dataValue.split("=");
const [cookieKey] = value.split("=");
if (dataCookieKey === cookieKey) {
data[i][1] = value;
return;
}
} else {
if (dataKey === key) {
data[i][1] += `, ${value}`;
return;
} }
appendHeader(headers, header[0], header[1]);
}
} else {
for (const key of Object.keys(object)) {
appendHeader(headers, key, object[key]);
} }
} }
data.push([key, value]);
} }
/** Gets a value of a key in the headers list. /**
* * https://fetch.spec.whatwg.org/#concept-headers-append
* This varies slightly from spec behaviour in that when the key is `set-cookie` * @param {Headers} headers
* the value returned will look like a concatenated value, when in fact, if the * @param {string} name
* headers were iterated over, each individual `set-cookie` value is a unique * @param {string} value
* entry in the headers list. */ */
function dataGet( function appendHeader(headers, name, value) {
data, // 1.
key, value = normalizeHeaderValue(value);
) {
const setCookieValues = []; // 2.
for (const [dataKey, value] of data) { if (!HTTP_TOKEN_CODE_POINT_RE.test(name)) {
if (dataKey === key) { throw new TypeError("Header name is not valid.");
if (key === "set-cookie") { }
setCookieValues.push(value); if (
} else { value.includes("\x00") || value.includes("\x0A") || value.includes("\x0D")
return value; ) {
} 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;
} }
} }
if (setCookieValues.length) { list.push([name, value]);
return setCookieValues.join(", ");
}
return undefined;
} }
/** Sets a value of a key in the headers list. /**
* * @param {HeaderList} list
* The spec indicates that the value should be replaced if the key already * @param {string} name
* exists. The behaviour here varies, where if the key is `set-cookie` the key */
* of the cookie is inspected, and if the key of the cookie already exists, function getHeader(list, name) {
* then the value is replaced. If the key of the cookie is not found, then const lowercaseName = byteLowerCase(name);
* the value of the `set-cookie` is added to the list of headers. const entries = list.filter((entry) =>
* byteLowerCase(entry[0]) === lowercaseName
* The browser specification of `Headers` is written for clients, and not ).map((entry) => entry[1]);
* servers, and Deno is a server, meaning that it needs to follow the patterns if (entries.length === 0) {
* expected for servers, of which a `set-cookie` header is expected for each return null;
* unique cookie key, but duplicate cookie keys should not exist. */ } else {
function dataSet( return entries.join("\x2C\x20");
data, }
key, }
value,
) { class Headers {
for (let i = 0; i < data.length; i++) { /** @type {HeaderList} */
const [dataKey] = data[i]; [_headerList] = [];
if (dataKey === key) { /** @type {"immutable"| "request"| "request-no-cors"| "response" | "none"} */
// there could be multiple set-cookie headers, but all others are unique [_guard];
if (key === "set-cookie") {
const [, dataValue] = data[i]; get [_iterableHeaders]() {
const [dataCookieKey] = dataValue.split("="); const list = this[_headerList];
const [cookieKey] = value.split("=");
if (cookieKey === dataCookieKey) { const headers = [];
data[i][1] = value; const headerNamesSet = new Set();
return; 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 { } else {
data[i][1] = value; const value = getHeader(list, name);
return; if (value === null) throw new TypeError("Unreachable");
headers.push([name, value]);
} }
} }
return headers;
} }
data.push([key, value]);
}
function dataDelete(data, key) { /** @param {HeadersInit} [init] */
let i = 0; constructor(init = undefined) {
while (i < data.length) { const prefix = "Failed to construct 'Event'";
const [dataKey] = data[i]; if (init !== undefined) {
if (dataKey === key) { init = webidl.converters["HeadersInit"](init, {
data.splice(i, 1); prefix,
} else { context: "Argument 1",
i++; });
} }
}
}
function dataHas(data, key) { this[webidl.brand] = webidl.brand;
for (const [dataKey] of data) { this[_guard] = "none";
if (dataKey === key) { if (init !== undefined) {
return true; fillHeaders(this, init);
}
}
return false;
}
// ref: https://fetch.spec.whatwg.org/#dom-headers
class HeadersBase {
constructor(init) {
if (init === null) {
throw new TypeError(
"Failed to construct 'Headers'; The provided value was not valid",
);
} else if (isHeaders(init)) {
this[headersData] = [...init];
} else {
this[headersData] = [];
if (Array.isArray(init)) {
for (const tuple of init) {
// If header does not contain exactly two items,
// then throw a TypeError.
// ref: https://fetch.spec.whatwg.org/#concept-headers-fill
requiredArguments(
"Headers.constructor tuple array argument",
tuple.length,
2,
);
this.append(tuple[0], tuple[1]);
}
} else if (init) {
for (const [rawName, rawValue] of Object.entries(init)) {
this.append(rawName, rawValue);
}
}
} }
} }
[Symbol.for("Deno.customInspect")]() { /**
let length = this[headersData].length; * @param {string} name
let output = ""; * @param {string} value
for (const [key, value] of this[headersData]) { */
const prefix = length === this[headersData].length ? " " : "";
const postfix = length === 1 ? " " : ", ";
output = output + `${prefix}${key}: ${value}${postfix}`;
length--;
}
return `Headers {${output}}`;
}
// ref: https://fetch.spec.whatwg.org/#concept-headers-append
append(name, value) { append(name, value) {
requiredArguments("Headers.append", arguments.length, 2); webidl.assertBranded(this, Headers);
const [newname, newvalue] = normalizeParams(name, value); const prefix = "Failed to execute 'append' on 'Headers'";
validateName(newname); webidl.requiredArguments(arguments.length, 2, { prefix });
validateValue(newvalue); name = webidl.converters["ByteString"](name, {
dataAppend(this[headersData], newname, newvalue); prefix,
context: "Argument 1",
});
value = webidl.converters["ByteString"](value, {
prefix,
context: "Argument 2",
});
appendHeader(this, name, value);
} }
/**
* @param {string} name
*/
delete(name) { delete(name) {
requiredArguments("Headers.delete", arguments.length, 1); const prefix = "Failed to execute 'delete' on 'Headers'";
const [newname] = normalizeParams(name); webidl.requiredArguments(arguments.length, 1, { prefix });
validateName(newname); name = webidl.converters["ByteString"](name, {
dataDelete(this[headersData], newname); 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) { get(name) {
requiredArguments("Headers.get", arguments.length, 1); const prefix = "Failed to execute 'get' on 'Headers'";
const [newname] = normalizeParams(name); webidl.requiredArguments(arguments.length, 1, { prefix });
validateName(newname); name = webidl.converters["ByteString"](name, {
return dataGet(this[headersData], newname) ?? null; 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) { has(name) {
requiredArguments("Headers.has", arguments.length, 1); const prefix = "Failed to execute 'has' on 'Headers'";
const [newname] = normalizeParams(name); webidl.requiredArguments(arguments.length, 1, { prefix });
validateName(newname); name = webidl.converters["ByteString"](name, {
return dataHas(this[headersData], newname); 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) { set(name, value) {
requiredArguments("Headers.set", arguments.length, 2); webidl.assertBranded(this, Headers);
const [newName, newValue] = normalizeParams(name, value); const prefix = "Failed to execute 'set' on 'Headers'";
validateName(newName); webidl.requiredArguments(arguments.length, 2, { prefix });
validateValue(newValue); name = webidl.converters["ByteString"](name, {
dataSet(this[headersData], newName, newValue); 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]() { get [Symbol.toStringTag]() {
@ -249,7 +329,35 @@
} }
} }
class Headers extends DomIterableMixin(HeadersBase, headersData) {} 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 = { window.__bootstrap.headers = {
Headers, Headers,

View file

@ -58,10 +58,6 @@ pub fn init(isolate: &mut JsRuntime) {
"deno:op_crates/fetch/01_fetch_util.js", "deno:op_crates/fetch/01_fetch_util.js",
include_str!("01_fetch_util.js"), include_str!("01_fetch_util.js"),
), ),
(
"deno:op_crates/fetch/03_dom_iterable.js",
include_str!("03_dom_iterable.js"),
),
( (
"deno:op_crates/fetch/11_streams.js", "deno:op_crates/fetch/11_streams.js",
include_str!("11_streams.js"), include_str!("11_streams.js"),

View file

@ -8,6 +8,74 @@
"use strict"; "use strict";
((window) => { ((window) => {
const ASCII_DIGIT = ["\u0030-\u0039"];
const ASCII_UPPER_ALPHA = ["\u0041-\u005A"];
const ASCII_LOWER_ALPHA = ["\u0061-\u007A"];
const ASCII_ALPHA = [...ASCII_UPPER_ALPHA, ...ASCII_LOWER_ALPHA];
const ASCII_ALPHANUMERIC = [...ASCII_DIGIT, ...ASCII_ALPHA];
const HTTP_TAB_OR_SPACE = ["\u0009", "\u0020"];
const HTTP_WHITESPACE = ["\u000A", "\u000D", ...HTTP_TAB_OR_SPACE];
const HTTP_TOKEN_CODE_POINT = [
"\u0021",
"\u0023",
"\u0024",
"\u0025",
"\u0026",
"\u0027",
"\u002A",
"\u002B",
"\u002D",
"\u002E",
"\u005E",
"\u005F",
"\u0060",
"\u007C",
"\u007E",
...ASCII_ALPHANUMERIC,
];
const HTTP_TOKEN_CODE_POINT_RE = new RegExp(
`^[${regexMatcher(HTTP_TOKEN_CODE_POINT)}]+$`,
);
const HTTP_QUOTED_STRING_TOKEN_POINT = [
"\u0009",
"\u0020-\u007E",
"\u0080-\u00FF",
];
const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new RegExp(
`^[${regexMatcher(HTTP_QUOTED_STRING_TOKEN_POINT)}]+$`,
);
const HTTP_WHITESPACE_MATCHER = regexMatcher(HTTP_WHITESPACE);
const HTTP_WHITESPACE_PREFIX_RE = new RegExp(
`^[${HTTP_WHITESPACE_MATCHER}]+`,
"g",
);
const HTTP_WHITESPACE_SUFFIX_RE = new RegExp(
`[${HTTP_WHITESPACE_MATCHER}]+$`,
"g",
);
/**
* Turn a string of chars into a regex safe matcher.
* @param {string[]} chars
* @returns {string}
*/
function regexMatcher(chars) {
const matchers = chars.map((char) => {
if (char.length === 1) {
return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
} else if (char.length === 3 && char[1] === "-") {
return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}-\\u${
char.charCodeAt(2).toString(16).padStart(4, "0")
}`;
} else {
throw TypeError("unreachable");
}
});
return matchers.join("");
}
/** /**
* https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points * https://infra.spec.whatwg.org/#collect-a-sequence-of-code-points
* @param {string} input * @param {string} input
@ -25,7 +93,43 @@
return { result: input.slice(start, position), position }; return { result: input.slice(start, position), position };
} }
/**
* @param {string} s
* @returns {string}
*/
function byteUpperCase(s) {
return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) {
return c.toUpperCase();
});
}
/**
* @param {string} s
* @returns {string}
*/
function byteLowerCase(s) {
return String(s).replace(/[A-Z]/g, function byteUpperCaseReplace(c) {
return c.toLowerCase();
});
}
window.__bootstrap.infra = { window.__bootstrap.infra = {
collectSequenceOfCodepoints, collectSequenceOfCodepoints,
ASCII_DIGIT,
ASCII_UPPER_ALPHA,
ASCII_LOWER_ALPHA,
ASCII_ALPHA,
ASCII_ALPHANUMERIC,
HTTP_TAB_OR_SPACE,
HTTP_WHITESPACE,
HTTP_TOKEN_CODE_POINT,
HTTP_TOKEN_CODE_POINT_RE,
HTTP_QUOTED_STRING_TOKEN_POINT,
HTTP_QUOTED_STRING_TOKEN_POINT_RE,
HTTP_WHITESPACE_PREFIX_RE,
HTTP_WHITESPACE_SUFFIX_RE,
regexMatcher,
byteUpperCase,
byteLowerCase,
}; };
})(globalThis); })(globalThis);

View file

@ -8,72 +8,14 @@
"use strict"; "use strict";
((window) => { ((window) => {
const { collectSequenceOfCodepoints } = window.__bootstrap.infra; const {
collectSequenceOfCodepoints,
/** HTTP_WHITESPACE,
* @param {string[]} chars HTTP_WHITESPACE_PREFIX_RE,
* @returns {string} HTTP_WHITESPACE_SUFFIX_RE,
*/ HTTP_QUOTED_STRING_TOKEN_POINT_RE,
function regexMatcher(chars) { HTTP_TOKEN_CODE_POINT_RE,
const matchers = chars.map((char) => { } = window.__bootstrap.infra;
if (char.length === 1) {
return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`;
} else if (char.length === 3 && char[1] === "-") {
return `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}-\\u${
char.charCodeAt(2).toString(16).padStart(4, "0")
}`;
} else {
throw TypeError("unreachable");
}
});
return matchers.join("");
}
const HTTP_TAB_OR_SPACE = ["\u0009", "\u0020"];
const HTTP_WHITESPACE = ["\u000A", "\u000D", ...HTTP_TAB_OR_SPACE];
const ASCII_DIGIT = ["\u0030-\u0039"];
const ASCII_UPPER_ALPHA = ["\u0041-\u005A"];
const ASCII_LOWER_ALPHA = ["\u0061-\u007A"];
const ASCII_ALPHA = [...ASCII_UPPER_ALPHA, ...ASCII_LOWER_ALPHA];
const ASCII_ALPHANUMERIC = [...ASCII_DIGIT, ...ASCII_ALPHA];
const HTTP_TOKEN_CODE_POINT = [
"\u0021",
"\u0023",
"\u0025",
"\u0026",
"\u0027",
"\u002A",
"\u002B",
"\u002D",
"\u002E",
"\u005E",
"\u005F",
"\u0060",
"\u007C",
"\u007E",
...ASCII_ALPHANUMERIC,
];
const HTTP_TOKEN_CODE_POINT_RE = new RegExp(
`^[${regexMatcher(HTTP_TOKEN_CODE_POINT)}]+$`,
);
const HTTP_QUOTED_STRING_TOKEN_POINT = [
"\u0009",
"\u0020-\u007E",
"\u0080-\u00FF",
];
const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new RegExp(
`^[${regexMatcher(HTTP_QUOTED_STRING_TOKEN_POINT)}]+$`,
);
const HTTP_WHITESPACE_MATCHER = regexMatcher(HTTP_WHITESPACE);
const HTTP_WHITESPACE_PREFIX_RE = new RegExp(
`^[${HTTP_WHITESPACE_MATCHER}]+`,
"g",
);
const HTTP_WHITESPACE_SUFFIX_RE = new RegExp(
`[${HTTP_WHITESPACE_MATCHER}]+$`,
"g",
);
/** /**
* https://fetch.spec.whatwg.org/#collect-an-http-quoted-string * https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
@ -131,8 +73,16 @@
return { result: input.substring(positionStart, position + 1), position }; return { result: input.substring(positionStart, position + 1), position };
} }
/**
* @typedef MimeType
* @property {string} type
* @property {string} subtype
* @property {Map<string,string>} parameters
*/
/** /**
* @param {string} input * @param {string} input
* @returns {MimeType | null}
*/ */
function parseMimeType(input) { function parseMimeType(input) {
// 1. // 1.

View file

@ -17,15 +17,32 @@ declare namespace globalThis {
result: string; result: string;
position: number; position: number;
}; };
ASCII_DIGIT: string[];
ASCII_UPPER_ALPHA: string[];
ASCII_LOWER_ALPHA: string[];
ASCII_ALPHA: string[];
ASCII_ALPHANUMERIC: string[];
HTTP_TAB_OR_SPACE: string[];
HTTP_WHITESPACE: string[];
HTTP_TOKEN_CODE_POINT: string[];
HTTP_TOKEN_CODE_POINT_RE: RegExp;
HTTP_QUOTED_STRING_TOKEN_POINT: string[];
HTTP_QUOTED_STRING_TOKEN_POINT_RE: RegExp;
HTTP_WHITESPACE_PREFIX_RE: RegExp;
HTTP_WHITESPACE_SUFFIX_RE: RegExp;
regexMatcher(chars: string[]): string;
byteUpperCase(s: string): string;
byteLowerCase(s: string): string;
}; };
declare var mimesniff: { declare namespace mimesniff {
parseMimeType(input: string): { declare interface MimeType {
type: string; type: string;
subtype: string; subtype: string;
parameters: Map<string, string>; parameters: Map<string, string>;
} | null; }
}; declare function parseMimeType(input: string): MimeType | null;
}
declare var eventTarget: { declare var eventTarget: {
EventTarget: typeof EventTarget; EventTarget: typeof EventTarget;

View file

@ -764,12 +764,16 @@
opts, opts,
); );
} }
const keys = Reflect.ownKeys(V);
const result = {}; const result = {};
for (const key of V) { for (const key of keys) {
const typedKey = keyConverter(key, opts); const desc = Object.getOwnPropertyDescriptor(V, key);
const value = V[key]; if (desc !== undefined && desc.enumerable === true) {
const typedValue = valueConverter(value, opts); const typedKey = keyConverter(key, opts);
result[typedKey] = typedValue; const value = V[key];
const typedValue = valueConverter(value, opts);
result[typedKey] = typedValue;
}
} }
return result; return result;
}; };
@ -802,29 +806,81 @@
throw new TypeError("Illegal constructor"); throw new TypeError("Illegal constructor");
} }
function define(target, source) {
for (const key of Reflect.ownKeys(source)) {
const descriptor = Reflect.getOwnPropertyDescriptor(source, key);
if (descriptor && !Reflect.defineProperty(target, key, descriptor)) {
throw new TypeError(`Cannot redefine property: ${String(key)}`);
}
}
}
const _iteratorInternal = Symbol("iterator internal");
const globalIteratorPrototype = Object.getPrototypeOf(Object.getPrototypeOf(
[][Symbol.iterator](),
));
function mixinPairIterable(name, prototype, dataSymbol, keyKey, valueKey) { function mixinPairIterable(name, prototype, dataSymbol, keyKey, valueKey) {
const methods = { const iteratorPrototype = Object.create(globalIteratorPrototype, {
*entries() { [Symbol.toStringTag]: { configurable: true, value: `${name} Iterator` },
assertBranded(this, prototype); });
for (const entry of this[dataSymbol]) { define(iteratorPrototype, {
yield [entry[keyKey], entry[valueKey]]; next() {
const internal = this && this[_iteratorInternal];
if (!internal) {
throw new TypeError(
`next() called on a value that is not a ${name} iterator object`,
);
} }
const { target, kind, index } = internal;
const values = target[dataSymbol];
const len = values.length;
if (index >= len) {
return { value: undefined, done: true };
}
const pair = values[index];
internal.index = index + 1;
let result;
switch (kind) {
case "key":
result = pair[keyKey];
break;
case "value":
result = pair[valueKey];
break;
case "key+value":
result = [pair[keyKey], pair[valueKey]];
break;
}
return { value: result, done: false };
},
});
function createDefaultIterator(target, kind) {
const iterator = Object.create(iteratorPrototype);
Object.defineProperty(iterator, _iteratorInternal, {
value: { target, kind, index: 0 },
configurable: true,
});
return iterator;
}
const methods = {
entries() {
assertBranded(this, prototype);
return createDefaultIterator(this, "key+value");
}, },
[Symbol.iterator]() { [Symbol.iterator]() {
assertBranded(this, prototype); assertBranded(this, prototype);
return this.entries(); return createDefaultIterator(this, "key+value");
}, },
*keys() { keys() {
assertBranded(this, prototype); assertBranded(this, prototype);
for (const entry of this[dataSymbol]) { return createDefaultIterator(this, "key");
yield entry[keyKey];
}
}, },
*values() { values() {
assertBranded(this, prototype); assertBranded(this, prototype);
for (const entry of this[dataSymbol]) { return createDefaultIterator(this, "value");
yield entry[valueKey];
}
}, },
forEach(idlCallback, thisArg) { forEach(idlCallback, thisArg) {
assertBranded(this, prototype); assertBranded(this, prototype);

@ -1 +1 @@
Subproject commit e19bdbe96243f2ba548c1fd01c0812d645ba0c6f Subproject commit 579608584916d582d38d0159666aae9a6aaf07ad

View file

@ -684,6 +684,15 @@
"Check isReloadNavigation attribute", "Check isReloadNavigation attribute",
"Check isHistoryNavigation attribute" "Check isHistoryNavigation attribute"
] ]
},
"headers": {
"headers-basic.any.js": true,
"headers-casing.any.js": true,
"headers-combine.any.js": true,
"headers-errors.any.js": true,
"headers-normalize.any.js": true,
"headers-record.any.js": true,
"headers-structure.any.js": true
} }
}, },
"data-urls": { "data-urls": {

View file

@ -25,6 +25,9 @@ export async function runWithTestUtil<T>(
} }
const passedTime = performance.now() - start; const passedTime = performance.now() - start;
if (passedTime > 15000) { if (passedTime > 15000) {
proc.kill(2);
await proc.status();
proc.close();
throw new Error("Timed out while trying to start wpt test util."); throw new Error("Timed out while trying to start wpt test util.");
} }
} }