summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock1
-rw-r--r--cli/tests/077_fetch_empty.ts.out2
-rw-r--r--cli/tests/unit/body_test.ts1
-rw-r--r--cli/tests/unit/fetch_test.ts73
-rw-r--r--cli/tests/unit/request_test.ts15
-rw-r--r--op_crates/fetch/20_headers.js88
-rw-r--r--op_crates/fetch/21_formdata.js25
-rw-r--r--op_crates/fetch/22_body.js338
-rw-r--r--op_crates/fetch/22_http_client.js41
-rw-r--r--op_crates/fetch/23_request.js521
-rw-r--r--op_crates/fetch/23_response.js415
-rw-r--r--op_crates/fetch/26_fetch.js1296
-rw-r--r--op_crates/fetch/internal.d.ts93
-rw-r--r--op_crates/fetch/lib.rs35
-rw-r--r--op_crates/url/00_url.js11
-rw-r--r--op_crates/url/Cargo.toml1
-rw-r--r--op_crates/url/internal.d.ts1
-rw-r--r--op_crates/url/lib.rs21
-rw-r--r--op_crates/web/00_infra.js68
-rw-r--r--op_crates/web/01_mimesniff.js88
-rw-r--r--op_crates/web/03_abort_signal.js7
-rw-r--r--op_crates/web/internal.d.ts12
-rw-r--r--op_crates/webgpu/02_idl_types.js36
-rw-r--r--op_crates/webidl/00_webidl.js97
-rw-r--r--runtime/js/40_http.js108
-rw-r--r--runtime/js/99_main.js2
-rw-r--r--runtime/ops/http.rs10
m---------test_util/wpt0
-rw-r--r--tools/wpt/expectation.json178
-rw-r--r--tools/wpt/runner.ts1
30 files changed, 2218 insertions, 1367 deletions
diff --git a/Cargo.lock b/Cargo.lock
index ea145c803..f1283b696 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -737,6 +737,7 @@ dependencies = [
"bench_util",
"deno_core",
"idna",
+ "percent-encoding",
"serde",
]
diff --git a/cli/tests/077_fetch_empty.ts.out b/cli/tests/077_fetch_empty.ts.out
index d94652bda..e546cfcec 100644
--- a/cli/tests/077_fetch_empty.ts.out
+++ b/cli/tests/077_fetch_empty.ts.out
@@ -1,2 +1,2 @@
-[WILDCARD]error: Uncaught URIError: relative URL without a base
+[WILDCARD]error: Uncaught TypeError: Invalid URL
[WILDCARD]
diff --git a/cli/tests/unit/body_test.ts b/cli/tests/unit/body_test.ts
index 2c94bb5f5..d889abfab 100644
--- a/cli/tests/unit/body_test.ts
+++ b/cli/tests/unit/body_test.ts
@@ -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;
}
diff --git a/cli/tests/unit/fetch_test.ts b/cli/tests/unit/fetch_test.ts
index 427ab9b53..a46104ff8 100644
--- a/cli/tests/unit/fetch_test.ts
+++ b/cli/tests/unit/fetch_test.ts
@@ -79,7 +79,7 @@ unitTest(
async (): Promise<void> => {
await fetch("http://<invalid>/");
},
- URIError,
+ TypeError,
);
},
);
@@ -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> {
@@ -278,7 +266,6 @@ unitTest(
TypeError,
"Invalid form data",
);
- await response.body.cancel();
},
);
@@ -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",
+ );
},
);
@@ -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",
@@ -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",
@@ -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",
@@ -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();
},
);
@@ -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",
+ );
},
);
@@ -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");
});
@@ -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]."));
}
}
});
@@ -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
> {
@@ -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",
diff --git a/cli/tests/unit/request_test.ts b/cli/tests/unit/request_test.ts
index a8cbed370..7c4fa4ad0 100644
--- a/cli/tests/unit/request_test.ts
+++ b/cli/tests/unit/request_test.ts
@@ -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() {
@@ -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();
diff --git a/op_crates/fetch/20_headers.js b/op_crates/fetch/20_headers.js
index ce46e5dee..3c6fc0b26 100644
--- a/op_crates/fetch/20_headers.js
+++ b/op_crates/fetch/20_headers.js
@@ -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");
@@ -35,7 +39,7 @@
*/
/**
- * @typedef {string} potentialValue
+ * @param {string} potentialValue
* @returns {string}
*/
function normalizeHeaderValue(potentialValue) {
@@ -103,6 +107,7 @@
}
/**
+ * https://fetch.spec.whatwg.org/#concept-header-list-get
* @param {HeaderList} list
* @param {string} name
*/
@@ -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]() {
@@ -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);
diff --git a/op_crates/fetch/21_formdata.js b/op_crates/fetch/21_formdata.js
index 106b67da4..00f97f346 100644
--- a/op_crates/fetch/21_formdata.js
+++ b/op_crates/fetch/21_formdata.js
@@ -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;
@@ -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);
diff --git a/op_crates/fetch/22_body.js b/op_crates/fetch/22_body.js
new file mode 100644
index 000000000..1c4ce4271
--- /dev/null
+++ b/op_crates/fetch/22_body.js
@@ -0,0 +1,338 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../webidl/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
+/// <reference path="../url/lib.deno_url.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 core = window.Deno.core;
+ const webidl = globalThis.__bootstrap.webidl;
+ const { parseUrlEncoded } = globalThis.__bootstrap.url;
+ const { parseFormData, formDataFromEntries, encodeFormData } =
+ globalThis.__bootstrap.formData;
+ const mimesniff = globalThis.__bootstrap.mimesniff;
+ const { isReadableStreamDisturbed } = globalThis.__bootstrap.streams;
+
+ class InnerBody {
+ /** @type {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} */
+ streamOrStatic;
+ /** @type {null | Uint8Array | Blob | FormData} */
+ source = null;
+ /** @type {null | number} */
+ length = null;
+
+ /**
+ * @param {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} stream
+ */
+ constructor(stream) {
+ this.streamOrStatic = stream ??
+ { body: new Uint8Array(), consumed: false };
+ }
+
+ get stream() {
+ if (!(this.streamOrStatic instanceof ReadableStream)) {
+ const { body, consumed } = this.streamOrStatic;
+ this.streamOrStatic = new ReadableStream({
+ start(controller) {
+ controller.enqueue(body);
+ controller.close();
+ },
+ });
+ if (consumed) {
+ this.streamOrStatic.cancel();
+ }
+ }
+ return this.streamOrStatic;
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#body-unusable
+ * @returns {boolean}
+ */
+ unusable() {
+ if (this.streamOrStatic instanceof ReadableStream) {
+ return this.streamOrStatic.locked ||
+ isReadableStreamDisturbed(this.streamOrStatic);
+ }
+ return this.streamOrStatic.consumed;
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ consumed() {
+ if (this.streamOrStatic instanceof ReadableStream) {
+ return isReadableStreamDisturbed(this.streamOrStatic);
+ }
+ return this.streamOrStatic.consumed;
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#concept-body-consume-body
+ * @returns {Promise<Uint8Array>}
+ */
+ async consume() {
+ if (this.unusable()) throw new TypeError("Body already consumed.");
+ if (this.streamOrStatic instanceof ReadableStream) {
+ const reader = this.stream.getReader();
+ /** @type {Uint8Array[]} */
+ const chunks = [];
+ let totalLength = 0;
+ while (true) {
+ const { value: chunk, done } = await reader.read();
+ if (done) break;
+ chunks.push(chunk);
+ totalLength += chunk.byteLength;
+ }
+ const finalBuffer = new Uint8Array(totalLength);
+ let i = 0;
+ for (const chunk of chunks) {
+ finalBuffer.set(chunk, i);
+ i += chunk.byteLength;
+ }
+ return finalBuffer;
+ } else {
+ this.streamOrStatic.consumed = true;
+ return this.streamOrStatic.body;
+ }
+ }
+
+ /**
+ * @returns {InnerBody}
+ */
+ clone() {
+ const [out1, out2] = this.stream.tee();
+ this.streamOrStatic = out1;
+ const second = new InnerBody(out2);
+ second.source = core.deserialize(core.serialize(this.source));
+ second.length = this.length;
+ return second;
+ }
+ }
+
+ /**
+ * @param {any} prototype
+ * @param {symbol} bodySymbol
+ * @param {symbol} mimeTypeSymbol
+ * @returns {void}
+ */
+ function mixinBody(prototype, bodySymbol, mimeTypeSymbol) {
+ function consumeBody(object) {
+ if (object[bodySymbol] !== null) {
+ return object[bodySymbol].consume();
+ }
+ return Promise.resolve(new Uint8Array());
+ }
+
+ /** @type {PropertyDescriptorMap} */
+ const mixin = {
+ body: {
+ /**
+ * @returns {ReadableStream<Uint8Array> | null}
+ */
+ get() {
+ webidl.assertBranded(this, prototype);
+ if (this[bodySymbol] === null) {
+ return null;
+ } else {
+ return this[bodySymbol].stream;
+ }
+ },
+ },
+ bodyUsed: {
+ /**
+ * @returns {boolean}
+ */
+ get() {
+ webidl.assertBranded(this, prototype);
+ if (this[bodySymbol] !== null) {
+ return this[bodySymbol].consumed();
+ }
+ return false;
+ },
+ },
+ arrayBuffer: {
+ /** @returns {Promise<ArrayBuffer>} */
+ value: async function arrayBuffer() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "ArrayBuffer");
+ },
+ },
+ blob: {
+ /** @returns {Promise<Blob>} */
+ value: async function blob() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "Blob", this[mimeTypeSymbol]);
+ },
+ },
+ formData: {
+ /** @returns {Promise<FormData>} */
+ value: async function formData() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "FormData", this[mimeTypeSymbol]);
+ },
+ },
+ json: {
+ /** @returns {Promise<any>} */
+ value: async function json() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "JSON");
+ },
+ },
+ text: {
+ /** @returns {Promise<string>} */
+ value: async function text() {
+ webidl.assertBranded(this, prototype);
+ const body = await consumeBody(this);
+ return packageData(body, "text");
+ },
+ },
+ };
+ return Object.defineProperties(prototype.prototype, mixin);
+ }
+
+ const decoder = new TextDecoder();
+
+ /**
+ * https://fetch.spec.whatwg.org/#concept-body-package-data
+ * @param {Uint8Array} bytes
+ * @param {"ArrayBuffer" | "Blob" | "FormData" | "JSON" | "text"} type
+ * @param {MimeType | null} [mimeType]
+ */
+ function packageData(bytes, type, mimeType) {
+ switch (type) {
+ case "ArrayBuffer":
+ return bytes.buffer;
+ case "Blob":
+ return new Blob([bytes], {
+ type: mimeType !== null ? mimesniff.serializeMimeType(mimeType) : "",
+ });
+ case "FormData": {
+ if (mimeType !== null) {
+ if (mimeType !== null) {
+ const essence = mimesniff.essence(mimeType);
+ if (essence === "multipart/form-data") {
+ const boundary = mimeType.parameters.get("boundary");
+ if (boundary === null) {
+ throw new TypeError(
+ "Missing boundary parameter in mime type of multipart formdata.",
+ );
+ }
+ return parseFormData(bytes, boundary);
+ } else if (essence === "application/x-www-form-urlencoded") {
+ const entries = parseUrlEncoded(bytes);
+ return formDataFromEntries(
+ entries.map((x) => ({ name: x[0], value: x[1] })),
+ );
+ }
+ }
+ throw new TypeError("Invalid form data");
+ }
+ throw new TypeError("Missing content type");
+ }
+ case "JSON":
+ return JSON.parse(decoder.decode(bytes));
+ case "text":
+ return decoder.decode(bytes);
+ }
+ }
+
+ const encoder = new TextEncoder();
+
+ /**
+ * @param {BodyInit} object
+ * @returns {{body: InnerBody, contentType: string | null}}
+ */
+ function extractBody(object) {
+ /** @type {ReadableStream<Uint8Array> | { body: Uint8Array, consumed: boolean }} */
+ let stream;
+ let source = null;
+ let length = null;
+ let contentType = null;
+ if (object instanceof Blob) {
+ stream = object.stream();
+ source = object;
+ length = object.size;
+ if (object.type.length !== 0) {
+ contentType = object.type;
+ }
+ } else if (ArrayBuffer.isView(object) || object instanceof ArrayBuffer) {
+ const u8 = ArrayBuffer.isView(object)
+ ? new Uint8Array(
+ object.buffer,
+ object.byteOffset,
+ object.byteLength,
+ )
+ : new Uint8Array(object);
+ const copy = u8.slice(0, u8.byteLength);
+ source = copy;
+ } else if (object instanceof FormData) {
+ const res = encodeFormData(object);
+ stream = { body: res.body, consumed: false };
+ source = object;
+ length = res.body.byteLength;
+ contentType = res.contentType;
+ } else if (object instanceof URLSearchParams) {
+ source = encoder.encode(object.toString());
+ contentType = "application/x-www-form-urlencoded;charset=UTF-8";
+ } else if (typeof object === "string") {
+ source = encoder.encode(object);
+ contentType = "text/plain;charset=UTF-8";
+ } else if (object instanceof ReadableStream) {
+ stream = object;
+ if (object.locked || isReadableStreamDisturbed(object)) {
+ throw new TypeError("ReadableStream is locked or disturbed");
+ }
+ }
+ if (source instanceof Uint8Array) {
+ stream = { body: source, consumed: false };
+ length = source.byteLength;
+ }
+ const body = new InnerBody(stream);
+ body.source = source;
+ body.length = length;
+ return { body, contentType };
+ }
+
+ webidl.converters["BodyInit"] = (V, opts) => {
+ // Union for (ReadableStream or Blob or ArrayBufferView or ArrayBuffer or FormData or URLSearchParams or USVString)
+ if (V instanceof ReadableStream) {
+ // TODO(lucacasonato): ReadableStream is not branded
+ return V;
+ } else if (V instanceof Blob) {
+ return webidl.converters["Blob"](V, opts);
+ } else if (V instanceof FormData) {
+ return webidl.converters["FormData"](V, opts);
+ } else if (V instanceof URLSearchParams) {
+ // TODO(lucacasonato): URLSearchParams is not branded
+ return V;
+ }
+ if (typeof V === "object") {
+ if (V instanceof ArrayBuffer || V instanceof SharedArrayBuffer) {
+ return webidl.converters["ArrayBuffer"](V, opts);
+ }
+ if (ArrayBuffer.isView(V)) {
+ return webidl.converters["ArrayBufferView"](V, opts);
+ }
+ }
+ return webidl.converters["USVString"](V, opts);
+ };
+ webidl.converters["BodyInit?"] = webidl.createNullableConverter(
+ webidl.converters["BodyInit"],
+ );
+
+ window.__bootstrap.fetchBody = { mixinBody, InnerBody, extractBody };
+})(globalThis);
diff --git a/op_crates/fetch/22_http_client.js b/op_crates/fetch/22_http_client.js
new file mode 100644
index 000000000..0a4be9e9f
--- /dev/null
+++ b/op_crates/fetch/22_http_client.js
@@ -0,0 +1,41 @@
+// 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="../url/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 core = window.Deno.core;
+
+ /**
+ * @param {Deno.CreateHttpClientOptions} options
+ * @returns {HttpClient}
+ */
+ function createHttpClient(options) {
+ return new HttpClient(core.opSync("op_create_http_client", options));
+ }
+
+ class HttpClient {
+ /**
+ * @param {number} rid
+ */
+ constructor(rid) {
+ this.rid = rid;
+ }
+ close() {
+ core.close(this.rid);
+ }
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.createHttpClient = createHttpClient;
+ window.__bootstrap.fetch.HttpClient = HttpClient;
+})(globalThis);
diff --git a/op_crates/fetch/23_request.js b/op_crates/fetch/23_request.js
new file mode 100644
index 000000000..f3764d96f
--- /dev/null
+++ b/op_crates/fetch/23_request.js
@@ -0,0 +1,521 @@
+// 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_TOKEN_CODE_POINT_RE, byteUpperCase } = window.__bootstrap.infra;
+ const { URL } = window.__bootstrap.url;
+ const { guardFromHeaders } = window.__bootstrap.headers;
+ const { InnerBody, mixinBody, extractBody } = window.__bootstrap.fetchBody;
+ const { getLocationHref } = window.__bootstrap.location;
+ const mimesniff = window.__bootstrap.mimesniff;
+ const {
+ headersFromHeaderList,
+ headerListFromHeaders,
+ fillHeaders,
+ getDecodeSplitHeader,
+ } = window.__bootstrap.headers;
+ const { HttpClient } = window.__bootstrap.fetch;
+
+ const _request = Symbol("request");
+ const _headers = Symbol("headers");
+ const _mimeType = Symbol("mime type");
+ const _body = Symbol("body");
+
+ /**
+ * @typedef InnerRequest
+ * @property {string} method
+ * @property {() => string} url
+ * @property {() => string} currentUrl
+ * @property {[string, string][]} headerList
+ * @property {null | InnerBody} body
+ * @property {"follow" | "error" | "manual"} redirectMode
+ * @property {number} redirectCount
+ * @property {string[]} urlList
+ * @property {number | null} clientRid NOTE: non standard extension for `Deno.HttpClient`.
+ */
+
+ const defaultInnerRequest = {
+ url() {
+ return this.urlList[0];
+ },
+ currentUrl() {
+ return this.urlList[this.urlList.length - 1];
+ },
+ redirectMode: "follow",
+ redirectCount: 0,
+ clientRid: null,
+ };
+
+ /**
+ * @param {string} method
+ * @param {string} url
+ * @param {[string, string][]} headerList
+ * @param {InnerBody} body
+ * @returns
+ */
+ function newInnerRequest(method, url, headerList = [], body = null) {
+ return {
+ method: method,
+ headerList,
+ body,
+ urlList: [url],
+ ...defaultInnerRequest,
+ };
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#concept-request-clone
+ * @param {InnerRequest} request
+ * @returns {InnerRequest}
+ */
+ function cloneInnerRequest(request) {
+ const headerList = [...request.headerList.map((x) => [x[0], x[1]])];
+ let body = null;
+ if (request.body !== null) {
+ body = request.body.clone();
+ }
+
+ return {
+ method: request.method,
+ url() {
+ return this.urlList[0];
+ },
+ currentUrl() {
+ return this.urlList[this.urlList.length - 1];
+ },
+ headerList,
+ body,
+ redirectMode: request.redirectMode,
+ redirectCount: request.redirectCount,
+ urlList: request.urlList,
+ clientRid: request.clientRid,
+ };
+ }
+
+ /**
+ * @param {string} m
+ * @returns {boolean}
+ */
+ function isKnownMethod(m) {
+ return (
+ m === "DELETE" ||
+ m === "GET" ||
+ m === "HEAD" ||
+ m === "OPTIONS" ||
+ m === "POST" ||
+ m === "PUT"
+ );
+ }
+ /**
+ * @param {string} m
+ * @returns {string}
+ */
+ function validateAndNormalizeMethod(m) {
+ // Fast path for well-known methods
+ if (isKnownMethod(m)) {
+ return m;
+ }
+
+ // Regular path
+ if (!HTTP_TOKEN_CODE_POINT_RE.test(m)) {
+ throw new TypeError("Method is not valid.");
+ }
+ const upperCase = byteUpperCase(m);
+ if (
+ upperCase === "CONNECT" || upperCase === "TRACE" || upperCase === "TRACK"
+ ) {
+ throw new TypeError("Method is forbidden.");
+ }
+ return upperCase;
+ }
+
+ class Request {
+ /** @type {InnerRequest} */
+ [_request];
+ /** @type {Headers} */
+ [_headers];
+ get [_mimeType]() {
+ let charset = null;
+ let essence = null;
+ let mimeType = null;
+ const values = getDecodeSplitHeader(
+ headerListFromHeaders(this[_headers]),
+ "Content-Type",
+ );
+ if (values === null) return null;
+ for (const value of values) {
+ const temporaryMimeType = mimesniff.parseMimeType(value);
+ if (
+ temporaryMimeType === null ||
+ mimesniff.essence(temporaryMimeType) == "*/*"
+ ) {
+ continue;
+ }
+ mimeType = temporaryMimeType;
+ if (mimesniff.essence(mimeType) !== essence) {
+ charset = null;
+ const newCharset = mimeType.parameters.get("charset");
+ if (newCharset !== undefined) {
+ charset = newCharset;
+ }
+ essence = mimesniff.essence(mimeType);
+ } else {
+ if (mimeType.parameters.has("charset") === null && charset !== null) {
+ mimeType.parameters.set("charset", charset);
+ }
+ }
+ }
+ if (mimeType === null) return null;
+ return mimeType;
+ }
+ get [_body]() {
+ return this[_request].body;
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#dom-request
+ * @param {RequestInfo} input
+ * @param {RequestInit} init
+ */
+ constructor(input, init = {}) {
+ const prefix = "Failed to construct 'Request'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ input = webidl.converters["RequestInfo"](input, {
+ prefix,
+ context: "Argument 1",
+ });
+ init = webidl.converters["RequestInit"](init, {
+ prefix,
+ context: "Argument 2",
+ });
+
+ this[webidl.brand] = webidl.brand;
+
+ /** @type {InnerRequest} */
+ let request;
+ const baseURL = getLocationHref();
+
+ // 5.
+ if (typeof input === "string") {
+ const parsedURL = new URL(input, baseURL);
+ request = newInnerRequest("GET", parsedURL.href, [], null);
+ } else { // 6.
+ if (!(input instanceof Request)) throw new TypeError("Unreachable");
+ request = input[_request];
+ }
+
+ // 22.
+ if (init.redirect !== undefined) {
+ request.redirectMode = init.redirect;
+ }
+
+ // 25.
+ if (init.method !== undefined) {
+ let method = init.method;
+ method = validateAndNormalizeMethod(method);
+ request.method = method;
+ }
+
+ // NOTE: non standard extension. This handles Deno.HttpClient parameter
+ if (init.client !== undefined) {
+ if (init.client !== null && !(init.client instanceof HttpClient)) {
+ throw webidl.makeException(
+ TypeError,
+ "`client` must be a Deno.HttpClient",
+ { prefix, context: "Argument 2" },
+ );
+ }
+ request.clientRid = init.client?.rid ?? null;
+ }
+
+ // 27.
+ this[_request] = request;
+
+ // 29.
+ this[_headers] = headersFromHeaderList(request.headerList, "request");
+
+ // 31.
+ if (Object.keys(init).length > 0) {
+ let headers = headerListFromHeaders(this[_headers]);
+ if (init.headers !== undefined) {
+ headers = init.headers;
+ }
+ headerListFromHeaders(this[_headers]).slice(
+ 0,
+ headerListFromHeaders(this[_headers]).length,
+ );
+ fillHeaders(this[_headers], headers);
+ }
+
+ // 32.
+ let inputBody = null;
+ if (input instanceof Request) {
+ inputBody = input[_body];
+ }
+
+ // 33.
+ if (
+ (request.method === "GET" || request.method === "HEAD") &&
+ ((init.body !== undefined && init.body !== null) ||
+ inputBody !== null)
+ ) {
+ throw new TypeError("HEAD and GET requests may not have a body.");
+ }
+
+ // 34.
+ let initBody = null;
+
+ // 35.
+ if (init.body !== undefined && init.body !== null) {
+ const res = extractBody(init.body);
+ initBody = res.body;
+ if (res.contentType !== null && !this[_headers].has("content-type")) {
+ this[_headers].append("Content-Type", res.contentType);
+ }
+ }
+
+ // 36.
+ const inputOrInitBody = initBody ?? inputBody;
+
+ // 38.
+ const finalBody = inputOrInitBody;
+
+ // 39.
+ // TODO(lucacasonato): implement this step. Is it needed?
+
+ // 40.
+ request.body = finalBody;
+ }
+
+ get method() {
+ webidl.assertBranded(this, Request);
+ return this[_request].method;
+ }
+
+ get url() {
+ webidl.assertBranded(this, Request);
+ return this[_request].url();
+ }
+
+ get headers() {
+ webidl.assertBranded(this, Request);
+ return this[_headers];
+ }
+
+ get destination() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get referrer() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get referrerPolicy() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get mode() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get credentials() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get cache() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get redirect() {
+ webidl.assertBranded(this, Request);
+ return this[_request].redirectMode;
+ }
+
+ get integrity() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get keepalive() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get isReloadNavigation() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get isHistoryNavigation() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ get signal() {
+ webidl.assertBranded(this, Request);
+ throw new TypeError("This property is not implemented.");
+ }
+
+ clone() {
+ webidl.assertBranded(this, Request);
+ if (this[_body] && this[_body].unusable()) {
+ throw new TypeError("Body is unusable.");
+ }
+ const newReq = cloneInnerRequest(this[_request]);
+ return fromInnerRequest(newReq, guardFromHeaders(this[_headers]));
+ }
+
+ get [Symbol.toStringTag]() {
+ return "Request";
+ }
+
+ [Symbol.for("Deno.customInspect")](inspect) {
+ const inner = {
+ bodyUsed: this.bodyUsed,
+ headers: this.headers,
+ method: this.method,
+ redirect: this.redirect,
+ url: this.url(),
+ };
+ return `Request ${inspect(inner)}`;
+ }
+ }
+
+ mixinBody(Request, _body, _mimeType);
+
+ webidl.converters["Request"] = webidl.createInterfaceConverter(
+ "Request",
+ Request,
+ );
+ webidl.converters["RequestInfo"] = (V, opts) => {
+ // Union for (Request or USVString)
+ if (typeof V == "object") {
+ if (V instanceof Request) {
+ return webidl.converters["Request"](V, opts);
+ }
+ }
+ return webidl.converters["USVString"](V, opts);
+ };
+
+ webidl.converters["ReferrerPolicy"] = webidl.createEnumConverter(
+ "ReferrerPolicy",
+ [
+ "",
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "same-origin",
+ "origin",
+ "strict-origin",
+ "origin-when-cross-origin",
+ "strict-origin-when-cross-origin",
+ "unsafe-url",
+ ],
+ );
+ webidl.converters["RequestMode"] = webidl.createEnumConverter("RequestMode", [
+ "navigate",
+ "same-origin",
+ "no-cors",
+ "cors",
+ ]);
+ webidl.converters["RequestCredentials"] = webidl.createEnumConverter(
+ "RequestCredentials",
+ [
+ "omit",
+ "same-origin",
+ "include",
+ ],
+ );
+ webidl.converters["RequestCache"] = webidl.createEnumConverter(
+ "RequestCache",
+ [
+ "default",
+ "no-store",
+ "reload",
+ "no-cache",
+ "force-cache",
+ "only-if-cached",
+ ],
+ );
+ webidl.converters["RequestRedirect"] = webidl.createEnumConverter(
+ "RequestRedirect",
+ [
+ "follow",
+ "error",
+ "manual",
+ ],
+ );
+ webidl.converters["RequestInit"] = webidl.createDictionaryConverter(
+ "RequestInit",
+ [
+ { key: "method", converter: webidl.converters["ByteString"] },
+ { key: "headers", converter: webidl.converters["HeadersInit"] },
+ {
+ key: "body",
+ converter: webidl.createNullableConverter(
+ webidl.converters["BodyInit"],
+ ),
+ },
+ { key: "referrer", converter: webidl.converters["USVString"] },
+ { key: "referrerPolicy", converter: webidl.converters["ReferrerPolicy"] },
+ { key: "mode", converter: webidl.converters["RequestMode"] },
+ {
+ key: "credentials",
+ converter: webidl.converters["RequestCredentials"],
+ },
+ { key: "cache", converter: webidl.converters["RequestCache"] },
+ { key: "redirect", converter: webidl.converters["RequestRedirect"] },
+ { key: "integrity", converter: webidl.converters["DOMString"] },
+ { key: "keepalive", converter: webidl.converters["boolean"] },
+ {
+ key: "signal",
+ converter: webidl.createNullableConverter(
+ webidl.converters["AbortSignal"],
+ ),
+ },
+ { key: "client", converter: webidl.converters.any },
+ ],
+ );
+
+ /**
+ * @param {Request} request
+ * @returns {InnerRequest}
+ */
+ function toInnerRequest(request) {
+ return request[_request];
+ }
+
+ /**
+ * @param {InnerRequest} inner
+ * @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard
+ * @returns {Request}
+ */
+ function fromInnerRequest(inner, guard) {
+ const request = webidl.createBranded(Request);
+ request[_request] = inner;
+ request[_headers] = headersFromHeaderList(inner.headerList, guard);
+ return request;
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.Request = Request;
+ window.__bootstrap.fetch.toInnerRequest = toInnerRequest;
+ window.__bootstrap.fetch.fromInnerRequest = fromInnerRequest;
+ window.__bootstrap.fetch.newInnerRequest = newInnerRequest;
+})(globalThis);
diff --git a/op_crates/fetch/23_response.js b/op_crates/fetch/23_response.js
new file mode 100644
index 000000000..44b74f789
--- /dev/null
+++ b/op_crates/fetch/23_response.js
@@ -0,0 +1,415 @@
+// 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="../url/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_TAB_OR_SPACE, regexMatcher } = window.__bootstrap.infra;
+ const { InnerBody, extractBody, mixinBody } = window.__bootstrap.fetchBody;
+ const { getLocationHref } = window.__bootstrap.location;
+ const mimesniff = window.__bootstrap.mimesniff;
+ const { URL } = window.__bootstrap.url;
+ const {
+ getDecodeSplitHeader,
+ headerListFromHeaders,
+ headersFromHeaderList,
+ guardFromHeaders,
+ fillHeaders,
+ } = window.__bootstrap.headers;
+
+ const VCHAR = ["\x21-\x7E"];
+ const OBS_TEXT = ["\x80-\xFF"];
+
+ const REASON_PHRASE = [...HTTP_TAB_OR_SPACE, ...VCHAR, ...OBS_TEXT];
+ const REASON_PHRASE_MATCHER = regexMatcher(REASON_PHRASE);
+ const REASON_PHRASE_RE = new RegExp(`^[${REASON_PHRASE_MATCHER}]*$`);
+
+ const _response = Symbol("response");
+ const _headers = Symbol("headers");
+ const _mimeType = Symbol("mime type");
+ const _body = Symbol("body");
+
+ /**
+ * @typedef InnerResponse
+ * @property {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"} type
+ * @property {() => string | null} url
+ * @property {string[]} urlList
+ * @property {number} status
+ * @property {string} statusMessage
+ * @property {[string, string][]} headerList
+ * @property {null | InnerBody} body
+ * @property {string} [error]
+ */
+
+ /**
+ * @param {number} status
+ * @returns {boolean}
+ */
+ function nullBodyStatus(status) {
+ return status === 101 || status === 204 || status === 205 || status === 304;
+ }
+
+ /**
+ * @param {number} status
+ * @returns {boolean}
+ */
+ function redirectStatus(status) {
+ return status === 301 || status === 302 || status === 303 ||
+ status === 307 || status === 308;
+ }
+
+ /**
+ * https://fetch.spec.whatwg.org/#concept-response-clone
+ * @param {InnerResponse} response
+ * @returns {InnerResponse}
+ */
+ function cloneInnerResponse(response) {
+ const urlList = [...response.urlList];
+ const headerList = [...response.headerList.map((x) => [x[0], x[1]])];
+ let body = null;
+ if (response.body !== null) {
+ body = response.body.clone();
+ }
+
+ return {
+ type: response.type,
+ body,
+ headerList,
+ url() {
+ if (this.urlList.length == 0) return null;
+ return this.urlList[this.urlList.length - 1];
+ },
+ urlList,
+ status: response.status,
+ statusMessage: response.statusMessage,
+ };
+ }
+
+ const defaultInnerResponse = {
+ type: "default",
+ body: null,
+ url() {
+ if (this.urlList.length == 0) return null;
+ return this.urlList[this.urlList.length - 1];
+ },
+ };
+
+ /**
+ * @returns {InnerResponse}
+ */
+ function newInnerResponse(status = 200, statusMessage = "") {
+ return {
+ headerList: [],
+ urlList: [],
+ status,
+ statusMessage,
+ ...defaultInnerResponse,
+ };
+ }
+
+ /**
+ * @param {string} error
+ * @returns {InnerResponse}
+ */
+ function networkError(error) {
+ const resp = newInnerResponse(0);
+ resp.type = "error";
+ resp.error = error;
+ return resp;
+ }
+
+ class Response {
+ /** @type {InnerResponse} */
+ [_response];
+ /** @type {Headers} */
+ [_headers];
+ get [_mimeType]() {
+ let charset = null;
+ let essence = null;
+ let mimeType = null;
+ const values = getDecodeSplitHeader(
+ headerListFromHeaders(this[_headers]),
+ "Content-Type",
+ );
+ if (values === null) return null;
+ for (const value of values) {
+ const temporaryMimeType = mimesniff.parseMimeType(value);
+ if (
+ temporaryMimeType === null ||
+ mimesniff.essence(temporaryMimeType) == "*/*"
+ ) {
+ continue;
+ }
+ mimeType = temporaryMimeType;
+ if (mimesniff.essence(mimeType) !== essence) {
+ charset = null;
+ const newCharset = mimeType.parameters.get("charset");
+ if (newCharset !== undefined) {
+ charset = newCharset;
+ }
+ essence = mimesniff.essence(mimeType);
+ } else {
+ if (mimeType.parameters.has("charset") === null && charset !== null) {
+ mimeType.parameters.set("charset", charset);
+ }
+ }
+ }
+ if (mimeType === null) return null;
+ return mimeType;
+ }
+ get [_body]() {
+ return this[_response].body;
+ }
+
+ /**
+ * @returns {Response}
+ */
+ static error() {
+ const inner = newInnerResponse(0);
+ inner.type = "error";
+ const response = webidl.createBranded(Response);
+ response[_response] = inner;
+ response[_headers] = headersFromHeaderList(
+ response[_response].headerList,
+ "immutable",
+ );
+ return response;
+ }
+
+ /**
+ * @param {string} url
+ * @param {number} status
+ * @returns {Response}
+ */
+ static redirect(url, status = 302) {
+ const prefix = "Failed to call 'Response.redirect'";
+ url = webidl.converters["USVString"](url, {
+ prefix,
+ context: "Argument 1",
+ });
+ status = webidl.converters["unsigned short"](status, {
+ prefix,
+ context: "Argument 2",
+ });
+
+ const baseURL = getLocationHref();
+ const parsedURL = new URL(url, baseURL);
+ if (!redirectStatus(status)) {
+ throw new RangeError("Invalid redirect status code.");
+ }
+ const inner = newInnerResponse(status);
+ inner.type = "default";
+ inner.headerList.push(["Location", parsedURL.href]);
+ const response = webidl.createBranded(Response);
+ response[_response] = inner;
+ response[_headers] = headersFromHeaderList(
+ response[_response].headerList,
+ "immutable",
+ );
+ return response;
+ }
+
+ /**
+ * @param {BodyInit | null} body
+ * @param {ResponseInit} init
+ */
+ constructor(body = null, init = {}) {
+ const prefix = "Failed to construct 'Response'";
+ body = webidl.converters["BodyInit?"](body, {
+ prefix,
+ context: "Argument 1",
+ });
+ init = webidl.converters["ResponseInit"](init, {
+ prefix,
+ context: "Argument 2",
+ });
+
+ if (init.status < 200 || init.status > 599) {
+ throw new RangeError(
+ `The status provided (${init.status}) is outside the range [200, 599].`,
+ );
+ }
+
+ if (!REASON_PHRASE_RE.test(init.statusText)) {
+ throw new TypeError("Status text is not valid.");
+ }
+
+ this[webidl.brand] = webidl.brand;
+ const response = newInnerResponse(init.status, init.statusText);
+ this[_response] = response;
+ this[_headers] = headersFromHeaderList(response.headerList, "response");
+ if (init.headers !== undefined) {
+ fillHeaders(this[_headers], init.headers);
+ }
+ if (body !== null) {
+ if (nullBodyStatus(response.status)) {
+ throw new TypeError(
+ "Response with null body status cannot have body",
+ );
+ }
+ const res = extractBody(body);
+ response.body = res.body;
+ if (res.contentType !== null && !this[_headers].has("content-type")) {
+ this[_headers].append("Content-Type", res.contentType);
+ }
+ }
+ }
+
+ /**
+ * @returns {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"}
+ */
+ get type() {
+ webidl.assertBranded(this, Response);
+ return this[_response].type;
+ }
+
+ /**
+ * @returns {string}
+ */
+ get url() {
+ webidl.assertBranded(this, Response);
+ const url = this[_response].url();
+ if (url === null) return "";
+ const newUrl = new URL(url);
+ newUrl.hash = "";
+ return newUrl.href;
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ get redirected() {
+ webidl.assertBranded(this, Response);
+ return this[_response].urlList.length > 1;
+ }
+
+ /**
+ * @returns {number}
+ */
+ get status() {
+ webidl.assertBranded(this, Response);
+ return this[_response].status;
+ }
+
+ /**
+ * @returns {boolean}
+ */
+ get ok() {
+ webidl.assertBranded(this, Response);
+ const status = this[_response].status;
+ return status >= 200 && status <= 299;
+ }
+
+ /**
+ * @returns {string}
+ */
+ get statusText() {
+ webidl.assertBranded(this, Response);
+ return this[_response].statusMessage;
+ }
+
+ /**
+ * @returns {Headers}
+ */
+ get headers() {
+ webidl.assertBranded(this, Response);
+ return this[_headers];
+ }
+
+ /**
+ * @returns {Response}
+ */
+ clone() {
+ webidl.assertBranded(this, Response);
+ if (this[_body] && this[_body].unusable()) {
+ throw new TypeError("Body is unusable.");
+ }
+ const second = webidl.createBranded(Response);
+ const newRes = cloneInnerResponse(this[_response]);
+ second[_response] = newRes;
+ second[_headers] = headersFromHeaderList(
+ newRes.headerList,
+ guardFromHeaders(this[_headers]),
+ );
+ return second;
+ }
+
+ get [Symbol.toStringTag]() {
+ return "Response";
+ }
+
+ [Symbol.for("Deno.customInspect")](inspect) {
+ const inner = {
+ body: this.body,
+ bodyUsed: this.bodyUsed,
+ headers: this.headers,
+ ok: this.ok,
+ redirected: this.redirected,
+ status: this.status,
+ statusText: this.statusText,
+ url: this.url(),
+ };
+ return `Response ${inspect(inner)}`;
+ }
+ }
+
+ mixinBody(Response, _body, _mimeType);
+
+ webidl.converters["Response"] = webidl.createInterfaceConverter(
+ "Response",
+ Response,
+ );
+ webidl.converters["ResponseInit"] = webidl.createDictionaryConverter(
+ "ResponseInit",
+ [{
+ key: "status",
+ defaultValue: 200,
+ converter: webidl.converters["unsigned short"],
+ }, {
+ key: "statusText",
+ defaultValue: "",
+ converter: webidl.converters["ByteString"],
+ }, {
+ key: "headers",
+ converter: webidl.converters["HeadersInit"],
+ }],
+ );
+
+ /**
+ * @param {Response} response
+ * @returns {InnerResponse}
+ */
+ function toInnerResponse(response) {
+ return response[_response];
+ }
+
+ /**
+ * @param {InnerResponse} inner
+ * @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard
+ * @returns {Response}
+ */
+ function fromInnerResponse(inner, guard) {
+ const response = webidl.createBranded(Response);
+ response[_response] = inner;
+ response[_headers] = headersFromHeaderList(inner.headerList, guard);
+ return response;
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.Response = Response;
+ window.__bootstrap.fetch.toInnerResponse = toInnerResponse;
+ window.__bootstrap.fetch.fromInnerResponse = fromInnerResponse;
+ window.__bootstrap.fetch.redirectStatus = redirectStatus;
+ window.__bootstrap.fetch.nullBodyStatus = nullBodyStatus;
+ window.__bootstrap.fetch.networkError = networkError;
+})(globalThis);
diff --git a/op_crates/fetch/26_fetch.js b/op_crates/fetch/26_fetch.js
index d07121e86..a6d1608d0 100644
--- a/op_crates/fetch/26_fetch.js
+++ b/op_crates/fetch/26_fetch.js
@@ -13,497 +13,36 @@
((window) => {
const core = window.Deno.core;
-
- // provided by "deno_web"
- const { URLSearchParams } = window.__bootstrap.url;
- const { getLocationHref } = window.__bootstrap.location;
- const { FormData, parseFormData, encodeFormData } =
- window.__bootstrap.formData;
- const { parseMimeType } = window.__bootstrap.mimesniff;
-
- const { ReadableStream, isReadableStreamDisturbed } =
- window.__bootstrap.streams;
- const { Headers } = window.__bootstrap.headers;
- const { Blob, _byteSequence, File } = window.__bootstrap.file;
-
- const MAX_SIZE = 2 ** 32 - 2;
-
- /**
- * @param {Uint8Array} src
- * @param {Uint8Array} dst
- * @param {number} off the offset into `dst` where it will at which to begin writing values from `src`
- *
- * @returns {number} number of bytes copied
- */
- function copyBytes(src, dst, off = 0) {
- const r = dst.byteLength - off;
- if (src.byteLength > r) {
- src = src.subarray(0, r);
- }
- dst.set(src, off);
- return src.byteLength;
- }
-
- class Buffer {
- /** @type {Uint8Array} */
- #buf; // contents are the bytes buf[off : len(buf)]
- #off = 0; // read at buf[off], write at buf[buf.byteLength]
-
- /** @param {ArrayBuffer} [ab] */
- constructor(ab) {
- if (ab == null) {
- this.#buf = new Uint8Array(0);
- return;
- }
-
- this.#buf = new Uint8Array(ab);
- }
-
- /**
- * @returns {Uint8Array}
- */
- bytes(options = { copy: true }) {
- if (options.copy === false) return this.#buf.subarray(this.#off);
- return this.#buf.slice(this.#off);
- }
-
- /**
- * @returns {boolean}
- */
- empty() {
- return this.#buf.byteLength <= this.#off;
- }
-
- /**
- * @returns {number}
- */
- get length() {
- return this.#buf.byteLength - this.#off;
- }
-
- /**
- * @returns {number}
- */
- get capacity() {
- return this.#buf.buffer.byteLength;
- }
-
- /**
- * @returns {void}
- */
- reset() {
- this.#reslice(0);
- this.#off = 0;
- }
-
- /**
- * @param {number} n
- * @returns {number}
- */
- #tryGrowByReslice = (n) => {
- const l = this.#buf.byteLength;
- if (n <= this.capacity - l) {
- this.#reslice(l + n);
- return l;
- }
- return -1;
- };
-
- /**
- * @param {number} len
- * @returns {void}
- */
- #reslice = (len) => {
- if (!(len <= this.#buf.buffer.byteLength)) {
- throw new Error("assert");
- }
- this.#buf = new Uint8Array(this.#buf.buffer, 0, len);
- };
-
- /**
- * @param {Uint8Array} p
- * @returns {number}
- */
- writeSync(p) {
- const m = this.#grow(p.byteLength);
- return copyBytes(p, this.#buf, m);
- }
-
- /**
- * @param {Uint8Array} p
- * @returns {Promise<number>}
- */
- write(p) {
- const n = this.writeSync(p);
- return Promise.resolve(n);
- }
-
- /**
- * @param {number} n
- * @returns {number}
- */
- #grow = (n) => {
- const m = this.length;
- // If buffer is empty, reset to recover space.
- if (m === 0 && this.#off !== 0) {
- this.reset();
- }
- // Fast: Try to grow by means of a reslice.
- const i = this.#tryGrowByReslice(n);
- if (i >= 0) {
- return i;
- }
- const c = this.capacity;
- if (n <= Math.floor(c / 2) - m) {
- // We can slide things down instead of allocating a new
- // ArrayBuffer. We only need m+n <= c to slide, but
- // we instead let capacity get twice as large so we
- // don't spend all our time copying.
- copyBytes(this.#buf.subarray(this.#off), this.#buf);
- } else if (c + n > MAX_SIZE) {
- throw new Error("The buffer cannot be grown beyond the maximum size.");
- } else {
- // Not enough space anywhere, we need to allocate.
- const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE));
- copyBytes(this.#buf.subarray(this.#off), buf);
- this.#buf = buf;
- }
- // Restore this.#off and len(this.#buf).
- this.#off = 0;
- this.#reslice(Math.min(m + n, MAX_SIZE));
- return m;
- };
-
- /**
- * @param {number} n
- * @returns {void}
- */
- grow(n) {
- if (n < 0) {
- throw Error("Buffer.grow: negative count");
- }
- const m = this.#grow(n);
- this.#reslice(m);
- }
- }
-
- /**
- * @param {unknown} x
- * @returns {x is ArrayBufferView}
- */
- function isTypedArray(x) {
- return ArrayBuffer.isView(x) && !(x instanceof DataView);
- }
-
- /**
- * @param {string} s
- * @param {string} value
- * @returns {boolean}
- */
- function hasHeaderValueOf(s, value) {
- return new RegExp(`^${value}(?:[\\s;]|$)`).test(s);
- }
-
- /**
- * @param {string} name
- * @param {BodyInit | null} bodySource
- */
- function validateBodyType(name, bodySource) {
- if (isTypedArray(bodySource)) {
- return true;
- } else if (bodySource instanceof ArrayBuffer) {
- return true;
- } else if (typeof bodySource === "string") {
- return true;
- } else if (bodySource instanceof ReadableStream) {
- return true;
- } else if (bodySource instanceof FormData) {
- return true;
- } else if (bodySource instanceof URLSearchParams) {
- return true;
- } else if (!bodySource) {
- return true; // null body is fine
- }
- throw new TypeError(
- `Bad ${name} body type: ${bodySource.constructor.name}`,
- );
- }
-
- /**
- * @param {ReadableStreamReader<Uint8Array>} stream
- * @param {number} [size]
- */
- async function bufferFromStream(
- stream,
- size,
- ) {
- const encoder = new TextEncoder();
- const buffer = new Buffer();
-
- if (size) {
- // grow to avoid unnecessary allocations & copies
- buffer.grow(size);
- }
-
- while (true) {
- const { done, value } = await stream.read();
-
- if (done) break;
-
- if (typeof value === "string") {
- buffer.writeSync(encoder.encode(value));
- } else if (value instanceof ArrayBuffer) {
- buffer.writeSync(new Uint8Array(value));
- } else if (value instanceof Uint8Array) {
- buffer.writeSync(value);
- } else if (!value) {
- // noop for undefined
- } else {
- throw new Error("unhandled type on stream read");
- }
- }
-
- return buffer.bytes().buffer;
- }
-
- /**
- * @param {Exclude<BodyInit, ReadableStream> | null} bodySource
- */
- function bodyToArrayBuffer(bodySource) {
- if (isTypedArray(bodySource)) {
- return bodySource.buffer;
- } else if (bodySource instanceof ArrayBuffer) {
- return bodySource;
- } else if (typeof bodySource === "string") {
- const enc = new TextEncoder();
- return enc.encode(bodySource).buffer;
- } else if (
- bodySource instanceof FormData ||
- bodySource instanceof URLSearchParams
- ) {
- const enc = new TextEncoder();
- return enc.encode(bodySource.toString()).buffer;
- } else if (!bodySource) {
- return new ArrayBuffer(0);
- }
- throw new Error(
- `Body type not implemented: ${bodySource.constructor.name}`,
- );
- }
-
- const BodyUsedError =
- "Failed to execute 'clone' on 'Body': body is already used";
-
- const teeBody = Symbol("Body#tee");
-
- // fastBody and dontValidateUrl allow users to opt out of certain behaviors
- const fastBody = Symbol("Body#fast");
- const dontValidateUrl = Symbol("dontValidateUrl");
- const lazyHeaders = Symbol("lazyHeaders");
-
- class Body {
- #contentType = "";
- #size;
- /** @type {BodyInit | null} */
- #bodySource;
- /** @type {ReadableStream<Uint8Array> | null} */
- #stream = null;
-
- /**
- * @param {BodyInit| null} bodySource
- * @param {{contentType: string, size?: number}} meta
- */
- constructor(bodySource, meta) {
- validateBodyType(this.constructor.name, bodySource);
- this.#bodySource = bodySource;
- this.#contentType = meta.contentType;
- this.#size = meta.size;
- }
-
- get body() {
- if (!this.#stream) {
- if (!this.#bodySource) {
- return null;
- } else if (this.#bodySource instanceof ReadableStream) {
- this.#stream = this.#bodySource;
- } else {
- const buf = bodyToArrayBuffer(this.#bodySource);
- if (!(buf instanceof ArrayBuffer)) {
- throw new Error(
- `Expected ArrayBuffer from body`,
- );
- }
-
- this.#stream = new ReadableStream({
- /**
- * @param {ReadableStreamDefaultController<Uint8Array>} controller
- */
- start(controller) {
- controller.enqueue(new Uint8Array(buf));
- controller.close();
- },
- });
- }
- }
-
- return this.#stream;
- }
-
- // Optimization that allows caller to bypass expensive ReadableStream.
- [fastBody]() {
- if (!this.#bodySource) {
- return null;
- } else if (!(this.#bodySource instanceof ReadableStream)) {
- return bodyToArrayBuffer(this.#bodySource);
- } else {
- return this.body;
- }
- }
-
- /** @returns {BodyInit | null} */
- [teeBody]() {
- if (this.#stream || this.#bodySource instanceof ReadableStream) {
- const body = this.body;
- if (body) {
- const [stream1, stream2] = body.tee();
- this.#stream = stream1;
- return stream2;
- } else {
- return null;
- }
- }
-
- return this.#bodySource;
- }
-
- get bodyUsed() {
- if (this.body && isReadableStreamDisturbed(this.body)) {
- return true;
- }
- return false;
- }
-
- set bodyUsed(_) {
- // this is a noop per spec
- }
-
- /** @returns {Promise<Blob>} */
- async blob() {
- return new Blob([await this.arrayBuffer()], {
- type: this.#contentType,
- });
- }
-
- // ref: https://fetch.spec.whatwg.org/#body-mixin
- /** @returns {Promise<FormData>} */
- async formData() {
- const formData = new FormData();
- const mimeType = parseMimeType(this.#contentType);
- if (mimeType) {
- if (mimeType.type === "multipart" && mimeType.subtype === "form-data") {
- // ref: https://tools.ietf.org/html/rfc2046#section-5.1
- const boundary = mimeType.parameters.get("boundary");
- const body = new Uint8Array(await this.arrayBuffer());
- return parseFormData(body, boundary);
- } else if (
- mimeType.type === "application" &&
- mimeType.subtype === "x-www-form-urlencoded"
- ) {
- // From https://github.com/github/fetch/blob/master/fetch.js
- // Copyright (c) 2014-2016 GitHub, Inc. MIT License
- const body = await this.text();
- try {
- body
- .trim()
- .split("&")
- .forEach((bytes) => {
- if (bytes) {
- const split = bytes.split("=");
- if (split.length >= 2) {
- // @ts-expect-error this is safe because of the above check
- const name = split.shift().replace(/\+/g, " ");
- const value = split.join("=").replace(/\+/g, " ");
- formData.append(
- decodeURIComponent(name),
- decodeURIComponent(value),
- );
- }
- }
- });
- } catch (e) {
- throw new TypeError("Invalid form urlencoded format");
- }
- return formData;
- }
- }
-
- throw new TypeError("Invalid form data");
- }
-
- /** @returns {Promise<string>} */
- async text() {
- if (typeof this.#bodySource === "string") {
- return this.#bodySource;
- }
-
- const ab = await this.arrayBuffer();
- const decoder = new TextDecoder("utf-8");
- return decoder.decode(ab);
- }
-
- /** @returns {Promise<any>} */
- async json() {
- const raw = await this.text();
- return JSON.parse(raw);
- }
-
- /** @returns {Promise<ArrayBuffer>} */
- arrayBuffer() {
- if (this.#bodySource instanceof ReadableStream) {
- const body = this.body;
- if (!body) throw new TypeError("Unreachable state (no body)");
- return bufferFromStream(body.getReader(), this.#size);
- }
- return Promise.resolve(bodyToArrayBuffer(this.#bodySource));
- }
- }
+ const webidl = window.__bootstrap.webidl;
+ const { byteLowerCase } = window.__bootstrap.infra;
+ const { InnerBody, extractBody } = window.__bootstrap.fetchBody;
+ const {
+ toInnerRequest,
+ fromInnerResponse,
+ redirectStatus,
+ nullBodyStatus,
+ networkError,
+ } = window.__bootstrap.fetch;
+
+ const REQUEST_BODY_HEADER_NAMES = [
+ "content-encoding",
+ "content-language",
+ "content-location",
+ "content-type",
+ ];
/**
- * @param {Deno.CreateHttpClientOptions} options
- * @returns {HttpClient}
- */
- function createHttpClient(options) {
- return new HttpClient(core.opSync("op_create_http_client", options));
- }
-
- class HttpClient {
- /**
- * @param {number} rid
- */
- constructor(rid) {
- this.rid = rid;
- }
- close() {
- core.close(this.rid);
- }
- }
-
- /**
- * @param {{ headers: [string,string][], method: string, url: string, baseUrl: string | null, clientRid: number | null, hasBody: boolean }} args
+ * @param {{ method: string, url: string, headers: [string, string][], clientRid: number | null, hasBody: boolean }} args
* @param {Uint8Array | null} body
- * @returns {{requestRid: number, requestBodyRid: number | null}}
+ * @returns {{ requestRid: number, requestBodyRid: number | null }}
*/
function opFetch(args, body) {
- let zeroCopy;
- if (body != null) {
- zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
- }
- return core.opSync("op_fetch", args, zeroCopy);
+ return core.opSync("op_fetch", args, body);
}
/**
- * @param {number} rid
- * @returns {Promise<{status: number, statusText: string, headers: Record<string,string[]>, url: string, responseRid: number}>}
+ * @param {number} rid
+ * @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number }>}
*/
function opFetchSend(rid) {
return core.opAsync("op_fetch_send", rid);
@@ -515,631 +54,254 @@
* @returns {Promise<void>}
*/
function opFetchRequestWrite(rid, body) {
- const zeroCopy = new Uint8Array(
- body.buffer,
- body.byteOffset,
- body.byteLength,
- );
- return core.opAsync("op_fetch_request_write", rid, zeroCopy);
+ return core.opAsync("op_fetch_request_write", rid, body);
}
- const NULL_BODY_STATUS = [101, 204, 205, 304];
- const REDIRECT_STATUS = [301, 302, 303, 307, 308];
-
/**
- * @param {string} s
- * @returns {string}
+ * @param {number} rid
+ * @param {Uint8Array} body
+ * @returns {Promise<number>}
*/
- function byteUpperCase(s) {
- return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) {
- return c.toUpperCase();
- });
+ function opFetchResponseRead(rid, body) {
+ return core.opAsync("op_fetch_response_read", rid, body);
}
/**
- * @param {string} m
- * @returns {boolean}
+ * @param {number} responseBodyRid
+ * @returns {ReadableStream<Uint8Array>}
*/
- function isKnownMethod(m) {
- return (
- m === "DELETE" ||
- m === "GET" ||
- m === "HEAD" ||
- m === "OPTIONS" ||
- m === "POST" ||
- m === "PUT"
- );
+ function createResponseBodyStream(responseBodyRid) {
+ return new ReadableStream({
+ type: "bytes",
+ async pull(controller) {
+ try {
+ // This is the largest possible size for a single packet on a TLS
+ // stream.
+ const chunk = new Uint8Array(16 * 1024 + 256);
+ const read = await opFetchResponseRead(
+ responseBodyRid,
+ chunk,
+ );
+ if (read > 0) {
+ // We read some data. Enqueue it onto the stream.
+ controller.enqueue(chunk.subarray(0, read));
+ } else {
+ // We have reached the end of the body, so we close the stream.
+ controller.close();
+ core.close(responseBodyRid);
+ }
+ } catch (err) {
+ // There was an error while reading a chunk of the body, so we
+ // error.
+ controller.error(err);
+ controller.close();
+ core.close(responseBodyRid);
+ }
+ },
+ cancel() {
+ core.close(responseBodyRid);
+ },
+ });
}
/**
- * @param {string} m
- * @returns {string}
+ * @param {InnerRequest} req
+ * @param {boolean} recursive
+ * @returns {Promise<InnerResponse>}
*/
- function normalizeMethod(m) {
- // Fast path for already valid methods
- if (isKnownMethod(m)) {
- return m;
- }
- // Normalize lower case (slowpath and should be avoided ...)
- const u = byteUpperCase(m);
- if (isKnownMethod(u)) {
- return u;
- }
- // Otherwise passthrough
- return m;
- }
-
- class Request extends Body {
- /** @type {string} */
- #method = "GET";
- /** @type {string} */
- #url = "";
- /** @type {Headers | string[][]} */
- #headers;
- /** @type {"include" | "omit" | "same-origin" | undefined} */
- #credentials = "omit";
-
- /**
- * @param {RequestInfo} input
- * @param {RequestInit} init
- */
- // @ts-expect-error because the use of super in this constructor is valid.
- constructor(input, init) {
- if (arguments.length < 1) {
- throw TypeError("Not enough arguments");
- }
-
- if (!init) {
- init = {};
- }
-
- let b;
-
- // prefer body from init
- if (init.body) {
- b = init.body;
- } else if (input instanceof Request) {
- if (input.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
- b = input[teeBody]();
- } else if (typeof input === "object" && "body" in input && input.body) {
- if (input.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
- b = input.body;
- } else {
- b = "";
- }
-
- let headers;
- let contentType = "";
- // prefer headers from init
- if (init.headers) {
- if (init[lazyHeaders] && Array.isArray(init.headers)) {
- // Trust the headers are valid, and only put them into the `Headers`
- // strucutre when the user accesses the property. We also assume that
- // all passed headers are lower-case (as is the case when they come
- // from hyper in Rust), and that headers are of type
- // `[string, string][]`.
- headers = init.headers;
- for (const tuple of headers) {
- if (tuple[0] === "content-type") {
- contentType = tuple[1];
- }
- }
+ async function mainFetch(req, recursive) {
+ /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
+ let reqBody = null;
+ if (req.body !== null) {
+ if (req.body.streamOrStatic instanceof ReadableStream) {
+ if (req.body.length === null) {
+ reqBody = req.body.stream;
} else {
- headers = new Headers(init.headers);
- contentType = headers.get("content-type") || "";
+ const reader = req.body.stream.getReader();
+ const r1 = await reader.read();
+ if (r1.done) throw new TypeError("Unreachable");
+ reqBody = r1.value;
+ const r2 = await reader.read();
+ if (!r2.done) throw new TypeError("Unreachable");
}
- } else if (input instanceof Request) {
- headers = input.headers;
- contentType = headers.get("content-type") || "";
} else {
- headers = new Headers();
- }
-
- super(b, { contentType });
- this.#headers = headers;
-
- if (input instanceof Request) {
- if (input.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
- // headers are already set above. no reason to do it again
- this.#method = input.method;
- this.#url = input.url;
- this.#credentials = input.credentials;
- } else {
- // Constructing a URL just for validation is known to be expensive.
- // dontValidateUrl allows one to opt out.
- if (init[dontValidateUrl]) {
- this.#url = input;
- } else {
- const baseUrl = getLocationHref();
- this.#url = baseUrl != null
- ? new URL(String(input), baseUrl).href
- : new URL(String(input)).href;
- }
- }
-
- if (init && "method" in init && init.method) {
- this.#method = normalizeMethod(init.method);
- }
-
- if (
- init &&
- "credentials" in init &&
- init.credentials &&
- ["omit", "same-origin", "include"].indexOf(init.credentials) !== -1
- ) {
- this.credentials = init.credentials;
- }
- }
-
- clone() {
- if (this.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
-
- const iterators = this.headers.entries();
- const headersList = [];
- for (const header of iterators) {
- headersList.push(header);
- }
-
- const body = this[teeBody]();
-
- return new Request(this.url, {
- body,
- method: this.method,
- headers: new Headers(headersList),
- credentials: this.credentials,
- });
- }
-
- get method() {
- return this.#method;
- }
-
- set method(_) {
- // can not set method
- }
-
- get url() {
- return this.#url;
- }
-
- set url(_) {
- // can not set url
- }
-
- get headers() {
- if (!(this.#headers instanceof Headers)) {
- this.#headers = new Headers(this.#headers);
- }
- return this.#headers;
- }
-
- set headers(_) {
- // can not set headers
- }
-
- get credentials() {
- return this.#credentials;
- }
-
- set credentials(_) {
- // can not set credentials
- }
- }
-
- const responseData = new WeakMap();
- class Response extends Body {
- /**
- * @param {BodyInit | null} body
- * @param {ResponseInit} [init]
- */
- constructor(body = null, init) {
- init = init ?? {};
-
- if (typeof init !== "object") {
- throw new TypeError(`'init' is not an object`);
- }
-
- const extraInit = responseData.get(init) || {};
- let { type = "default", url = "" } = extraInit;
-
- let status = init.status === undefined ? 200 : Number(init.status || 0);
- let statusText = init.statusText ?? "";
- let headers = init.headers instanceof Headers
- ? init.headers
- : new Headers(init.headers);
-
- if (init.status !== undefined && (status < 200 || status > 599)) {
- throw new RangeError(
- `The status provided (${init.status}) is outside the range [200, 599]`,
- );
- }
-
- // null body status
- if (body && NULL_BODY_STATUS.includes(status)) {
- throw new TypeError("Response with null body status cannot have body");
- }
-
- if (!type) {
- type = "default";
- } else {
- if (type == "error") {
- // spec: https://fetch.spec.whatwg.org/#concept-network-error
- status = 0;
- statusText = "";
- headers = new Headers();
- body = null;
- /* spec for other Response types:
- https://fetch.spec.whatwg.org/#concept-filtered-response-basic
- Please note that type "basic" is not the same thing as "default".*/
- } else if (type == "basic") {
- for (const h of headers) {
- /* Forbidden Response-Header Names:
- https://fetch.spec.whatwg.org/#forbidden-response-header-name */
- if (["set-cookie", "set-cookie2"].includes(h[0].toLowerCase())) {
- headers.delete(h[0]);
- }
+ req.body.streamOrStatic.consumed = true;
+ reqBody = req.body.streamOrStatic.body;
+ }
+ }
+
+ const { requestRid, requestBodyRid } = opFetch({
+ method: req.method,
+ url: req.currentUrl(),
+ headers: req.headerList,
+ clientRid: req.clientRid,
+ hasBody: reqBody !== null,
+ }, reqBody instanceof Uint8Array ? reqBody : null);
+
+ if (requestBodyRid !== null) {
+ if (reqBody === null || !(reqBody instanceof ReadableStream)) {
+ throw new TypeError("Unreachable");
+ }
+ const reader = reqBody.getReader();
+ (async () => {
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ if (!(value instanceof Uint8Array)) {
+ await reader.cancel("value not a Uint8Array");
+ break;
}
- } else if (type == "cors") {
- /* CORS-safelisted Response-Header Names:
- https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name */
- const allowedHeaders = [
- "Cache-Control",
- "Content-Language",
- "Content-Length",
- "Content-Type",
- "Expires",
- "Last-Modified",
- "Pragma",
- ].map((c) => c.toLowerCase());
- for (const h of headers) {
- /* Technically this is still not standards compliant because we are
- supposed to allow headers allowed in the
- 'Access-Control-Expose-Headers' header in the 'internal response'
- However, this implementation of response doesn't seem to have an
- easy way to access the internal response, so we ignore that
- header.
- TODO(serverhiccups): change how internal responses are handled
- so we can do this properly. */
- if (!allowedHeaders.includes(h[0].toLowerCase())) {
- headers.delete(h[0]);
- }
+ try {
+ await opFetchRequestWrite(requestBodyRid, value);
+ } catch (err) {
+ await reader.cancel(err);
+ break;
}
- /* TODO(serverhiccups): Once I fix the 'internal response' thing,
- these actually need to treat the internal response differently */
- } else if (type == "opaque" || type == "opaqueredirect") {
- url = "";
- status = 0;
- statusText = "";
- headers = new Headers();
- body = null;
}
- }
-
- const contentType = headers.get("content-type") || "";
- const size = Number(headers.get("content-length")) || undefined;
-
- super(body, { contentType, size });
-
- this.url = url;
- this.statusText = statusText;
- this.status = extraInit.status || status;
- this.headers = headers;
- this.redirected = extraInit.redirected || false;
- this.type = type;
- }
-
- get ok() {
- return 200 <= this.status && this.status < 300;
+ core.close(requestBodyRid);
+ })();
+ }
+
+ const resp = await opFetchSend(requestRid);
+ /** @type {InnerResponse} */
+ const response = {
+ headerList: resp.headers,
+ status: resp.status,
+ body: null,
+ statusMessage: resp.statusText,
+ type: "basic",
+ url() {
+ if (this.urlList.length == 0) return null;
+ return this.urlList[this.urlList.length - 1];
+ },
+ urlList: req.urlList,
+ };
+ if (redirectStatus(resp.status)) {
+ switch (req.redirectMode) {
+ case "error":
+ core.close(resp.responseRid);
+ return networkError(
+ "Encountered redirect while redirect mode is set to 'error'",
+ );
+ case "follow":
+ core.close(resp.responseRid);
+ return httpRedirectFetch(req, response);
+ case "manual":
+ break;
+ }
+ }
+
+ if (nullBodyStatus(response.status)) {
+ core.close(resp.responseRid);
+ } else {
+ response.body = new InnerBody(createResponseBodyStream(resp.responseRid));
}
- clone() {
- if (this.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
-
- const iterators = this.headers.entries();
- const headersList = [];
- for (const header of iterators) {
- headersList.push(header);
- }
-
- const body = this[teeBody]();
+ if (recursive) return response;
- return new Response(body, {
- status: this.status,
- statusText: this.statusText,
- headers: new Headers(headersList),
- });
+ if (response.urlList.length === 0) {
+ response.urlList = [...req.urlList];
}
- /**
- * @param {string } url
- * @param {number} status
- */
- static redirect(url, status = 302) {
- if (![301, 302, 303, 307, 308].includes(status)) {
- throw new RangeError(
- "The redirection status must be one of 301, 302, 303, 307 and 308.",
- );
- }
- return new Response(null, {
- status,
- statusText: "",
- headers: [["Location", String(url)]],
- });
- }
- }
-
- /** @type {string | null} */
- let baseUrl = null;
-
- /** @param {string} href */
- function setBaseUrl(href) {
- baseUrl = href;
+ return response;
}
/**
- * @param {string} url
- * @param {string} method
- * @param {Headers} headers
- * @param {ReadableStream<Uint8Array> | ArrayBufferView | undefined} body
- * @param {number | null} clientRid
- * @returns {Promise<{status: number, statusText: string, headers: Record<string,string[]>, url: string, responseRid: number}>}
+ * @param {InnerRequest} request
+ * @param {InnerResponse} response
+ * @returns {Promise<InnerResponse>}
*/
- async function sendFetchReq(url, method, headers, body, clientRid) {
- /** @type {[string, string][]} */
- let headerArray = [];
- if (headers) {
- headerArray = Array.from(headers.entries());
+ function httpRedirectFetch(request, response) {
+ const locationHeaders = response.headerList.filter((entry) =>
+ byteLowerCase(entry[0]) === "location"
+ );
+ if (locationHeaders.length === 0) {
+ return response;
}
-
- const { requestRid, requestBodyRid } = opFetch(
- {
- method,
- url,
- baseUrl,
- headers: headerArray,
- clientRid,
- hasBody: !!body,
- },
- body instanceof Uint8Array ? body : null,
+ const locationURL = new URL(
+ locationHeaders[0][1],
+ response.url() ?? undefined,
);
- if (requestBodyRid) {
- if (!(body instanceof ReadableStream)) {
- throw new TypeError("Unreachable state (body is not ReadableStream).");
+ if (locationURL.hash === "") {
+ locationURL.hash = request.currentUrl().hash;
+ }
+ if (locationURL.protocol !== "https:" && locationURL.protocol !== "http:") {
+ return networkError("Can not redirect to a non HTTP(s) url");
+ }
+ if (request.redirectCount === 20) {
+ return networkError("Maximum number of redirects (20) reached");
+ }
+ request.redirectCount++;
+ if (
+ response.status !== 303 && request.body !== null &&
+ request.body.source === null
+ ) {
+ return networkError(
+ "Can not redeliver a streaming request body after a redirect",
+ );
+ }
+ if (
+ ((response.status === 301 || response.status === 302) &&
+ request.method === "POST") ||
+ (response.status === 303 &&
+ (request.method !== "GET" && request.method !== "HEAD"))
+ ) {
+ request.method = "GET";
+ request.body = null;
+ for (let i = 0; i < request.headerList.length; i++) {
+ if (
+ REQUEST_BODY_HEADER_NAMES.includes(
+ byteLowerCase(request.headerList[i][0]),
+ )
+ ) {
+ request.headerList.splice(i, 1);
+ i--;
+ }
}
- const writer = new WritableStream({
- /**
- * @param {Uint8Array} chunk
- * @param {WritableStreamDefaultController} controller
- */
- async write(chunk, controller) {
- try {
- await opFetchRequestWrite(requestBodyRid, chunk);
- } catch (err) {
- controller.error(err);
- }
- },
- close() {
- core.close(requestBodyRid);
- },
- });
- body.pipeTo(writer);
}
-
- return await opFetchSend(requestRid);
+ if (request.body !== null) {
+ const res = extractBody(request.body.source);
+ request.body = res.body;
+ }
+ request.urlList.push(locationURL.href);
+ return mainFetch(request, true);
}
/**
- * @param {Request | URL | string} input
- * @param {RequestInit & {client: Deno.HttpClient}} [init]
- * @returns {Promise<Response>}
+ * @param {RequestInfo} input
+ * @param {RequestInit} init
*/
- async function fetch(input, init) {
- /** @type {string | null} */
- let url;
- let method = null;
- let headers = null;
- let body;
- let clientRid = null;
- let redirected = false;
- let remRedirectCount = 20; // TODO(bartlomieju): use a better way to handle
-
- if (typeof input === "string" || input instanceof URL) {
- url = typeof input === "string" ? input : input.href;
- if (init != null) {
- method = init.method || null;
- if (init.headers) {
- headers = init.headers instanceof Headers
- ? init.headers
- : new Headers(init.headers);
- } else {
- headers = null;
- }
-
- // ref: https://fetch.spec.whatwg.org/#body-mixin
- // Body should have been a mixin
- // but we are treating it as a separate class
- if (init.body) {
- if (!headers) {
- headers = new Headers();
- }
- let contentType = "";
- if (typeof init.body === "string") {
- body = new TextEncoder().encode(init.body);
- contentType = "text/plain;charset=UTF-8";
- } else if (isTypedArray(init.body)) {
- body = init.body;
- } else if (init.body instanceof ArrayBuffer) {
- body = new Uint8Array(init.body);
- } else if (init.body instanceof URLSearchParams) {
- body = new TextEncoder().encode(init.body.toString());
- contentType = "application/x-www-form-urlencoded;charset=UTF-8";
- } else if (init.body instanceof Blob) {
- body = init.body[_byteSequence];
- contentType = init.body.type;
- } else if (init.body instanceof FormData) {
- const res = encodeFormData(init.body);
- body = res.body;
- contentType = res.contentType;
- } else if (init.body instanceof ReadableStream) {
- body = init.body;
- }
- if (contentType && !headers.has("content-type")) {
- headers.set("content-type", contentType);
- }
- }
-
- if (init.client instanceof HttpClient) {
- clientRid = init.client.rid;
- }
- }
- } else {
- url = input.url;
- method = input.method;
- headers = input.headers;
+ async function fetch(input, init = {}) {
+ const prefix = "Failed to call 'fetch'";
+ input = webidl.converters["RequestInfo"](input, {
+ prefix,
+ context: "Argument 1",
+ });
+ init = webidl.converters["RequestInit"](init, {
+ prefix,
+ context: "Argument 2",
+ });
- if (input.body) {
- body = input.body;
- }
+ // 1.
+ const requestObject = new Request(input, init);
+ // 2.
+ const request = toInnerRequest(requestObject);
+ // 10.
+ if (!requestObject.headers.has("Accept")) {
+ request.headerList.push(["Accept", "*/*"]);
}
- let responseBody;
- let responseInit = {};
- while (remRedirectCount) {
- const fetchResp = await sendFetchReq(
- url,
- method ?? "GET",
- headers ?? new Headers(),
- body,
- clientRid,
+ // 12.
+ const response = await mainFetch(request, false);
+ if (response.type === "error") {
+ throw new TypeError(
+ "Fetch failed: " + (response.error ?? "unknown error"),
);
- const rid = fetchResp.responseRid;
-
- if (
- NULL_BODY_STATUS.includes(fetchResp.status) ||
- REDIRECT_STATUS.includes(fetchResp.status)
- ) {
- // We won't use body of received response, so close it now
- // otherwise it will be kept in resource table.
- core.close(rid);
- responseBody = null;
- } else {
- responseBody = new ReadableStream({
- type: "bytes",
- /** @param {ReadableStreamDefaultController<Uint8Array>} controller */
- async pull(controller) {
- try {
- const chunk = new Uint8Array(16 * 1024 + 256);
- const read = await core.opAsync(
- "op_fetch_response_read",
- rid,
- chunk,
- );
- if (read != 0) {
- if (chunk.length == read) {
- controller.enqueue(chunk);
- } else {
- controller.enqueue(chunk.subarray(0, read));
- }
- } else {
- controller.close();
- core.close(rid);
- }
- } catch (e) {
- controller.error(e);
- controller.close();
- core.close(rid);
- }
- },
- cancel() {
- // When reader.cancel() is called
- core.close(rid);
- },
- });
- }
-
- responseInit = {
- status: 200,
- statusText: fetchResp.statusText,
- headers: fetchResp.headers,
- };
-
- responseData.set(responseInit, {
- redirected,
- rid: fetchResp.responseRid,
- status: fetchResp.status,
- url: fetchResp.url,
- });
-
- const response = new Response(responseBody, responseInit);
-
- if (REDIRECT_STATUS.includes(fetchResp.status)) {
- // We're in a redirect status
- switch ((init && init.redirect) || "follow") {
- case "error":
- responseInit = {};
- responseData.set(responseInit, {
- type: "error",
- redirected: false,
- url: "",
- });
- return new Response(null, responseInit);
- case "manual":
- // On the web this would return a `opaqueredirect` response, but
- // those don't make sense server side. See denoland/deno#8351.
- return response;
- case "follow":
- // fallthrough
- default: {
- /** @type {string | null} */
- let redirectUrl = response.headers.get("Location");
- if (redirectUrl == null) {
- return response; // Unspecified
- }
- if (
- !redirectUrl.startsWith("http://") &&
- !redirectUrl.startsWith("https://")
- ) {
- redirectUrl = new URL(redirectUrl, fetchResp.url).href;
- }
- url = redirectUrl;
- redirected = true;
- remRedirectCount--;
- }
- }
- } else {
- return response;
- }
}
- responseData.set(responseInit, {
- type: "error",
- redirected: false,
- url: "",
- });
-
- return new Response(null, responseInit);
+ return fromInnerResponse(response, "immutable");
}
- window.__bootstrap.fetch = {
- FormData,
- setBaseUrl,
- fetch,
- Request,
- Response,
- HttpClient,
- createHttpClient,
- fastBody,
- dontValidateUrl,
- lazyHeaders,
- };
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.fetch = fetch;
})(this);
diff --git a/op_crates/fetch/internal.d.ts b/op_crates/fetch/internal.d.ts
index 3206008c5..86de52761 100644
--- a/op_crates/fetch/internal.d.ts
+++ b/op_crates/fetch/internal.d.ts
@@ -15,22 +15,99 @@ declare namespace globalThis {
DomIterableMixin(base: any, dataSymbol: symbol): any;
};
- declare var headers: {
- Headers: typeof Headers;
- };
+ declare namespace headers {
+ class Headers {
+ }
+ type HeaderList = [string, string][];
+ function headersFromHeaderList(
+ list: HeaderList,
+ guard:
+ | "immutable"
+ | "request"
+ | "request-no-cors"
+ | "response"
+ | "none",
+ ): Headers;
+ function headerListFromHeaders(headers: Headers): HeaderList;
+ function fillHeaders(headers: Headers, object: HeadersInit): void;
+ function getDecodeSplitHeader(
+ list: HeaderList,
+ name: string,
+ ): string[] | null;
+ function guardFromHeaders(
+ headers: Headers,
+ ): "immutable" | "request" | "request-no-cors" | "response" | "none";
+ }
- declare var formData: {
- FormData: typeof FormData;
- encodeFormData(formdata: FormData): {
+ declare namespace formData {
+ declare type FormData = typeof FormData;
+ declare function encodeFormData(formdata: FormData): {
body: Uint8Array;
contentType: string;
};
- parseFormData(body: Uint8Array, boundary: string | undefined): FormData;
- };
+ declare function parseFormData(
+ body: Uint8Array,
+ boundary: string | undefined,
+ ): FormData;
+ declare function formDataFromEntries(entries: FormDataEntry[]): FormData;
+ }
declare var streams: {
ReadableStream: typeof ReadableStream;
isReadableStreamDisturbed(stream: ReadableStream): boolean;
};
+
+ declare namespace fetchBody {
+ function mixinBody(
+ prototype: any,
+ bodySymbol: symbol,
+ mimeTypeSymbol: symbol,
+ ): void;
+ class InnerBody {
+ constructor(stream?: ReadableStream<Uint8Array>);
+ stream: ReadableStream<Uint8Array>;
+ source: null | Uint8Array | Blob | FormData;
+ length: null | number;
+ unusable(): boolean;
+ consume(): Promise<Uint8Array>;
+ clone(): InnerBody;
+ }
+ function extractBody(object: BodyInit): {
+ body: InnerBody;
+ contentType: string | null;
+ };
+ }
+
+ declare namespace fetch {
+ function toInnerRequest(request: Request): InnerRequest;
+ function fromInnerRequest(
+ inner: InnerRequest,
+ guard:
+ | "request"
+ | "immutable"
+ | "request-no-cors"
+ | "response"
+ | "none",
+ ): Request;
+ function redirectStatus(status: number): boolean;
+ function nullBodyStatus(status: number): boolean;
+ function newInnerRequest(
+ method: string,
+ url: any,
+ headerList?: [string, string][],
+ body?: globalThis.__bootstrap.fetchBody.InnerBody,
+ ): InnerResponse;
+ function toInnerResponse(response: Response): InnerResponse;
+ function fromInnerResponse(
+ inner: InnerResponse,
+ guard:
+ | "request"
+ | "immutable"
+ | "request-no-cors"
+ | "response"
+ | "none",
+ ): Response;
+ function networkError(error: string): InnerResponse;
+ }
}
}
diff --git a/op_crates/fetch/lib.rs b/op_crates/fetch/lib.rs
index 030f8a809..41fb153e0 100644
--- a/op_crates/fetch/lib.rs
+++ b/op_crates/fetch/lib.rs
@@ -71,12 +71,28 @@ pub fn init(isolate: &mut JsRuntime) {
include_str!("21_formdata.js"),
),
(
+ "deno:op_crates/fetch/22_body.js",
+ include_str!("22_body.js"),
+ ),
+ (
+ "deno:op_crates/fetch/22_http_client.js",
+ include_str!("22_http_client.js"),
+ ),
+ (
+ "deno:op_crates/fetch/23_request.js",
+ include_str!("23_request.js"),
+ ),
+ (
+ "deno:op_crates/fetch/23_response.js",
+ include_str!("23_response.js"),
+ ),
+ (
"deno:op_crates/fetch/26_fetch.js",
include_str!("26_fetch.js"),
),
];
for (url, source_code) in files {
- isolate.execute(url, source_code).unwrap();
+ isolate.execute(url, source_code).expect(url);
}
}
@@ -110,9 +126,8 @@ pub fn get_declaration() -> PathBuf {
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FetchArgs {
- method: Option<String>,
+ method: String,
url: String,
- base_url: Option<String>,
headers: Vec<(String, String)>,
client_rid: Option<u32>,
has_body: bool,
@@ -144,18 +159,8 @@ where
client.clone()
};
- let method = match args.method {
- Some(method_str) => Method::from_bytes(method_str.as_bytes())?,
- None => Method::GET,
- };
-
- let base_url = match args.base_url {
- Some(base_url) => Some(Url::parse(&base_url)?),
- _ => None,
- };
- let url = Url::options()
- .base_url(base_url.as_ref())
- .parse(&args.url)?;
+ let method = Method::from_bytes(args.method.as_bytes())?;
+ let url = Url::parse(&args.url)?;
// Check scheme before asking for net permission
let scheme = url.scheme();
diff --git a/op_crates/url/00_url.js b/op_crates/url/00_url.js
index 7c24a871a..f51b4aedc 100644
--- a/op_crates/url/00_url.js
+++ b/op_crates/url/00_url.js
@@ -391,8 +391,19 @@
}
}
+ /**
+ * This function implements application/x-www-form-urlencoded parsing.
+ * https://url.spec.whatwg.org/#concept-urlencoded-parser
+ * @param {Uint8Array} bytes
+ * @returns {[string, string][]}
+ */
+ function parseUrlEncoded(bytes) {
+ return core.opSync("op_url_parse_search_params", null, bytes);
+ }
+
window.__bootstrap.url = {
URL,
URLSearchParams,
+ parseUrlEncoded,
};
})(this);
diff --git a/op_crates/url/Cargo.toml b/op_crates/url/Cargo.toml
index ab3ac9e1d..a67b59d7f 100644
--- a/op_crates/url/Cargo.toml
+++ b/op_crates/url/Cargo.toml
@@ -16,6 +16,7 @@ path = "lib.rs"
[dependencies]
deno_core = { version = "0.84.0", path = "../../core" }
idna = "0.2.2"
+percent-encoding = "2.1.0"
serde = { version = "1.0.125", features = ["derive"] }
[dev-dependencies]
diff --git a/op_crates/url/internal.d.ts b/op_crates/url/internal.d.ts
index f852928d3..ec2c2688c 100644
--- a/op_crates/url/internal.d.ts
+++ b/op_crates/url/internal.d.ts
@@ -8,6 +8,7 @@ declare namespace globalThis {
declare var url: {
URL: typeof URL;
URLSearchParams: typeof URLSearchParams;
+ parseUrlEncoded(bytes: Uint8Array): [string, string][];
};
}
}
diff --git a/op_crates/url/lib.rs b/op_crates/url/lib.rs
index f216768c3..04663e411 100644
--- a/op_crates/url/lib.rs
+++ b/op_crates/url/lib.rs
@@ -118,14 +118,21 @@ pub fn op_url_parse(
pub fn op_url_parse_search_params(
_state: &mut deno_core::OpState,
- args: String,
- _zero_copy: Option<ZeroCopyBuf>,
+ args: Option<String>,
+ zero_copy: Option<ZeroCopyBuf>,
) -> Result<Vec<(String, String)>, AnyError> {
- let search_params: Vec<_> = form_urlencoded::parse(args.as_bytes())
- .into_iter()
- .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned()))
- .collect();
- Ok(search_params)
+ let params = match (args, zero_copy) {
+ (None, Some(zero_copy)) => form_urlencoded::parse(&zero_copy)
+ .into_iter()
+ .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned()))
+ .collect(),
+ (Some(args), None) => form_urlencoded::parse(args.as_bytes())
+ .into_iter()
+ .map(|(k, v)| (k.as_ref().to_owned(), v.as_ref().to_owned()))
+ .collect(),
+ _ => return Err(type_error("invalid parameters")),
+ };
+ Ok(params)
}
pub fn op_url_stringify_search_params(
diff --git a/op_crates/web/00_infra.js b/op_crates/web/00_infra.js
index ff9cb7cd4..bc87c8217 100644
--- a/op_crates/web/00_infra.js
+++ b/op_crates/web/00_infra.js
@@ -46,6 +46,15 @@
const HTTP_QUOTED_STRING_TOKEN_POINT_RE = new RegExp(
`^[${regexMatcher(HTTP_QUOTED_STRING_TOKEN_POINT)}]+$`,
);
+ const HTTP_TAB_OR_SPACE_MATCHER = regexMatcher(HTTP_TAB_OR_SPACE);
+ const HTTP_TAB_OR_SPACE_PREFIX_RE = new RegExp(
+ `^[${HTTP_TAB_OR_SPACE_MATCHER}]+`,
+ "g",
+ );
+ const HTTP_TAB_OR_SPACE_SUFFIX_RE = new RegExp(
+ `[${HTTP_TAB_OR_SPACE_MATCHER}]+$`,
+ "g",
+ );
const HTTP_WHITESPACE_MATCHER = regexMatcher(HTTP_WHITESPACE);
const HTTP_WHITESPACE_PREFIX_RE = new RegExp(
`^[${HTTP_WHITESPACE_MATCHER}]+`,
@@ -113,6 +122,62 @@
});
}
+ /**
+ * https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
+ * @param {string} input
+ * @param {number} position
+ * @param {boolean} extractValue
+ * @returns {{result: string, position: number}}
+ */
+ function collectHttpQuotedString(input, position, extractValue) {
+ // 1.
+ const positionStart = position;
+ // 2.
+ let value = "";
+ // 3.
+ if (input[position] !== "\u0022") throw new Error('must be "');
+ // 4.
+ position++;
+ // 5.
+ while (true) {
+ // 5.1.
+ const res = collectSequenceOfCodepoints(
+ input,
+ position,
+ (c) => c !== "\u0022" && c !== "\u005C",
+ );
+ value += res.result;
+ position = res.position;
+ // 5.2.
+ if (position >= input.length) break;
+ // 5.3.
+ const quoteOrBackslash = input[position];
+ // 5.4.
+ position++;
+ // 5.5.
+ if (quoteOrBackslash === "\u005C") {
+ // 5.5.1.
+ if (position >= input.length) {
+ value += "\u005C";
+ break;
+ }
+ // 5.5.2.
+ value += input[position];
+ // 5.5.3.
+ position++;
+ } else { // 5.6.
+ // 5.6.1
+ if (quoteOrBackslash !== "\u0022") throw new Error('must be "');
+ // 5.6.2
+ break;
+ }
+ }
+ // 6.
+ if (extractValue) return { result: value, position };
+ // 7.
+ return { result: input.substring(positionStart, position + 1), position };
+ }
+
window.__bootstrap.infra = {
collectSequenceOfCodepoints,
ASCII_DIGIT,
@@ -126,10 +191,13 @@
HTTP_TOKEN_CODE_POINT_RE,
HTTP_QUOTED_STRING_TOKEN_POINT,
HTTP_QUOTED_STRING_TOKEN_POINT_RE,
+ HTTP_TAB_OR_SPACE_PREFIX_RE,
+ HTTP_TAB_OR_SPACE_SUFFIX_RE,
HTTP_WHITESPACE_PREFIX_RE,
HTTP_WHITESPACE_SUFFIX_RE,
regexMatcher,
byteUpperCase,
byteLowerCase,
+ collectHttpQuotedString,
};
})(globalThis);
diff --git a/op_crates/web/01_mimesniff.js b/op_crates/web/01_mimesniff.js
index 534e39c31..077a40df1 100644
--- a/op_crates/web/01_mimesniff.js
+++ b/op_crates/web/01_mimesniff.js
@@ -15,65 +15,10 @@
HTTP_WHITESPACE_SUFFIX_RE,
HTTP_QUOTED_STRING_TOKEN_POINT_RE,
HTTP_TOKEN_CODE_POINT_RE,
+ collectHttpQuotedString,
} = window.__bootstrap.infra;
/**
- * https://fetch.spec.whatwg.org/#collect-an-http-quoted-string
- * @param {string} input
- * @param {number} position
- * @param {boolean} extractValue
- * @returns {{result: string, position: number}}
- */
- function collectHttpQuotedString(input, position, extractValue) {
- // 1.
- const positionStart = position;
- // 2.
- let value = "";
- // 3.
- if (input[position] !== "\u0022") throw new Error('must be "');
- // 4.
- position++;
- // 5.
- while (true) {
- // 5.1.
- const res = collectSequenceOfCodepoints(
- input,
- position,
- (c) => c !== "\u0022" && c !== "\u005C",
- );
- value += res.result;
- position = res.position;
- // 5.2.
- if (position >= input.length) break;
- // 5.3.
- const quoteOrBackslash = input[position];
- // 5.4.
- position++;
- // 5.5.
- if (quoteOrBackslash === "\u005C") {
- // 5.5.1.
- if (position >= input.length) {
- value += "\u005C";
- break;
- }
- // 5.5.2.
- value += input[position];
- // 5.5.3.
- position++;
- } else { // 5.6.
- // 5.6.1
- if (input[position] !== "\u0022") throw new Error('must be "');
- // 5.6.2
- break;
- }
- }
- // 6.
- if (extractValue) return { result: value, position };
- // 7.
- return { result: input.substring(positionStart, position + 1), position };
- }
-
- /**
* @typedef MimeType
* @property {string} type
* @property {string} subtype
@@ -172,7 +117,7 @@
let parameterValue = null;
// 11.8.
- if (input[position] == "\u0022") {
+ if (input[position] === "\u0022") {
// 11.8.1.
const res = collectHttpQuotedString(input, position, true);
parameterValue = res.result;
@@ -214,5 +159,32 @@
return mimeType;
}
- window.__bootstrap.mimesniff = { parseMimeType };
+ /**
+ * @param {MimeType} mimeType
+ * @returns {string}
+ */
+ function essence(mimeType) {
+ return `${mimeType.type}/${mimeType.subtype}`;
+ }
+
+ /**
+ * @param {MimeType} mimeType
+ * @returns {string}
+ */
+ function serializeMimeType(mimeType) {
+ let serialization = essence(mimeType);
+ for (const param of mimeType.parameters) {
+ serialization += `;${param[0]}=`;
+ let value = param[1];
+ if (!HTTP_TOKEN_CODE_POINT_RE.test(value)) {
+ value = value.replaceAll("\\", "\\\\");
+ value = value.replaceAll('"', '\\"');
+ value = `"${value}"`;
+ }
+ serialization += value;
+ }
+ return serialization;
+ }
+
+ window.__bootstrap.mimesniff = { parseMimeType, essence, serializeMimeType };
})(this);
diff --git a/op_crates/web/03_abort_signal.js b/op_crates/web/03_abort_signal.js
index 693b5342a..b87a56ce3 100644
--- a/op_crates/web/03_abort_signal.js
+++ b/op_crates/web/03_abort_signal.js
@@ -2,6 +2,7 @@
"use strict";
((window) => {
+ const webidl = window.__bootstrap.webidl;
const { setIsTrusted } = window.__bootstrap.event;
const add = Symbol("add");
@@ -47,6 +48,7 @@
throw new TypeError("Illegal constructor.");
}
super();
+ this[webidl.brand] = webidl.brand;
}
get aborted() {
@@ -111,6 +113,11 @@
});
}
+ webidl.converters["AbortSignal"] = webidl.createInterfaceConverter(
+ "AbortSignal",
+ AbortSignal,
+ );
+
window.AbortSignal = AbortSignal;
window.AbortController = AbortController;
window.__bootstrap = window.__bootstrap || {};
diff --git a/op_crates/web/internal.d.ts b/op_crates/web/internal.d.ts
index a5b653218..bfce3e1e1 100644
--- a/op_crates/web/internal.d.ts
+++ b/op_crates/web/internal.d.ts
@@ -28,11 +28,21 @@ declare namespace globalThis {
HTTP_TOKEN_CODE_POINT_RE: RegExp;
HTTP_QUOTED_STRING_TOKEN_POINT: string[];
HTTP_QUOTED_STRING_TOKEN_POINT_RE: RegExp;
+ HTTP_TAB_OR_SPACE_PREFIX_RE: RegExp;
+ HTTP_TAB_OR_SPACE_SUFFIX_RE: RegExp;
HTTP_WHITESPACE_PREFIX_RE: RegExp;
HTTP_WHITESPACE_SUFFIX_RE: RegExp;
regexMatcher(chars: string[]): string;
byteUpperCase(s: string): string;
byteLowerCase(s: string): string;
+ collectHttpQuotedString(
+ input: string,
+ position: number,
+ extractValue: boolean,
+ ): {
+ result: string;
+ position: number;
+ };
};
declare namespace mimesniff {
@@ -42,6 +52,8 @@ declare namespace globalThis {
parameters: Map<string, string>;
}
declare function parseMimeType(input: string): MimeType | null;
+ declare function essence(mimeType: MimeType): string;
+ declare function serializeMimeType(mimeType: MimeType): string;
}
declare var eventTarget: {
diff --git a/op_crates/webgpu/02_idl_types.js b/op_crates/webgpu/02_idl_types.js
index bcc323893..f990a40e8 100644
--- a/op_crates/webgpu/02_idl_types.js
+++ b/op_crates/webgpu/02_idl_types.js
@@ -135,7 +135,9 @@
converter: webidl.createSequenceConverter(
webidl.converters["GPUFeatureName"],
),
- defaultValue: [],
+ get defaultValue() {
+ return [];
+ },
},
{
key: "nonGuaranteedLimits",
@@ -143,7 +145,9 @@
webidl.converters["DOMString"],
webidl.converters["GPUSize32"],
),
- defaultValue: {},
+ get defaultValue() {
+ return {};
+ },
},
];
webidl.converters["GPUDeviceDescriptor"] = webidl.createDictionaryConverter(
@@ -1046,7 +1050,9 @@
webidl.converters["GPUVertexBufferLayout"],
),
),
- defaultValue: [],
+ get defaultValue() {
+ return [];
+ },
},
];
webidl.converters["GPUVertexState"] = webidl.createDictionaryConverter(
@@ -1187,12 +1193,16 @@
{
key: "stencilFront",
converter: webidl.converters["GPUStencilFaceState"],
- defaultValue: {},
+ get defaultValue() {
+ return {};
+ },
},
{
key: "stencilBack",
converter: webidl.converters["GPUStencilFaceState"],
- defaultValue: {},
+ get defaultValue() {
+ return {};
+ },
},
{
key: "stencilReadMask",
@@ -1379,7 +1389,9 @@
{
key: "primitive",
converter: webidl.converters["GPUPrimitiveState"],
- defaultValue: {},
+ get defaultValue() {
+ return {};
+ },
},
{
key: "depthStencil",
@@ -1388,7 +1400,9 @@
{
key: "multisample",
converter: webidl.converters["GPUMultisampleState"],
- defaultValue: {},
+ get defaultValue() {
+ return {};
+ },
},
{ key: "fragment", converter: webidl.converters["GPUFragmentState"] },
];
@@ -1530,7 +1544,9 @@
{
key: "origin",
converter: webidl.converters["GPUOrigin3D"],
- defaultValue: {},
+ get defaultValue() {
+ return {};
+ },
},
{
key: "aspect",
@@ -1793,7 +1809,9 @@
converter: webidl.createSequenceConverter(
webidl.converters["GPUPipelineStatisticName"],
),
- defaultValue: [],
+ get defaultValue() {
+ return [];
+ },
},
];
webidl.converters["GPUQuerySetDescriptor"] = webidl.createDictionaryConverter(
diff --git a/op_crates/webidl/00_webidl.js b/op_crates/webidl/00_webidl.js
index 63946c9a1..6bf98be06 100644
--- a/op_crates/webidl/00_webidl.js
+++ b/op_crates/webidl/00_webidl.js
@@ -375,40 +375,12 @@
return V;
}
- const abByteLengthGetter = Object.getOwnPropertyDescriptor(
- ArrayBuffer.prototype,
- "byteLength",
- ).get;
-
function isNonSharedArrayBuffer(V) {
- try {
- // This will throw on SharedArrayBuffers, but not detached ArrayBuffers.
- // (The spec says it should throw, but the spec conflicts with implementations: https://github.com/tc39/ecma262/issues/678)
- abByteLengthGetter.call(V);
-
- return true;
- } catch {
- return false;
- }
+ return V instanceof ArrayBuffer;
}
- let sabByteLengthGetter;
-
function isSharedArrayBuffer(V) {
- // TODO(lucacasonato): vulnerable to prototype pollution. Needs to happen
- // here because SharedArrayBuffer is not available during snapshotting.
- if (!sabByteLengthGetter) {
- sabByteLengthGetter = Object.getOwnPropertyDescriptor(
- SharedArrayBuffer.prototype,
- "byteLength",
- ).get;
- }
- try {
- sabByteLengthGetter.call(V);
- return true;
- } catch {
- return false;
- }
+ return V instanceof SharedArrayBuffer;
}
function isArrayBufferDetached(V) {
@@ -439,14 +411,8 @@
return V;
};
- const dvByteLengthGetter = Object.getOwnPropertyDescriptor(
- DataView.prototype,
- "byteLength",
- ).get;
converters.DataView = (V, opts = {}) => {
- try {
- dvByteLengthGetter.call(V);
- } catch (e) {
+ if (!(V instanceof DataView)) {
throw makeException(TypeError, "is not a DataView", opts);
}
@@ -614,10 +580,19 @@
}
}
+ function isEmptyObject(V) {
+ for (const _ in V) return false;
+ return true;
+ }
+
function createDictionaryConverter(name, ...dictionaries) {
+ let hasRequiredKey = false;
const allMembers = [];
for (const members of dictionaries) {
for (const member of members) {
+ if (member.required) {
+ hasRequiredKey = true;
+ }
allMembers.push(member);
}
}
@@ -628,6 +603,29 @@
return a.key < b.key ? -1 : 1;
});
+ const defaultValues = {};
+ for (const member of allMembers) {
+ if ("defaultValue" in member) {
+ const idlMemberValue = member.defaultValue;
+ const imvType = typeof idlMemberValue;
+ // Copy by value types can be directly assigned, copy by reference types
+ // need to be re-created for each allocation.
+ if (
+ imvType === "number" || imvType === "boolean" ||
+ imvType === "string" || imvType === "bigint" ||
+ imvType === "undefined"
+ ) {
+ defaultValues[member.key] = idlMemberValue;
+ } else {
+ Object.defineProperty(defaultValues, member.key, {
+ get() {
+ return member.defaultValue;
+ },
+ });
+ }
+ }
+ }
+
return function (V, opts = {}) {
const typeV = type(V);
switch (typeV) {
@@ -644,7 +642,14 @@
}
const esDict = V;
- const idlDict = {};
+ const idlDict = { ...defaultValues };
+
+ // NOTE: fast path Null and Undefined and empty objects.
+ if (
+ (V === undefined || V === null || isEmptyObject(V)) && !hasRequiredKey
+ ) {
+ return idlDict;
+ }
for (const member of allMembers) {
const key = member.key;
@@ -656,20 +661,12 @@
esMemberValue = esDict[key];
}
- const context = `'${key}' of '${name}'${
- opts.context ? ` (${opts.context})` : ""
- }`;
-
if (esMemberValue !== undefined) {
+ const context = `'${key}' of '${name}'${
+ opts.context ? ` (${opts.context})` : ""
+ }`;
const converter = member.converter;
- const idlMemberValue = converter(esMemberValue, {
- ...opts,
- context,
- });
- idlDict[key] = idlMemberValue;
- } else if ("defaultValue" in member) {
- const defaultValue = member.defaultValue;
- const idlMemberValue = defaultValue;
+ const idlMemberValue = converter(esMemberValue, { ...opts, context });
idlDict[key] = idlMemberValue;
} else if (member.required) {
throw makeException(
diff --git a/runtime/js/40_http.js b/runtime/js/40_http.js
index 45361ee34..67faf19cb 100644
--- a/runtime/js/40_http.js
+++ b/runtime/js/40_http.js
@@ -2,9 +2,9 @@
"use strict";
((window) => {
- const { Request, dontValidateUrl, lazyHeaders, fastBody, Response } =
+ const { InnerBody } = window.__bootstrap.fetchBody;
+ const { Response, fromInnerRequest, toInnerResponse, newInnerRequest } =
window.__bootstrap.fetch;
- const { Headers } = window.__bootstrap.headers;
const errors = window.__bootstrap.errors.errors;
const core = window.Deno.core;
const { ReadableStream } = window.__bootstrap.streams;
@@ -53,18 +53,18 @@
] = nextRequest;
/** @type {ReadableStream<Uint8Array> | undefined} */
- let body = undefined;
+ let body = null;
if (typeof requestBodyRid === "number") {
body = createRequestBodyStream(requestBodyRid);
}
- const request = new Request(url, {
- body,
+ const innerRequest = newInnerRequest(
method,
- headers: headersList,
- [dontValidateUrl]: true,
- [lazyHeaders]: true,
- });
+ url,
+ headersList,
+ body !== null ? new InnerBody(body) : null,
+ );
+ const request = fromInnerRequest(innerRequest, "immutable");
const respondWith = createRespondWith(responseSenderRid, this.#rid);
@@ -96,16 +96,6 @@
);
}
- /** IMPORTANT: Equivalent to `Array.from(headers).flat()` but more performant.
- * Please preserve. */
- function flattenHeaders(headers) {
- const array = [];
- for (const pair of headers) {
- array.push(pair[0], pair[1]);
- }
- return array;
- }
-
function createRespondWith(responseSenderRid) {
return async function respondWith(resp) {
if (resp instanceof Promise) {
@@ -117,46 +107,66 @@
"First argument to respondWith must be a Response or a promise resolving to a Response.",
);
}
- // If response body is Uint8Array it will be sent synchronously
- // in a single op, in other case a "response body" resource will be
- // created and we'll be streaming it.
- const body = resp[fastBody]();
- let zeroCopyBuf;
- if (body instanceof ArrayBuffer) {
- zeroCopyBuf = new Uint8Array(body);
- } else if (!body) {
- zeroCopyBuf = new Uint8Array(0);
+
+ const innerResp = toInnerResponse(resp);
+
+ // If response body length is known, it will be sent synchronously in a
+ // single op, in other case a "response body" resource will be created and
+ // we'll be streaming it.
+ /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
+ let respBody = null;
+ if (innerResp.body !== null) {
+ if (innerResp.body.unusable()) throw new TypeError("Body is unusable.");
+ if (innerResp.body.streamOrStatic instanceof ReadableStream) {
+ if (innerResp.body.length === null) {
+ respBody = innerResp.body.stream;
+ } else {
+ const reader = innerResp.body.stream.getReader();
+ const r1 = await reader.read();
+ if (r1.done) throw new TypeError("Unreachable");
+ respBody = r1.value;
+ const r2 = await reader.read();
+ if (!r2.done) throw new TypeError("Unreachable");
+ }
+ } else {
+ innerResp.body.streamOrStatic.consumed = true;
+ respBody = innerResp.body.streamOrStatic.body;
+ }
} else {
- zeroCopyBuf = null;
+ respBody = new Uint8Array(0);
}
const responseBodyRid = await Deno.core.opAsync("op_http_response", [
responseSenderRid,
- resp.status ?? 200,
- flattenHeaders(resp.headers),
- ], zeroCopyBuf);
+ innerResp.status ?? 200,
+ innerResp.headerList,
+ ], respBody instanceof Uint8Array ? respBody : null);
// If `respond` returns a responseBodyRid, we should stream the body
// to that resource.
- if (typeof responseBodyRid === "number") {
- if (!body || !(body instanceof ReadableStream)) {
- throw new Error(
- "internal error: recieved responseBodyRid, but response has no body or is not a stream",
- );
+ if (responseBodyRid !== null) {
+ if (respBody === null || !(respBody instanceof ReadableStream)) {
+ throw new TypeError("Unreachable");
}
- for await (const chunk of body) {
- const data = new Uint8Array(
- chunk.buffer,
- chunk.byteOffset,
- chunk.byteLength,
- );
- await Deno.core.opAsync(
- "op_http_response_write",
- responseBodyRid,
- data,
- );
+ const reader = respBody.getReader();
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ if (!(value instanceof Uint8Array)) {
+ await reader.cancel("value not a Uint8Array");
+ break;
+ }
+ try {
+ await Deno.core.opAsync(
+ "op_http_response_write",
+ responseBodyRid,
+ value,
+ );
+ } catch (err) {
+ await reader.cancel(err);
+ break;
+ }
}
-
// Once all chunks are sent, and the request body is closed, we can close
// the response body.
await Deno.core.opAsync("op_http_response_close", responseBodyRid);
diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js
index dd7e3793c..7742e2ba2 100644
--- a/runtime/js/99_main.js
+++ b/runtime/js/99_main.js
@@ -421,7 +421,6 @@ delete Object.prototype.__proto__;
if (locationHref != null) {
location.setLocationHref(locationHref);
- fetch.setBaseUrl(locationHref);
}
registerErrors();
@@ -488,7 +487,6 @@ delete Object.prototype.__proto__;
runtimeOptions;
location.setLocationHref(locationHref);
- fetch.setBaseUrl(locationHref);
registerErrors();
const internalSymbol = Symbol("Deno.internal");
diff --git a/runtime/ops/http.rs b/runtime/ops/http.rs
index bdef14594..4d9787cde 100644
--- a/runtime/ops/http.rs
+++ b/runtime/ops/http.rs
@@ -331,7 +331,7 @@ struct RespondArgs(
// status:
u16,
// headers:
- Vec<String>,
+ Vec<(String, String)>,
);
async fn op_http_response(
@@ -358,11 +358,9 @@ async fn op_http_response(
let mut builder = Response::builder().status(status);
- debug_assert_eq!(headers.len() % 2, 0);
- let headers_count = headers.len() / 2;
- builder.headers_mut().unwrap().reserve(headers_count);
- for i in 0..headers_count {
- builder = builder.header(&headers[2 * i], &headers[2 * i + 1]);
+ builder.headers_mut().unwrap().reserve(headers.len());
+ for (key, value) in &headers {
+ builder = builder.header(key, value);
}
let res;
diff --git a/test_util/wpt b/test_util/wpt
-Subproject 579608584916d582d38d0159666aae9a6aaf07a
+Subproject 5d9a0686bd51cc20df785fc013700c7b18fc0e0
diff --git a/tools/wpt/expectation.json b/tools/wpt/expectation.json
index 5291b95f2..bcb12ce93 100644
--- a/tools/wpt/expectation.json
+++ b/tools/wpt/expectation.json
@@ -627,28 +627,7 @@
"Setting pathname with trailing U+001F (wpt++:)"
],
"url-tojson.any.js": true,
- "urlencoded-parser.any.js": [
- "request.formData() with input: test=",
- "response.formData() with input: test=",
- "request.formData() with input: †&†=x",
- "response.formData() with input: †&†=x",
- "request.formData() with input: _charset_=windows-1252&test=%C2x",
- "response.formData() with input: _charset_=windows-1252&test=%C2x",
- "request.formData() with input: %=a",
- "response.formData() with input: %=a",
- "request.formData() with input: %a=a",
- "response.formData() with input: %a=a",
- "request.formData() with input: %a_=a",
- "response.formData() with input: %a_=a",
- "request.formData() with input: id=0&value=%",
- "response.formData() with input: id=0&value=%",
- "request.formData() with input: b=%2sf%2a",
- "response.formData() with input: b=%2sf%2a",
- "request.formData() with input: b=%2%2af%2a",
- "response.formData() with input: b=%2%2af%2a",
- "request.formData() with input: b=%%2a",
- "response.formData() with input: b=%%2a"
- ],
+ "urlencoded-parser.any.js": true,
"urlsearchparams-append.any.js": true,
"urlsearchparams-constructor.any.js": [
"Construct with 2 unpaired surrogates (no trailing)",
@@ -672,18 +651,16 @@
"fetch": {
"api": {
"request": {
- "request-structure.any.js": [
- "Check destination attribute",
- "Check referrer attribute",
- "Check referrerPolicy attribute",
- "Check mode attribute",
- "Check credentials attribute",
- "Check cache attribute",
- "Check redirect attribute",
- "Check integrity attribute",
- "Check isReloadNavigation attribute",
- "Check isHistoryNavigation attribute"
- ]
+ "request-init-002.any.js": true,
+ "request-init-stream.any.js": [
+ "Constructing a Request with a Request on which body.getReader() is called",
+ "Constructing a Request with a Request on which body.getReader().read() is called",
+ "Constructing a Request with a Request on which read() and releaseLock() are called"
+ ],
+ "request-consume-empty.any.js": [
+ "Consume empty FormData request body as text"
+ ],
+ "request-consume.any.js": true
},
"headers": {
"headers-basic.any.js": true,
@@ -693,12 +670,143 @@
"headers-normalize.any.js": true,
"headers-record.any.js": true,
"headers-structure.any.js": true
+ },
+ "basic": {
+ "request-head.any.js": true,
+ "request-headers-case.any.js": false,
+ "request-headers-nonascii.any.js": false,
+ "request-headers.any.js": [
+ "Fetch with PUT without body",
+ "Fetch with PUT with body",
+ "Fetch with POST without body",
+ "Fetch with POST with text body",
+ "Fetch with POST with FormData body",
+ "Fetch with POST with URLSearchParams body",
+ "Fetch with POST with Blob body",
+ "Fetch with POST with ArrayBuffer body",
+ "Fetch with POST with Uint8Array body",
+ "Fetch with POST with Int8Array body",
+ "Fetch with POST with Float32Array body",
+ "Fetch with POST with Float64Array body",
+ "Fetch with POST with DataView body",
+ "Fetch with POST with Blob body with mime type",
+ "Fetch with Chicken",
+ "Fetch with Chicken with body",
+ "Fetch with POST and mode \"same-origin\" needs an Origin header",
+ "Fetch with POST and mode \"no-cors\" needs an Origin header",
+ "Fetch with PUT and mode \"same-origin\" needs an Origin header",
+ "Fetch with TacO and mode \"same-origin\" needs an Origin header",
+ "Fetch with TacO and mode \"cors\" needs an Origin header"
+ ],
+ "text-utf8.any.js": true,
+ "accept-header.any.js": [
+ "Request through fetch should have a 'accept-language' header"
+ ],
+ "conditional-get.any.js": false,
+ "error-after-response.any.js": false,
+ "header-value-combining.any.js": false,
+ "header-value-null-byte.any.js": true,
+ "historical.any.js": true,
+ "http-response-code.any.js": true,
+ "integrity.sub.any.js": [
+ "Invalid integrity",
+ "Multiple integrities: invalid stronger than valid",
+ "Multiple integrities: both are invalid",
+ "CORS invalid integrity",
+ "Empty string integrity for opaque response"
+ ],
+ "request-upload.any.js": [
+ "Fetch with POST with ReadableStream",
+ "Fetch with POST with ReadableStream containing String",
+ "Fetch with POST with ReadableStream containing null",
+ "Fetch with POST with ReadableStream containing number",
+ "Fetch with POST with ReadableStream containing ArrayBuffer",
+ "Fetch with POST with ReadableStream containing Blob",
+ "Fetch with POST with text body on 421 response should be retried once on new connection."
+ ],
+ "response-url.sub.any.js": true,
+ "scheme-about.any.js": true,
+ "scheme-blob.sub.any.js": true,
+ "scheme-data.any.js": false,
+ "scheme-others.sub.any.js": true,
+ "stream-response.any.js": true,
+ "stream-safe-creation.any.js": false
+ },
+ "response": {
+ "json.any.js": true,
+ "response-init-001.any.js": true,
+ "response-init-002.any.js": true,
+ "response-static-error.any.js": true,
+ "response-static-redirect.any.js": true,
+ "response-stream-disturbed-1.any.js": true,
+ "response-stream-disturbed-2.any.js": true,
+ "response-stream-disturbed-3.any.js": true,
+ "response-stream-disturbed-4.any.js": true,
+ "response-stream-disturbed-5.any.js": true,
+ "response-stream-disturbed-6.any.js": true,
+ "response-stream-disturbed-by-pipe.any.js": true,
+ "response-stream-with-broken-then.any.js": [
+ "Attempt to inject {done: false, value: bye} via Object.prototype.then.",
+ "Attempt to inject value: undefined via Object.prototype.then.",
+ "Attempt to inject undefined via Object.prototype.then.",
+ "Attempt to inject 8.2 via Object.prototype.then.",
+ "intercepting arraybuffer to text conversion via Object.prototype.then should not be possible"
+ ],
+ "response-error-from-stream.any.js": true,
+ "response-error.any.js": true,
+ "response-from-stream.any.js": true,
+ "response-cancel-stream.any.js": true,
+ "response-clone.any.js": [
+ "Check response clone use structureClone for teed ReadableStreams (Int8Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Int16Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Int32Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (ArrayBufferchunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Uint8Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Uint8ClampedArraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Uint16Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Uint32Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Float32Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (Float64Arraychunk)",
+ "Check response clone use structureClone for teed ReadableStreams (DataViewchunk)"
+ ],
+ "response-consume-empty.any.js": [
+ "Consume empty FormData response body as text"
+ ],
+ "response-consume-stream.any.js": true
+ },
+ "body": {
+ "mime-type.any.js": true
+ },
+ "redirect": {
+ "redirect-count.any.js": true,
+ "redirect-empty-location.any.js": [
+ "redirect response with empty Location, manual mode"
+ ],
+ "redirect-location.any.js": [
+ "Redirect 301 in \"manual\" mode without location",
+ "Redirect 301 in \"manual\" mode with invalid location",
+ "Redirect 301 in \"manual\" mode with data location",
+ "Redirect 302 in \"manual\" mode without location",
+ "Redirect 302 in \"manual\" mode with invalid location",
+ "Redirect 302 in \"manual\" mode with data location",
+ "Redirect 303 in \"manual\" mode without location",
+ "Redirect 303 in \"manual\" mode with invalid location",
+ "Redirect 303 in \"manual\" mode with data location",
+ "Redirect 307 in \"manual\" mode without location",
+ "Redirect 307 in \"manual\" mode with invalid location",
+ "Redirect 307 in \"manual\" mode with data location",
+ "Redirect 308 in \"manual\" mode without location",
+ "Redirect 308 in \"manual\" mode with invalid location",
+ "Redirect 308 in \"manual\" mode with data location"
+ ],
+ "redirect-method.any.js": true,
+ "redirect-schemes.any.js": true,
+ "redirect-to-dataurl.any.js": true
}
},
"data-urls": {
"base64.any.js": true,
"processing.any.js": [
- "\"data://test:test/,X\"",
"\"data:text/plain;a=\\\",\\\",X\""
]
}
diff --git a/tools/wpt/runner.ts b/tools/wpt/runner.ts
index 0ea14c5b7..972519d27 100644
--- a/tools/wpt/runner.ts
+++ b/tools/wpt/runner.ts
@@ -100,6 +100,7 @@ export async function runSingleTest(
reporter(result);
} else {
stderr += line + "\n";
+ console.error(stderr);
}
}