Skip to content

Commit

Permalink
chore: align fetch to spec (#10203)
Browse files Browse the repository at this point in the history
This commit aligns the `fetch` API and the `Request` / `Response`
classes belonging to it to the spec. This commit enables all the
relevant `fetch` WPT tests. Spec compliance is now at around 90%.

Performance is essentially identical now (within 1% of 1.9.0).
  • Loading branch information
lucacasonato authored Apr 20, 2021
1 parent 115197f commit 9e6cd91
Show file tree
Hide file tree
Showing 30 changed files with 2,219 additions and 1,368 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion cli/tests/077_fetch_empty.ts.out
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
[WILDCARD]error: Uncaught URIError: relative URL without a base
[WILDCARD]error: Uncaught TypeError: Invalid URL
[WILDCARD]
1 change: 1 addition & 0 deletions cli/tests/unit/body_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function buildBody(body: any, headers?: Headers): Body {
const stub = new Request("http://foo/", {
body: body,
headers,
method: "POST",
});
return stub as Body;
}
Expand Down
73 changes: 28 additions & 45 deletions cli/tests/unit/fetch_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ unitTest(
async (): Promise<void> => {
await fetch("http://<invalid>/");
},
URIError,
TypeError,
);
},
);
Expand Down Expand Up @@ -129,18 +129,6 @@ unitTest({ perms: { net: true } }, async function fetchBlob(): Promise<void> {
assertEquals(blob.size, Number(headers.get("Content-Length")));
});

unitTest({ perms: { net: true } }, async function fetchBodyUsed(): Promise<
void
> {
const response = await fetch("http://localhost:4545/cli/tests/fixture.json");
assertEquals(response.bodyUsed, false);
// deno-lint-ignore no-explicit-any
(response as any).bodyUsed = true;
assertEquals(response.bodyUsed, false);
await response.blob();
assertEquals(response.bodyUsed, true);
});

unitTest(
{ perms: { net: true } },
async function fetchBodyUsedReader(): Promise<void> {
Expand Down Expand Up @@ -278,7 +266,6 @@ unitTest(
TypeError,
"Invalid form data",
);
await response.body.cancel();
},
);

Expand Down Expand Up @@ -424,10 +411,11 @@ unitTest(
perms: { net: true },
},
async function fetchWithInfRedirection(): Promise<void> {
const response = await fetch("http://localhost:4549/cli/tests"); // will redirect to the same place
assertEquals(response.status, 0); // network error
assertEquals(response.type, "error");
assertEquals(response.ok, false);
await assertThrowsAsync(
() => fetch("http://localhost:4549/cli/tests"),
TypeError,
"redirect",
);
},
);

Expand Down Expand Up @@ -661,8 +649,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
"foo: Bar\r\n",
"hello: World\r\n",
"foo: Bar\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
Expand Down Expand Up @@ -695,9 +683,9 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
"content-type: text/plain;charset=UTF-8\r\n",
"foo: Bar\r\n",
"hello: World\r\n",
"foo: Bar\r\n",
"content-type: text/plain;charset=UTF-8\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
Expand Down Expand Up @@ -733,8 +721,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
"foo: Bar\r\n",
"hello: World\r\n",
"foo: Bar\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
Expand Down Expand Up @@ -770,8 +758,9 @@ unitTest(
}); // will redirect to http://localhost:4545/
assertEquals(response.status, 301);
assertEquals(response.url, "http://localhost:4546/");
assertEquals(response.type, "default");
assertEquals(response.type, "basic");
assertEquals(response.headers.get("Location"), "http://localhost:4545/");
await response.body!.cancel();
},
);

Expand All @@ -780,21 +769,14 @@ unitTest(
perms: { net: true },
},
async function fetchWithErrorRedirection(): Promise<void> {
const response = await fetch("http://localhost:4546/", {
redirect: "error",
}); // will redirect to http://localhost:4545/
assertEquals(response.status, 0);
assertEquals(response.statusText, "");
assertEquals(response.url, "");
assertEquals(response.type, "error");
try {
await response.text();
fail(
"Response.text() didn't throw on a filtered response without a body (type error)",
);
} catch (_e) {
return;
}
await assertThrowsAsync(
() =>
fetch("http://localhost:4546/", {
redirect: "error",
}),
TypeError,
"redirect",
);
},
);

