summaryrefslogtreecommitdiff
path: root/op_crates/fetch/26_fetch.js
diff options
context:
space:
mode:
authorLuca Casonato <lucacasonato@yahoo.com>2021-04-20 14:47:22 +0200
committerGitHub <noreply@github.com>2021-04-20 14:47:22 +0200
commit9e6cd91014ac4a0d34556b0d09cbe25e4e0930c6 (patch)
tree4523790510a17676c987039feb03f208a258dc16 /op_crates/fetch/26_fetch.js
parent115197ffb06aad2a3045e8478980ab911b5a5eeb (diff)
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).
Diffstat (limited to 'op_crates/fetch/26_fetch.js')
-rw-r--r--op_crates/fetch/26_fetch.js1296
1 files changed, 229 insertions, 1067 deletions
diff --git a/op_crates/fetch/26_fetch.js b/op_crates/fetch/26_fetch.js
index d07121e86..a6d1608d0 100644
--- a/op_crates/fetch/26_fetch.js
+++ b/op_crates/fetch/26_fetch.js
@@ -13,497 +13,36 @@
((window) => {
const core = window.Deno.core;
-
- // provided by "deno_web"
- const { URLSearchParams } = window.__bootstrap.url;
- const { getLocationHref } = window.__bootstrap.location;
- const { FormData, parseFormData, encodeFormData } =
- window.__bootstrap.formData;
- const { parseMimeType } = window.__bootstrap.mimesniff;
-
- const { ReadableStream, isReadableStreamDisturbed } =
- window.__bootstrap.streams;
- const { Headers } = window.__bootstrap.headers;
- const { Blob, _byteSequence, File } = window.__bootstrap.file;
-
- const MAX_SIZE = 2 ** 32 - 2;
-
- /**
- * @param {Uint8Array} src
- * @param {Uint8Array} dst
- * @param {number} off the offset into `dst` where it will at which to begin writing values from `src`
- *
- * @returns {number} number of bytes copied
- */
- function copyBytes(src, dst, off = 0) {
- const r = dst.byteLength - off;
- if (src.byteLength > r) {
- src = src.subarray(0, r);
- }
- dst.set(src, off);
- return src.byteLength;
- }
-
- class Buffer {
- /** @type {Uint8Array} */
- #buf; // contents are the bytes buf[off : len(buf)]
- #off = 0; // read at buf[off], write at buf[buf.byteLength]
-
- /** @param {ArrayBuffer} [ab] */
- constructor(ab) {
- if (ab == null) {
- this.#buf = new Uint8Array(0);
- return;
- }
-
- this.#buf = new Uint8Array(ab);
- }
-
- /**
- * @returns {Uint8Array}
- */
- bytes(options = { copy: true }) {
- if (options.copy === false) return this.#buf.subarray(this.#off);
- return this.#buf.slice(this.#off);
- }
-
- /**
- * @returns {boolean}
- */
- empty() {
- return this.#buf.byteLength <= this.#off;
- }
-
- /**
- * @returns {number}
- */
- get length() {
- return this.#buf.byteLength - this.#off;
- }
-
- /**
- * @returns {number}
- */
- get capacity() {
- return this.#buf.buffer.byteLength;
- }
-
- /**
- * @returns {void}
- */
- reset() {
- this.#reslice(0);
- this.#off = 0;
- }
-
- /**
- * @param {number} n
- * @returns {number}
- */
- #tryGrowByReslice = (n) => {
- const l = this.#buf.byteLength;
- if (n <= this.capacity - l) {
- this.#reslice(l + n);
- return l;
- }
- return -1;
- };
-
- /**
- * @param {number} len
- * @returns {void}
- */
- #reslice = (len) => {
- if (!(len <= this.#buf.buffer.byteLength)) {
- throw new Error("assert");
- }
- this.#buf = new Uint8Array(this.#buf.buffer, 0, len);
- };
-
- /**
- * @param {Uint8Array} p
- * @returns {number}
- */
- writeSync(p) {
- const m = this.#grow(p.byteLength);
- return copyBytes(p, this.#buf, m);
- }
-
- /**
- * @param {Uint8Array} p
- * @returns {Promise<number>}
- */
- write(p) {
- const n = this.writeSync(p);
- return Promise.resolve(n);
- }
-
- /**
- * @param {number} n
- * @returns {number}
- */
- #grow = (n) => {
- const m = this.length;
- // If buffer is empty, reset to recover space.
- if (m === 0 && this.#off !== 0) {
- this.reset();
- }
- // Fast: Try to grow by means of a reslice.
- const i = this.#tryGrowByReslice(n);
- if (i >= 0) {
- return i;
- }
- const c = this.capacity;
- if (n <= Math.floor(c / 2) - m) {
- // We can slide things down instead of allocating a new
- // ArrayBuffer. We only need m+n <= c to slide, but
- // we instead let capacity get twice as large so we
- // don't spend all our time copying.
- copyBytes(this.#buf.subarray(this.#off), this.#buf);
- } else if (c + n > MAX_SIZE) {
- throw new Error("The buffer cannot be grown beyond the maximum size.");
- } else {
- // Not enough space anywhere, we need to allocate.
- const buf = new Uint8Array(Math.min(2 * c + n, MAX_SIZE));
- copyBytes(this.#buf.subarray(this.#off), buf);
- this.#buf = buf;
- }
- // Restore this.#off and len(this.#buf).
- this.#off = 0;
- this.#reslice(Math.min(m + n, MAX_SIZE));
- return m;
- };
-
- /**
- * @param {number} n
- * @returns {void}
- */
- grow(n) {
- if (n < 0) {
- throw Error("Buffer.grow: negative count");
- }
- const m = this.#grow(n);
- this.#reslice(m);
- }
- }
-
- /**
- * @param {unknown} x
- * @returns {x is ArrayBufferView}
- */
- function isTypedArray(x) {
- return ArrayBuffer.isView(x) && !(x instanceof DataView);
- }
-
- /**
- * @param {string} s
- * @param {string} value
- * @returns {boolean}
- */
- function hasHeaderValueOf(s, value) {
- return new RegExp(`^${value}(?:[\\s;]|$)`).test(s);
- }
-
- /**
- * @param {string} name
- * @param {BodyInit | null} bodySource
- */
- function validateBodyType(name, bodySource) {
- if (isTypedArray(bodySource)) {
- return true;
- } else if (bodySource instanceof ArrayBuffer) {
- return true;
- } else if (typeof bodySource === "string") {
- return true;
- } else if (bodySource instanceof ReadableStream) {
- return true;
- } else if (bodySource instanceof FormData) {
- return true;
- } else if (bodySource instanceof URLSearchParams) {
- return true;
- } else if (!bodySource) {
- return true; // null body is fine
- }
- throw new TypeError(
- `Bad ${name} body type: ${bodySource.constructor.name}`,
- );
- }
-
- /**
- * @param {ReadableStreamReader<Uint8Array>} stream
- * @param {number} [size]
- */
- async function bufferFromStream(
- stream,
- size,
- ) {
- const encoder = new TextEncoder();
- const buffer = new Buffer();
-
- if (size) {
- // grow to avoid unnecessary allocations & copies
- buffer.grow(size);
- }
-
- while (true) {
- const { done, value } = await stream.read();
-
- if (done) break;
-
- if (typeof value === "string") {
- buffer.writeSync(encoder.encode(value));
- } else if (value instanceof ArrayBuffer) {
- buffer.writeSync(new Uint8Array(value));
- } else if (value instanceof Uint8Array) {
- buffer.writeSync(value);
- } else if (!value) {
- // noop for undefined
- } else {
- throw new Error("unhandled type on stream read");
- }
- }
-
- return buffer.bytes().buffer;
- }
-
- /**
- * @param {Exclude<BodyInit, ReadableStream> | null} bodySource
- */
- function bodyToArrayBuffer(bodySource) {
- if (isTypedArray(bodySource)) {
- return bodySource.buffer;
- } else if (bodySource instanceof ArrayBuffer) {
- return bodySource;
- } else if (typeof bodySource === "string") {
- const enc = new TextEncoder();
- return enc.encode(bodySource).buffer;
- } else if (
- bodySource instanceof FormData ||
- bodySource instanceof URLSearchParams
- ) {
- const enc = new TextEncoder();
- return enc.encode(bodySource.toString()).buffer;
- } else if (!bodySource) {
- return new ArrayBuffer(0);
- }
- throw new Error(
- `Body type not implemented: ${bodySource.constructor.name}`,
- );
- }
-
- const BodyUsedError =
- "Failed to execute 'clone' on 'Body': body is already used";
-
- const teeBody = Symbol("Body#tee");
-
- // fastBody and dontValidateUrl allow users to opt out of certain behaviors
- const fastBody = Symbol("Body#fast");
- const dontValidateUrl = Symbol("dontValidateUrl");
- const lazyHeaders = Symbol("lazyHeaders");
-
- class Body {
- #contentType = "";
- #size;
- /** @type {BodyInit | null} */
- #bodySource;
- /** @type {ReadableStream<Uint8Array> | null} */
- #stream = null;
-
- /**
- * @param {BodyInit| null} bodySource
- * @param {{contentType: string, size?: number}} meta
- */
- constructor(bodySource, meta) {
- validateBodyType(this.constructor.name, bodySource);
- this.#bodySource = bodySource;
- this.#contentType = meta.contentType;
- this.#size = meta.size;
- }
-
- get body() {
- if (!this.#stream) {
- if (!this.#bodySource) {
- return null;
- } else if (this.#bodySource instanceof ReadableStream) {
- this.#stream = this.#bodySource;
- } else {
- const buf = bodyToArrayBuffer(this.#bodySource);
- if (!(buf instanceof ArrayBuffer)) {
- throw new Error(
- `Expected ArrayBuffer from body`,
- );
- }
-
- this.#stream = new ReadableStream({
- /**
- * @param {ReadableStreamDefaultController<Uint8Array>} controller
- */
- start(controller) {
- controller.enqueue(new Uint8Array(buf));
- controller.close();
- },
- });
- }
- }
-
- return this.#stream;
- }
-
- // Optimization that allows caller to bypass expensive ReadableStream.
- [fastBody]() {
- if (!this.#bodySource) {
- return null;
- } else if (!(this.#bodySource instanceof ReadableStream)) {
- return bodyToArrayBuffer(this.#bodySource);
- } else {
- return this.body;
- }
- }
-
- /** @returns {BodyInit | null} */
- [teeBody]() {
- if (this.#stream || this.#bodySource instanceof ReadableStream) {
- const body = this.body;
- if (body) {
- const [stream1, stream2] = body.tee();
- this.#stream = stream1;
- return stream2;
- } else {
- return null;
- }
- }
-
- return this.#bodySource;
- }
-
- get bodyUsed() {
- if (this.body && isReadableStreamDisturbed(this.body)) {
- return true;
- }
- return false;
- }
-
- set bodyUsed(_) {
- // this is a noop per spec
- }
-
- /** @returns {Promise<Blob>} */
- async blob() {
- return new Blob([await this.arrayBuffer()], {
- type: this.#contentType,
- });
- }
-
- // ref: https://fetch.spec.whatwg.org/#body-mixin
- /** @returns {Promise<FormData>} */
- async formData() {
- const formData = new FormData();
- const mimeType = parseMimeType(this.#contentType);
- if (mimeType) {
- if (mimeType.type === "multipart" && mimeType.subtype === "form-data") {
- // ref: https://tools.ietf.org/html/rfc2046#section-5.1
- const boundary = mimeType.parameters.get("boundary");
- const body = new Uint8Array(await this.arrayBuffer());
- return parseFormData(body, boundary);
- } else if (
- mimeType.type === "application" &&
- mimeType.subtype === "x-www-form-urlencoded"
- ) {
- // From https://github.com/github/fetch/blob/master/fetch.js
- // Copyright (c) 2014-2016 GitHub, Inc. MIT License
- const body = await this.text();
- try {
- body
- .trim()
- .split("&")
- .forEach((bytes) => {
- if (bytes) {
- const split = bytes.split("=");
- if (split.length >= 2) {
- // @ts-expect-error this is safe because of the above check
- const name = split.shift().replace(/\+/g, " ");
- const value = split.join("=").replace(/\+/g, " ");
- formData.append(
- decodeURIComponent(name),
- decodeURIComponent(value),
- );
- }
- }
- });
- } catch (e) {
- throw new TypeError("Invalid form urlencoded format");
- }
- return formData;
- }
- }
-
- throw new TypeError("Invalid form data");
- }
-
- /** @returns {Promise<string>} */
- async text() {
- if (typeof this.#bodySource === "string") {
- return this.#bodySource;
- }
-
- const ab = await this.arrayBuffer();
- const decoder = new TextDecoder("utf-8");
- return decoder.decode(ab);
- }
-
- /** @returns {Promise<any>} */
- async json() {
- const raw = await this.text();
- return JSON.parse(raw);
- }
-
- /** @returns {Promise<ArrayBuffer>} */
- arrayBuffer() {
- if (this.#bodySource instanceof ReadableStream) {
- const body = this.body;
- if (!body) throw new TypeError("Unreachable state (no body)");
- return bufferFromStream(body.getReader(), this.#size);
- }
- return Promise.resolve(bodyToArrayBuffer(this.#bodySource));
- }
- }
+ const webidl = window.__bootstrap.webidl;
+ const { byteLowerCase } = window.__bootstrap.infra;
+ const { InnerBody, extractBody } = window.__bootstrap.fetchBody;
+ const {
+ toInnerRequest,
+ fromInnerResponse,
+ redirectStatus,
+ nullBodyStatus,
+ networkError,
+ } = window.__bootstrap.fetch;
+
+ const REQUEST_BODY_HEADER_NAMES = [
+ "content-encoding",
+ "content-language",
+ "content-location",
+ "content-type",
+ ];
/**
- * @param {Deno.CreateHttpClientOptions} options
- * @returns {HttpClient}
- */
- function createHttpClient(options) {
- return new HttpClient(core.opSync("op_create_http_client", options));
- }
-
- class HttpClient {
- /**
- * @param {number} rid
- */
- constructor(rid) {
- this.rid = rid;
- }
- close() {
- core.close(this.rid);
- }
- }
-
- /**
- * @param {{ headers: [string,string][], method: string, url: string, baseUrl: string | null, clientRid: number | null, hasBody: boolean }} args
+ * @param {{ method: string, url: string, headers: [string, string][], clientRid: number | null, hasBody: boolean }} args
* @param {Uint8Array | null} body
- * @returns {{requestRid: number, requestBodyRid: number | null}}
+ * @returns {{ requestRid: number, requestBodyRid: number | null }}
*/
function opFetch(args, body) {
- let zeroCopy;
- if (body != null) {
- zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
- }
- return core.opSync("op_fetch", args, zeroCopy);
+ return core.opSync("op_fetch", args, body);
}
/**
- * @param {number} rid
- * @returns {Promise<{status: number, statusText: string, headers: Record<string,string[]>, url: string, responseRid: number}>}
+ * @param {number} rid
+ * @returns {Promise<{ status: number, statusText: string, headers: [string, string][], url: string, responseRid: number }>}
*/
function opFetchSend(rid) {
return core.opAsync("op_fetch_send", rid);
@@ -515,631 +54,254 @@
* @returns {Promise<void>}
*/
function opFetchRequestWrite(rid, body) {
- const zeroCopy = new Uint8Array(
- body.buffer,
- body.byteOffset,
- body.byteLength,
- );
- return core.opAsync("op_fetch_request_write", rid, zeroCopy);
+ return core.opAsync("op_fetch_request_write", rid, body);
}
- const NULL_BODY_STATUS = [101, 204, 205, 304];
- const REDIRECT_STATUS = [301, 302, 303, 307, 308];
-
/**
- * @param {string} s
- * @returns {string}
+ * @param {number} rid
+ * @param {Uint8Array} body
+ * @returns {Promise<number>}
*/
- function byteUpperCase(s) {
- return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) {
- return c.toUpperCase();
- });
+ function opFetchResponseRead(rid, body) {
+ return core.opAsync("op_fetch_response_read", rid, body);
}
/**
- * @param {string} m
- * @returns {boolean}
+ * @param {number} responseBodyRid
+ * @returns {ReadableStream<Uint8Array>}
*/
- function isKnownMethod(m) {
- return (
- m === "DELETE" ||
- m === "GET" ||
- m === "HEAD" ||
- m === "OPTIONS" ||
- m === "POST" ||
- m === "PUT"
- );
+ function createResponseBodyStream(responseBodyRid) {
+ return new ReadableStream({
+ type: "bytes",
+ async pull(controller) {
+ try {
+ // This is the largest possible size for a single packet on a TLS
+ // stream.
+ const chunk = new Uint8Array(16 * 1024 + 256);
+ const read = await opFetchResponseRead(
+ responseBodyRid,
+ chunk,
+ );
+ if (read > 0) {
+ // We read some data. Enqueue it onto the stream.
+ controller.enqueue(chunk.subarray(0, read));
+ } else {
+ // We have reached the end of the body, so we close the stream.
+ controller.close();
+ core.close(responseBodyRid);
+ }
+ } catch (err) {
+ // There was an error while reading a chunk of the body, so we
+ // error.
+ controller.error(err);
+ controller.close();
+ core.close(responseBodyRid);
+ }
+ },
+ cancel() {
+ core.close(responseBodyRid);
+ },
+ });
}
/**
- * @param {string} m
- * @returns {string}
+ * @param {InnerRequest} req
+ * @param {boolean} recursive
+ * @returns {Promise<InnerResponse>}
*/
- function normalizeMethod(m) {
- // Fast path for already valid methods
- if (isKnownMethod(m)) {
- return m;
- }
- // Normalize lower case (slowpath and should be avoided ...)
- const u = byteUpperCase(m);
- if (isKnownMethod(u)) {
- return u;
- }
- // Otherwise passthrough
- return m;
- }
-
- class Request extends Body {
- /** @type {string} */
- #method = "GET";
- /** @type {string} */
- #url = "";
- /** @type {Headers | string[][]} */
- #headers;
- /** @type {"include" | "omit" | "same-origin" | undefined} */
- #credentials = "omit";
-
- /**
- * @param {RequestInfo} input
- * @param {RequestInit} init
- */
- // @ts-expect-error because the use of super in this constructor is valid.
- constructor(input, init) {
- if (arguments.length < 1) {
- throw TypeError("Not enough arguments");
- }
-
- if (!init) {
- init = {};
- }
-
- let b;
-
- // prefer body from init
- if (init.body) {
- b = init.body;
- } else if (input instanceof Request) {
- if (input.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
- b = input[teeBody]();
- } else if (typeof input === "object" && "body" in input && input.body) {
- if (input.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
- b = input.body;
- } else {
- b = "";
- }
-
- let headers;
- let contentType = "";
- // prefer headers from init
- if (init.headers) {
- if (init[lazyHeaders] && Array.isArray(init.headers)) {
- // Trust the headers are valid, and only put them into the `Headers`
- // strucutre when the user accesses the property. We also assume that
- // all passed headers are lower-case (as is the case when they come
- // from hyper in Rust), and that headers are of type
- // `[string, string][]`.
- headers = init.headers;
- for (const tuple of headers) {
- if (tuple[0] === "content-type") {
- contentType = tuple[1];
- }
- }
+ async function mainFetch(req, recursive) {
+ /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
+ let reqBody = null;
+ if (req.body !== null) {
+ if (req.body.streamOrStatic instanceof ReadableStream) {
+ if (req.body.length === null) {
+ reqBody = req.body.stream;
} else {
- headers = new Headers(init.headers);
- contentType = headers.get("content-type") || "";
+ const reader = req.body.stream.getReader();
+ const r1 = await reader.read();
+ if (r1.done) throw new TypeError("Unreachable");
+ reqBody = r1.value;
+ const r2 = await reader.read();
+ if (!r2.done) throw new TypeError("Unreachable");
}
- } else if (input instanceof Request) {
- headers = input.headers;
- contentType = headers.get("content-type") || "";
} else {
- headers = new Headers();
- }
-
- super(b, { contentType });
- this.#headers = headers;
-
- if (input instanceof Request) {
- if (input.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
- // headers are already set above. no reason to do it again
- this.#method = input.method;
- this.#url = input.url;
- this.#credentials = input.credentials;
- } else {
- // Constructing a URL just for validation is known to be expensive.
- // dontValidateUrl allows one to opt out.
- if (init[dontValidateUrl]) {
- this.#url = input;
- } else {
- const baseUrl = getLocationHref();
- this.#url = baseUrl != null
- ? new URL(String(input), baseUrl).href
- : new URL(String(input)).href;
- }
- }
-
- if (init && "method" in init && init.method) {
- this.#method = normalizeMethod(init.method);
- }
-
- if (
- init &&
- "credentials" in init &&
- init.credentials &&
- ["omit", "same-origin", "include"].indexOf(init.credentials) !== -1
- ) {
- this.credentials = init.credentials;
- }
- }
-
- clone() {
- if (this.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
-
- const iterators = this.headers.entries();
- const headersList = [];
- for (const header of iterators) {
- headersList.push(header);
- }
-
- const body = this[teeBody]();
-
- return new Request(this.url, {
- body,
- method: this.method,
- headers: new Headers(headersList),
- credentials: this.credentials,
- });
- }
-
- get method() {
- return this.#method;
- }
-
- set method(_) {
- // can not set method
- }
-
- get url() {
- return this.#url;
- }
-
- set url(_) {
- // can not set url
- }
-
- get headers() {
- if (!(this.#headers instanceof Headers)) {
- this.#headers = new Headers(this.#headers);
- }
- return this.#headers;
- }
-
- set headers(_) {
- // can not set headers
- }
-
- get credentials() {
- return this.#credentials;
- }
-
- set credentials(_) {
- // can not set credentials
- }
- }
-
- const responseData = new WeakMap();
- class Response extends Body {
- /**
- * @param {BodyInit | null} body
- * @param {ResponseInit} [init]
- */
- constructor(body = null, init) {
- init = init ?? {};
-
- if (typeof init !== "object") {
- throw new TypeError(`'init' is not an object`);
- }
-
- const extraInit = responseData.get(init) || {};
- let { type = "default", url = "" } = extraInit;
-
- let status = init.status === undefined ? 200 : Number(init.status || 0);
- let statusText = init.statusText ?? "";
- let headers = init.headers instanceof Headers
- ? init.headers
- : new Headers(init.headers);
-
- if (init.status !== undefined && (status < 200 || status > 599)) {
- throw new RangeError(
- `The status provided (${init.status}) is outside the range [200, 599]`,
- );
- }
-
- // null body status
- if (body && NULL_BODY_STATUS.includes(status)) {
- throw new TypeError("Response with null body status cannot have body");
- }
-
- if (!type) {
- type = "default";
- } else {
- if (type == "error") {
- // spec: https://fetch.spec.whatwg.org/#concept-network-error
- status = 0;
- statusText = "";
- headers = new Headers();
- body = null;
- /* spec for other Response types:
- https://fetch.spec.whatwg.org/#concept-filtered-response-basic
- Please note that type "basic" is not the same thing as "default".*/
- } else if (type == "basic") {
- for (const h of headers) {
- /* Forbidden Response-Header Names:
- https://fetch.spec.whatwg.org/#forbidden-response-header-name */
- if (["set-cookie", "set-cookie2"].includes(h[0].toLowerCase())) {
- headers.delete(h[0]);
- }
+ req.body.streamOrStatic.consumed = true;
+ reqBody = req.body.streamOrStatic.body;
+ }
+ }
+
+ const { requestRid, requestBodyRid } = opFetch({
+ method: req.method,
+ url: req.currentUrl(),
+ headers: req.headerList,
+ clientRid: req.clientRid,
+ hasBody: reqBody !== null,
+ }, reqBody instanceof Uint8Array ? reqBody : null);
+
+ if (requestBodyRid !== null) {
+ if (reqBody === null || !(reqBody instanceof ReadableStream)) {
+ throw new TypeError("Unreachable");
+ }
+ const reader = reqBody.getReader();
+ (async () => {
+ while (true) {
+ const { value, done } = await reader.read();
+ if (done) break;
+ if (!(value instanceof Uint8Array)) {
+ await reader.cancel("value not a Uint8Array");
+ break;
}
- } else if (type == "cors") {
- /* CORS-safelisted Response-Header Names:
- https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name */
- const allowedHeaders = [
- "Cache-Control",
- "Content-Language",
- "Content-Length",
- "Content-Type",
- "Expires",
- "Last-Modified",
- "Pragma",
- ].map((c) => c.toLowerCase());
- for (const h of headers) {
- /* Technically this is still not standards compliant because we are
- supposed to allow headers allowed in the
- 'Access-Control-Expose-Headers' header in the 'internal response'
- However, this implementation of response doesn't seem to have an
- easy way to access the internal response, so we ignore that
- header.
- TODO(serverhiccups): change how internal responses are handled
- so we can do this properly. */
- if (!allowedHeaders.includes(h[0].toLowerCase())) {
- headers.delete(h[0]);
- }
+ try {
+ await opFetchRequestWrite(requestBodyRid, value);
+ } catch (err) {
+ await reader.cancel(err);
+ break;
}
- /* TODO(serverhiccups): Once I fix the 'internal response' thing,
- these actually need to treat the internal response differently */
- } else if (type == "opaque" || type == "opaqueredirect") {
- url = "";
- status = 0;
- statusText = "";
- headers = new Headers();
- body = null;
}
- }
-
- const contentType = headers.get("content-type") || "";
- const size = Number(headers.get("content-length")) || undefined;
-
- super(body, { contentType, size });
-
- this.url = url;
- this.statusText = statusText;
- this.status = extraInit.status || status;
- this.headers = headers;
- this.redirected = extraInit.redirected || false;
- this.type = type;
- }
-
- get ok() {
- return 200 <= this.status && this.status < 300;
+ core.close(requestBodyRid);
+ })();
+ }
+
+ const resp = await opFetchSend(requestRid);
+ /** @type {InnerResponse} */
+ const response = {
+ headerList: resp.headers,
+ status: resp.status,
+ body: null,
+ statusMessage: resp.statusText,
+ type: "basic",
+ url() {
+ if (this.urlList.length == 0) return null;
+ return this.urlList[this.urlList.length - 1];
+ },
+ urlList: req.urlList,
+ };
+ if (redirectStatus(resp.status)) {
+ switch (req.redirectMode) {
+ case "error":
+ core.close(resp.responseRid);
+ return networkError(
+ "Encountered redirect while redirect mode is set to 'error'",
+ );
+ case "follow":
+ core.close(resp.responseRid);
+ return httpRedirectFetch(req, response);
+ case "manual":
+ break;
+ }
+ }
+
+ if (nullBodyStatus(response.status)) {
+ core.close(resp.responseRid);
+ } else {
+ response.body = new InnerBody(createResponseBodyStream(resp.responseRid));
}
- clone() {
- if (this.bodyUsed) {
- throw TypeError(BodyUsedError);
- }
-
- const iterators = this.headers.entries();
- const headersList = [];
- for (const header of iterators) {
- headersList.push(header);
- }
-
- const body = this[teeBody]();
+ if (recursive) return response;
- return new Response(body, {
- status: this.status,
- statusText: this.statusText,
- headers: new Headers(headersList),
- });
+ if (response.urlList.length === 0) {
+ response.urlList = [...req.urlList];
}
- /**
- * @param {string } url
- * @param {number} status
- */
- static redirect(url, status = 302) {
- if (![301, 302, 303, 307, 308].includes(status)) {
- throw new RangeError(
- "The redirection status must be one of 301, 302, 303, 307 and 308.",
- );
- }
- return new Response(null, {
- status,
- statusText: "",
- headers: [["Location", String(url)]],
- });
- }
- }
-
- /** @type {string | null} */
- let baseUrl = null;
-
- /** @param {string} href */
- function setBaseUrl(href) {
- baseUrl = href;
+ return response;
}
/**
- * @param {string} url
- * @param {string} method
- * @param {Headers} headers
- * @param {ReadableStream<Uint8Array> | ArrayBufferView | undefined} body
- * @param {number | null} clientRid
- * @returns {Promise<{status: number, statusText: string, headers: Record<string,string[]>, url: string, responseRid: number}>}
+ * @param {InnerRequest} request
+ * @param {InnerResponse} response
+ * @returns {Promise<InnerResponse>}
*/
- async function sendFetchReq(url, method, headers, body, clientRid) {
- /** @type {[string, string][]} */
- let headerArray = [];
- if (headers) {
- headerArray = Array.from(headers.entries());
+ function httpRedirectFetch(request, response) {
+ const locationHeaders = response.headerList.filter((entry) =>
+ byteLowerCase(entry[0]) === "location"
+ );
+ if (locationHeaders.length === 0) {
+ return response;
}
-
- const { requestRid, requestBodyRid } = opFetch(
- {
- method,
- url,
- baseUrl,
- headers: headerArray,
- clientRid,
- hasBody: !!body,
- },
- body instanceof Uint8Array ? body : null,
+ const locationURL = new URL(
+ locationHeaders[0][1],
+ response.url() ?? undefined,
);
- if (requestBodyRid) {
- if (!(body instanceof ReadableStream)) {
- throw new TypeError("Unreachable state (body is not ReadableStream).");
+ if (locationURL.hash === "") {
+ locationURL.hash = request.currentUrl().hash;
+ }
+ if (locationURL.protocol !== "https:" && locationURL.protocol !== "http:") {
+ return networkError("Can not redirect to a non HTTP(s) url");
+ }
+ if (request.redirectCount === 20) {
+ return networkError("Maximum number of redirects (20) reached");
+ }
+ request.redirectCount++;
+ if (
+ response.status !== 303 && request.body !== null &&
+ request.body.source === null
+ ) {
+ return networkError(
+ "Can not redeliver a streaming request body after a redirect",
+ );
+ }
+ if (
+ ((response.status === 301 || response.status === 302) &&
+ request.method === "POST") ||
+ (response.status === 303 &&
+ (request.method !== "GET" && request.method !== "HEAD"))
+ ) {
+ request.method = "GET";
+ request.body = null;
+ for (let i = 0; i < request.headerList.length; i++) {
+ if (
+ REQUEST_BODY_HEADER_NAMES.includes(
+ byteLowerCase(request.headerList[i][0]),
+ )
+ ) {
+ request.headerList.splice(i, 1);
+ i--;
+ }
}
- const writer = new WritableStream({
- /**
- * @param {Uint8Array} chunk
- * @param {WritableStreamDefaultController} controller
- */
- async write(chunk, controller) {
- try {
- await opFetchRequestWrite(requestBodyRid, chunk);
- } catch (err) {
- controller.error(err);
- }
- },
- close() {
- core.close(requestBodyRid);
- },
- });
- body.pipeTo(writer);
}
-
- return await opFetchSend(requestRid);
+ if (request.body !== null) {
+ const res = extractBody(request.body.source);
+ request.body = res.body;
+ }
+ request.urlList.push(locationURL.href);
+ return mainFetch(request, true);
}
/**
- * @param {Request | URL | string} input
- * @param {RequestInit & {client: Deno.HttpClient}} [init]
- * @returns {Promise<Response>}
+ * @param {RequestInfo} input
+ * @param {RequestInit} init
*/
- async function fetch(input, init) {
- /** @type {string | null} */
- let url;
- let method = null;
- let headers = null;
- let body;
- let clientRid = null;
- let redirected = false;
- let remRedirectCount = 20; // TODO(bartlomieju): use a better way to handle
-
- if (typeof input === "string" || input instanceof URL) {
- url = typeof input === "string" ? input : input.href;
- if (init != null) {
- method = init.method || null;
- if (init.headers) {
- headers = init.headers instanceof Headers
- ? init.headers
- : new Headers(init.headers);
- } else {
- headers = null;
- }
-
- // ref: https://fetch.spec.whatwg.org/#body-mixin
- // Body should have been a mixin
- // but we are treating it as a separate class
- if (init.body) {
- if (!headers) {
- headers = new Headers();
- }
- let contentType = "";
- if (typeof init.body === "string") {
- body = new TextEncoder().encode(init.body);
- contentType = "text/plain;charset=UTF-8";
- } else if (isTypedArray(init.body)) {
- body = init.body;
- } else if (init.body instanceof ArrayBuffer) {
- body = new Uint8Array(init.body);
- } else if (init.body instanceof URLSearchParams) {
- body = new TextEncoder().encode(init.body.toString());
- contentType = "application/x-www-form-urlencoded;charset=UTF-8";
- } else if (init.body instanceof Blob) {
- body = init.body[_byteSequence];
- contentType = init.body.type;
- } else if (init.body instanceof FormData) {
- const res = encodeFormData(init.body);
- body = res.body;
- contentType = res.contentType;
- } else if (init.body instanceof ReadableStream) {
- body = init.body;
- }
- if (contentType && !headers.has("content-type")) {
- headers.set("content-type", contentType);
- }
- }
-
- if (init.client instanceof HttpClient) {
- clientRid = init.client.rid;
- }
- }
- } else {
- url = input.url;
- method = input.method;
- headers = input.headers;
+ async function fetch(input, init = {}) {
+ const prefix = "Failed to call 'fetch'";
+ input = webidl.converters["RequestInfo"](input, {
+ prefix,
+ context: "Argument 1",
+ });
+ init = webidl.converters["RequestInit"](init, {
+ prefix,
+ context: "Argument 2",
+ });
- if (input.body) {
- body = input.body;
- }
+ // 1.
+ const requestObject = new Request(input, init);
+ // 2.
+ const request = toInnerRequest(requestObject);
+ // 10.
+ if (!requestObject.headers.has("Accept")) {
+ request.headerList.push(["Accept", "*/*"]);
}
- let responseBody;
- let responseInit = {};
- while (remRedirectCount) {
- const fetchResp = await sendFetchReq(
- url,
- method ?? "GET",
- headers ?? new Headers(),
- body,
- clientRid,
+ // 12.
+ const response = await mainFetch(request, false);
+ if (response.type === "error") {
+ throw new TypeError(
+ "Fetch failed: " + (response.error ?? "unknown error"),
);
- const rid = fetchResp.responseRid;
-
- if (
- NULL_BODY_STATUS.includes(fetchResp.status) ||
- REDIRECT_STATUS.includes(fetchResp.status)
- ) {
- // We won't use body of received response, so close it now
- // otherwise it will be kept in resource table.
- core.close(rid);
- responseBody = null;
- } else {
- responseBody = new ReadableStream({
- type: "bytes",
- /** @param {ReadableStreamDefaultController<Uint8Array>} controller */
- async pull(controller) {
- try {
- const chunk = new Uint8Array(16 * 1024 + 256);
- const read = await core.opAsync(
- "op_fetch_response_read",
- rid,
- chunk,
- );
- if (read != 0) {
- if (chunk.length == read) {
- controller.enqueue(chunk);
- } else {
- controller.enqueue(chunk.subarray(0, read));
- }
- } else {
- controller.close();
- core.close(rid);
- }
- } catch (e) {
- controller.error(e);
- controller.close();
- core.close(rid);
- }
- },
- cancel() {
- // When reader.cancel() is called
- core.close(rid);
- },
- });
- }
-
- responseInit = {
- status: 200,
- statusText: fetchResp.statusText,
- headers: fetchResp.headers,
- };
-
- responseData.set(responseInit, {
- redirected,
- rid: fetchResp.responseRid,
- status: fetchResp.status,
- url: fetchResp.url,
- });
-
- const response = new Response(responseBody, responseInit);
-
- if (REDIRECT_STATUS.includes(fetchResp.status)) {
- // We're in a redirect status
- switch ((init && init.redirect) || "follow") {
- case "error":
- responseInit = {};
- responseData.set(responseInit, {
- type: "error",
- redirected: false,
- url: "",
- });
- return new Response(null, responseInit);
- case "manual":
- // On the web this would return a `opaqueredirect` response, but
- // those don't make sense server side. See denoland/deno#8351.
- return response;
- case "follow":
- // fallthrough
- default: {
- /** @type {string | null} */
- let redirectUrl = response.headers.get("Location");
- if (redirectUrl == null) {
- return response; // Unspecified
- }
- if (
- !redirectUrl.startsWith("http://") &&
- !redirectUrl.startsWith("https://")
- ) {
- redirectUrl = new URL(redirectUrl, fetchResp.url).href;
- }
- url = redirectUrl;
- redirected = true;
- remRedirectCount--;
- }
- }
- } else {
- return response;
- }
}
- responseData.set(responseInit, {
- type: "error",
- redirected: false,
- url: "",
- });
-
- return new Response(null, responseInit);
+ return fromInnerResponse(response, "immutable");
}
- window.__bootstrap.fetch = {
- FormData,
- setBaseUrl,
- fetch,
- Request,
- Response,
- HttpClient,
- createHttpClient,
- fastBody,
- dontValidateUrl,
- lazyHeaders,
- };
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.fetch = fetch;
})(this);