Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: align fetch to spec #10203

Merged
merged 12 commits into from
Apr 20, 2021
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",
lucacasonato marked this conversation as resolved.
Show resolved Hide resolved
);
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