Expand All @@ -803,7 +785,10 @@ unitTest(function responseRedirect(): void {
assertEquals(redir.status, 301);
assertEquals(redir.statusText, "");
assertEquals(redir.url, "");
assertEquals(redir.headers.get("Location"), "example.com/newLocation");
assertEquals(
redir.headers.get("Location"),
"http://js-unit-tests/foo/example.com/newLocation",
);
assertEquals(redir.type, "default");
});

Expand Down Expand Up @@ -1004,10 +989,7 @@ unitTest(function fetchResponseConstructorInvalidStatus(): void {
fail(`Invalid status: ${status}`);
} catch (e) {
assert(e instanceof RangeError);
assertEquals(
e.message,
`The status provided (${status}) is outside the range [200, 599]`,
);
assert(e.message.endsWith("is outside the range [200, 599]."));
}
}
});
Expand All @@ -1024,8 +1006,9 @@ unitTest(function fetchResponseEmptyConstructor(): void {
assertEquals([...response.headers], []);
});

// TODO(lucacasonato): reenable this test
unitTest(
{ perms: { net: true } },
{ perms: { net: true }, ignore: true },
async function fetchCustomHttpClientParamCertificateSuccess(): Promise<
void
> {
Expand Down Expand Up @@ -1115,8 +1098,8 @@ unitTest(
const actual = new TextDecoder().decode(buf.bytes());
const expected = [
"POST /blah HTTP/1.1\r\n",
"foo: Bar\r\n",
"hello: World\r\n",
"foo: Bar\r\n",
"accept: */*\r\n",
`user-agent: Deno/${Deno.version.deno}\r\n`,
"accept-encoding: gzip, br\r\n",
Expand Down
15 changes: 3 additions & 12 deletions cli/tests/unit/request_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,6 @@ unitTest(async function fromInit(): Promise<void> {
assertEquals(req.headers.get("test-header"), "value");
});

unitTest(async function fromRequest(): Promise<void> {
const r = new Request("http://foo/", { body: "ahoyhoy" });
r.headers.set("test-header", "value");

const req = new Request(r);

assertEquals(await r.text(), await req.text());
assertEquals(req.url, r.url);
assertEquals(req.headers.get("test-header"), r.headers.get("test-header"));
});

unitTest(function requestNonString(): void {
const nonString = {
toString() {
Expand All @@ -50,9 +39,11 @@ unitTest(function requestRelativeUrl(): void {

unitTest(async function cloneRequestBodyStream(): Promise<void> {
// hack to get a stream
const stream = new Request("http://foo/", { body: "a test body" }).body;
const stream =
new Request("http://foo/", { body: "a test body", method: "POST" }).body;
const r1 = new Request("http://foo/", {
body: stream,
method: "POST",
});

const r2 = r1.clone();
Expand Down
88 changes: 86 additions & 2 deletions op_crates/fetch/20_headers.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,14 @@
((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 _headerList = Symbol("header list");
Expand All @@ -35,7 +39,7 @@
*/

/**
* @typedef {string} potentialValue
* @param {string} potentialValue
* @returns {string}
*/
function normalizeHeaderValue(potentialValue) {
Expand Down Expand Up @@ -103,6 +107,7 @@
}

/**
* https://fetch.spec.whatwg.org/#concept-header-list-get
* @param {HeaderList} list
* @param {string} name
*/
Expand All @@ -118,10 +123,56 @@
}
}

/**
* 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 = value.replaceAll(HTTP_TAB_OR_SPACE_PREFIX_RE, "");
value = value.replaceAll(HTTP_TAB_OR_SPACE_SUFFIX_RE, "");

values.push(value);
value = "";
}
return values;
}

class Headers {
/** @type {HeaderList} */
[_headerList] = [];
/** @type {"immutable"| "request"| "request-no-cors"| "response" | "none"} */
/** @type {"immutable" | "request" | "request-no-cors" | "response" | "none"} */
[_guard];

get [_iterableHeaders]() {
Expand Down Expand Up @@ -359,7 +410,40 @@
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);
25 changes: 24 additions & 1 deletion op_crates/fetch/21_formdata.js
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,11 @@
* @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;
Expand Down Expand Up @@ -525,5 +530,23 @@
return parser.parse();
}

globalThis.__bootstrap.formData = { FormData, encodeFormData, parseFormData };
/**
* @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,
encodeFormData,
parseFormData,
formDataFromEntries,
};
})(globalThis);
Loading

0 comments on commit 9e6cd91

Please sign in to comment.