chore: align fetch to spec (#10203)

This commit aligns the `fetch` API and the `Request` / `Response`
classes belonging to it to the spec. This commit enables all the
relevant `fetch` WPT tests. Spec compliance is now at around 90%.

Performance is essentially identical now (within 1% of 1.9.0).
This commit is contained in:
Luca Casonato 2021-04-20 14:47:22 +02:00 committed by GitHub
parent 115197ffb0
commit 9e6cd91014
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2235 additions and 1384 deletions

1
Cargo.lock generated
View File

@ -737,6 +737,7 @@ dependencies = [
"bench_util",
"deno_core",
"idna",
"percent-encoding",
"serde",
]

View File

@ -1,2 +1,2 @@
[WILDCARD]error: Uncaught URIError: relative URL without a base
[WILDCARD]error: Uncaught TypeError: Invalid URL
[WILDCARD]

View File

@ -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;
}

View File

@ -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",

View File

@ -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();

View File

@ -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);

View File

@ -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);

338
op_crates/fetch/22_body.js Normal file
View File

@ -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);

View File

@ -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);

View File

@ -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);

View File

@ -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);

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
}
}

View File

@ -70,13 +70,29 @@ pub fn init(isolate: &mut JsRuntime) {
"deno:op_crates/fetch/21_formdata.js",
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();

View File

@ -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);

View File

@ -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]

View File

@ -8,6 +8,7 @@ declare namespace globalThis {
declare var url: {
URL: typeof URL;
URLSearchParams: typeof URLSearchParams;
parseUrlEncoded(bytes: Uint8Array): [string, string][];
};
}
}

View File

@ -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(

View File

@ -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);

View File

@ -15,64 +15,9 @@
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
@ -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);

View File

@ -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 || {};

View File

@ -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: {

View File

@ -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(

View File

@ -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(

View File

@ -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);

View File

@ -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");

View File

@ -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;

@ -1 +1 @@
Subproject commit 579608584916d582d38d0159666aae9a6aaf07ad
Subproject commit 5d9a0686bd51cc20df785fc013700c7b18fc0e0b

View File

@ -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\""
]
}

View File

@ -100,6 +100,7 @@ export async function runSingleTest(
reporter(result);
} else {
stderr += line + "\n";
console.error(stderr);
}
}