summaryrefslogtreecommitdiff
path: root/ext/fetch/26_fetch.js
diff options
context:
space:
mode:
authorRyan Dahl <ry@tinyclouds.org>2021-08-11 12:27:05 +0200
committerGitHub <noreply@github.com>2021-08-11 12:27:05 +0200
commita0285e2eb88f6254f6494b0ecd1878db3a3b2a58 (patch)
tree90671b004537e20f9493fd3277ffd21d30b39a0e /ext/fetch/26_fetch.js
parent3a6994115176781b3a93d70794b1b81bc95e42b4 (diff)
Rename extensions/ directory to ext/ (#11643)
Diffstat (limited to 'ext/fetch/26_fetch.js')
-rw-r--r--ext/fetch/26_fetch.js542
1 files changed, 542 insertions, 0 deletions
diff --git a/ext/fetch/26_fetch.js b/ext/fetch/26_fetch.js
new file mode 100644
index 000000000..f7166001e
--- /dev/null
+++ b/ext/fetch/26_fetch.js
@@ -0,0 +1,542 @@
+// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+
+// @ts-check
+/// <reference path="../../core/lib.deno_core.d.ts" />
+/// <reference path="../web/internal.d.ts" />
+/// <reference path="../url/internal.d.ts" />
+/// <reference path="../web/lib.deno_web.d.ts" />
+/// <reference path="../web/06_streams_types.d.ts" />
+/// <reference path="./internal.d.ts" />
+/// <reference path="./lib.deno_fetch.d.ts" />
+/// <reference lib="esnext" />
+"use strict";
+
+((window) => {
+ const core = window.Deno.core;
+ const webidl = window.__bootstrap.webidl;
+ const { errorReadableStream } = window.__bootstrap.streams;
+ const { InnerBody, extractBody } = window.__bootstrap.fetchBody;
+ const {
+ toInnerRequest,
+ toInnerResponse,
+ fromInnerResponse,
+ redirectStatus,
+ nullBodyStatus,
+ networkError,
+ abortedNetworkError,
+ } = window.__bootstrap.fetch;
+ const abortSignal = window.__bootstrap.abortSignal;
+ const { DOMException } = window.__bootstrap.domException;
+ const {
+ ArrayPrototypePush,
+ ArrayPrototypeSplice,
+ ArrayPrototypeFilter,
+ ArrayPrototypeIncludes,
+ Promise,
+ PromisePrototypeThen,
+ PromisePrototypeCatch,
+ StringPrototypeToLowerCase,
+ TypedArrayPrototypeSubarray,
+ TypeError,
+ Uint8Array,
+ } = window.__bootstrap.primordials;
+
+ const REQUEST_BODY_HEADER_NAMES = [
+ "content-encoding",
+ "content-language",
+ "content-location",
+ "content-type",
+ ];
+
+ /**
+ * @param {{ method: string, url: string, headers: [string, string][], clientRid: number | null, hasBody: boolean }} args
+ * @param {Uint8Array | null} body
+ * @returns {{ requestRid: number, requestBodyRid: number | null }}
+ */
+ function opFetch(args, body) {
+ return core.opSync("op_fetch", args, body);
+ }
+
+ /**
+ * @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);
+ }
+
+ /**
+ * @param {number} rid
+ * @param {Uint8Array} body
+ * @returns {Promise<void>}
+ */
+ function opFetchRequestWrite(rid, body) {
+ return core.opAsync("op_fetch_request_write", rid, body);
+ }
+
+ /**
+ * @param {number} rid
+ * @param {Uint8Array} body
+ * @returns {Promise<number>}
+ */
+ function opFetchResponseRead(rid, body) {
+ return core.opAsync("op_fetch_response_read", rid, body);
+ }
+
+ // A finalization registry to clean up underlying fetch resources that are GC'ed.
+ const RESOURCE_REGISTRY = new FinalizationRegistry((rid) => {
+ try {
+ core.close(rid);
+ } catch {
+ // might have already been closed
+ }
+ });
+
+ /**
+ * @param {number} responseBodyRid
+ * @param {AbortSignal} [terminator]
+ * @returns {ReadableStream<Uint8Array>}
+ */
+ function createResponseBodyStream(responseBodyRid, terminator) {
+ function onAbort() {
+ if (readable) {
+ errorReadableStream(
+ readable,
+ new DOMException("Ongoing fetch was aborted.", "AbortError"),
+ );
+ }
+ try {
+ core.close(responseBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ // TODO(lucacasonato): clean up registration
+ terminator[abortSignal.add](onAbort);
+ const readable = 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(TypedArrayPrototypeSubarray(chunk, 0, read));
+ } else {
+ RESOURCE_REGISTRY.unregister(readable);
+ // We have reached the end of the body, so we close the stream.
+ controller.close();
+ try {
+ core.close(responseBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ } catch (err) {
+ RESOURCE_REGISTRY.unregister(readable);
+ if (terminator.aborted) {
+ controller.error(
+ new DOMException("Ongoing fetch was aborted.", "AbortError"),
+ );
+ } else {
+ // There was an error while reading a chunk of the body, so we
+ // error.
+ controller.error(err);
+ }
+ try {
+ core.close(responseBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ },
+ cancel() {
+ if (!terminator.aborted) {
+ terminator[abortSignal.signalAbort]();
+ }
+ },
+ });
+ RESOURCE_REGISTRY.register(readable, responseBodyRid, readable);
+ return readable;
+ }
+
+ /**
+ * @param {InnerRequest} req
+ * @param {boolean} recursive
+ * @param {AbortSignal} terminator
+ * @returns {Promise<InnerResponse>}
+ */
+ async function mainFetch(req, recursive, terminator) {
+ /** @type {ReadableStream<Uint8Array> | Uint8Array | null} */
+ let reqBody = null;
+
+ if (req.body !== null) {
+ if (req.body.streamOrStatic instanceof ReadableStream) {
+ if (req.body.length === null || req.body.source instanceof Blob) {
+ reqBody = req.body.stream;
+ } else {
+ const reader = req.body.stream.getReader();
+ const r1 = await reader.read();
+ if (r1.done) {
+ reqBody = new Uint8Array(0);
+ } else {
+ reqBody = r1.value;
+ const r2 = await reader.read();
+ if (!r2.done) throw new TypeError("Unreachable");
+ }
+ }
+ } else {
+ req.body.streamOrStatic.consumed = true;
+ reqBody = req.body.streamOrStatic.body;
+ }
+ }
+
+ const { requestRid, requestBodyRid, cancelHandleRid } = opFetch({
+ method: req.method,
+ url: req.currentUrl(),
+ headers: req.headerList,
+ clientRid: req.clientRid,
+ hasBody: reqBody !== null,
+ bodyLength: req.body?.length,
+ }, reqBody instanceof Uint8Array ? reqBody : null);
+
+ function onAbort() {
+ try {
+ core.close(cancelHandleRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ try {
+ core.close(requestBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ terminator[abortSignal.add](onAbort);
+
+ if (requestBodyRid !== null) {
+ if (reqBody === null || !(reqBody instanceof ReadableStream)) {
+ throw new TypeError("Unreachable");
+ }
+ const reader = reqBody.getReader();
+ (async () => {
+ while (true) {
+ const { value, done } = await PromisePrototypeCatch(
+ reader.read(),
+ (err) => {
+ if (terminator.aborted) return { done: true, value: undefined };
+ throw err;
+ },
+ );
+ if (done) break;
+ if (!(value instanceof Uint8Array)) {
+ await reader.cancel("value not a Uint8Array");
+ break;
+ }
+ try {
+ await PromisePrototypeCatch(
+ opFetchRequestWrite(requestBodyRid, value),
+ (err) => {
+ if (terminator.aborted) return;
+ throw err;
+ },
+ );
+ if (terminator.aborted) break;
+ } catch (err) {
+ await reader.cancel(err);
+ break;
+ }
+ }
+ try {
+ core.close(requestBodyRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ })();
+ }
+
+ let resp;
+ try {
+ resp = await PromisePrototypeCatch(opFetchSend(requestRid), (err) => {
+ if (terminator.aborted) return;
+ throw err;
+ });
+ } finally {
+ try {
+ core.close(cancelHandleRid);
+ } catch (_) {
+ // might have already been closed
+ }
+ }
+ if (terminator.aborted) return abortedNetworkError();
+
+ /** @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, terminator);
+ case "manual":
+ break;
+ }
+ }
+
+ if (nullBodyStatus(response.status)) {
+ core.close(resp.responseRid);
+ } else {
+ if (req.method === "HEAD" || req.method === "CONNECT") {
+ response.body = null;
+ core.close(resp.responseRid);
+ } else {
+ response.body = new InnerBody(
+ createResponseBodyStream(resp.responseRid, terminator),
+ );
+ }
+ }
+
+ if (recursive) return response;
+
+ if (response.urlList.length === 0) {
+ response.urlList = [...req.urlList];
+ }
+
+ return response;
+ }
+
+ /**
+ * @param {InnerRequest} request
+ * @param {InnerResponse} response
+ * @returns {Promise<InnerResponse>}
+ */
+ function httpRedirectFetch(request, response, terminator) {
+ const locationHeaders = ArrayPrototypeFilter(
+ response.headerList,
+ (entry) => entry[0] === "location",
+ );
+ if (locationHeaders.length === 0) {
+ return response;
+ }
+ const locationURL = new URL(
+ locationHeaders[0][1],
+ response.url() ?? undefined,
+ );
+ 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 (
+ ArrayPrototypeIncludes(
+ REQUEST_BODY_HEADER_NAMES,
+ request.headerList[i][0],
+ )
+ ) {
+ ArrayPrototypeSplice(request.headerList, i, 1);
+ i--;
+ }
+ }
+ }
+ if (request.body !== null) {
+ const res = extractBody(request.body.source);
+ request.body = res.body;
+ }
+ ArrayPrototypePush(request.urlList, locationURL.href);
+ return mainFetch(request, true, terminator);
+ }
+
+ /**
+ * @param {RequestInfo} input
+ * @param {RequestInit} init
+ */
+ function fetch(input, init = {}) {
+ // 1.
+ const p = new Promise((resolve, reject) => {
+ const prefix = "Failed to call 'fetch'";
+ webidl.requiredArguments(arguments.length, 1, { prefix });
+ input = webidl.converters["RequestInfo"](input, {
+ prefix,
+ context: "Argument 1",
+ });
+ init = webidl.converters["RequestInit"](init, {
+ prefix,
+ context: "Argument 2",
+ });
+
+ // 2.
+ const requestObject = new Request(input, init);
+ // 3.
+ const request = toInnerRequest(requestObject);
+ // 4.
+ if (requestObject.signal.aborted) {
+ reject(abortFetch(request, null));
+ return;
+ }
+
+ // 7.
+ let responseObject = null;
+ // 9.
+ let locallyAborted = false;
+ // 10.
+ function onabort() {
+ locallyAborted = true;
+ reject(abortFetch(request, responseObject));
+ }
+ requestObject.signal[abortSignal.add](onabort);
+
+ if (!requestObject.headers.has("accept")) {
+ ArrayPrototypePush(request.headerList, ["accept", "*/*"]);
+ }
+
+ // 12.
+ PromisePrototypeCatch(
+ PromisePrototypeThen(
+ mainFetch(request, false, requestObject.signal),
+ (response) => {
+ // 12.1.
+ if (locallyAborted) return;
+ // 12.2.
+ if (response.aborted) {
+ reject(request, responseObject);
+ requestObject.signal[abortSignal.remove](onabort);
+ return;
+ }
+ // 12.3.
+ if (response.type === "error") {
+ const err = new TypeError(
+ "Fetch failed: " + (response.error ?? "unknown error"),
+ );
+ reject(err);
+ requestObject.signal[abortSignal.remove](onabort);
+ return;
+ }
+ responseObject = fromInnerResponse(response, "immutable");
+ resolve(responseObject);
+ requestObject.signal[abortSignal.remove](onabort);
+ },
+ ),
+ (err) => {
+ reject(err);
+ requestObject.signal[abortSignal.remove](onabort);
+ },
+ );
+ });
+ return p;
+ }
+
+ function abortFetch(request, responseObject) {
+ const error = new DOMException("Ongoing fetch was aborted.", "AbortError");
+ if (request.body !== null) request.body.cancel(error);
+ if (responseObject !== null) {
+ const response = toInnerResponse(responseObject);
+ if (response.body !== null) response.body.error(error);
+ }
+ return error;
+ }
+
+ /**
+ * Handle the Promise<Response> argument to the WebAssembly streaming
+ * APIs. This function should be registered through
+ * `Deno.core.setWasmStreamingCallback`.
+ *
+ * @param {any} source The source parameter that the WebAssembly
+ * streaming API was called with.
+ * @param {number} rid An rid that can be used with
+ * `Deno.core.wasmStreamingFeed`.
+ */
+ function handleWasmStreaming(source, rid) {
+ // This implements part of
+ // https://webassembly.github.io/spec/web-api/#compile-a-potential-webassembly-response
+ (async () => {
+ try {
+ const res = webidl.converters["Response"](await source, {
+ prefix: "Failed to call 'WebAssembly.compileStreaming'",
+ context: "Argument 1",
+ });
+
+ // 2.3.
+ // The spec is ambiguous here, see
+ // https://github.com/WebAssembly/spec/issues/1138. The WPT tests
+ // expect the raw value of the Content-Type attribute lowercased.
+ if (
+ StringPrototypeToLowerCase(res.headers.get("Content-Type")) !==
+ "application/wasm"
+ ) {
+ throw new TypeError("Invalid WebAssembly content type.");
+ }
+
+ // 2.5.
+ if (!res.ok) {
+ throw new TypeError(`HTTP status code ${res.status}`);
+ }
+
+ // 2.6.
+ // Rather than consuming the body as an ArrayBuffer, this passes each
+ // chunk to the feed as soon as it's available.
+ if (res.body !== null) {
+ const reader = res.body.getReader();
+ while (true) {
+ const { value: chunk, done } = await reader.read();
+ if (done) break;
+ Deno.core.wasmStreamingFeed(rid, "bytes", chunk);
+ }
+ }
+
+ // 2.7.
+ Deno.core.wasmStreamingFeed(rid, "finish");
+ } catch (err) {
+ // 2.8 and 3
+ Deno.core.wasmStreamingFeed(rid, "abort", err);
+ }
+ })();
+ }
+
+ window.__bootstrap.fetch ??= {};
+ window.__bootstrap.fetch.fetch = fetch;
+ window.__bootstrap.fetch.handleWasmStreaming = handleWasmStreaming;
+})(this);