mirror of
https://github.com/denoland/deno.git
synced 2024-12-25 08:39:09 -05:00
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:
parent
115197ffb0
commit
9e6cd91014
30 changed files with 2235 additions and 1384 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -737,6 +737,7 @@ dependencies = [
|
|||
"bench_util",
|
||||
"deno_core",
|
||||
"idna",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
[WILDCARD]error: Uncaught URIError: relative URL without a base
|
||||
[WILDCARD]error: Uncaught TypeError: Invalid URL
|
||||
[WILDCARD]
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
338
op_crates/fetch/22_body.js
Normal 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);
|
41
op_crates/fetch/22_http_client.js
Normal file
41
op_crates/fetch/22_http_client.js
Normal 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);
|
521
op_crates/fetch/23_request.js
Normal file
521
op_crates/fetch/23_request.js
Normal 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);
|
415
op_crates/fetch/23_response.js
Normal file
415
op_crates/fetch/23_response.js
Normal 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
93
op_crates/fetch/internal.d.ts
vendored
93
op_crates/fetch/internal.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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]
|
||||
|
|
1
op_crates/url/internal.d.ts
vendored
1
op_crates/url/internal.d.ts
vendored
|
@ -8,6 +8,7 @@ declare namespace globalThis {
|
|||
declare var url: {
|
||||
URL: typeof URL;
|
||||
URLSearchParams: typeof URLSearchParams;
|
||||
parseUrlEncoded(bytes: Uint8Array): [string, string][];
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 || {};
|
||||
|
|
12
op_crates/web/internal.d.ts
vendored
12
op_crates/web/internal.d.ts
vendored
|
@ -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: {
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
|
@ -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\""
|
||||
]
|
||||
}
|
||||
|
|
|
@ -100,6 +100,7 @@ export async function runSingleTest(
|
|||
reporter(result);
|
||||
} else {
|
||||
stderr += line + "\n";
|
||||
console.error(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue