1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-24 16:19:12 -05:00
denoland-deno/op_crates/fetch/26_fetch.js

1506 lines
40 KiB
JavaScript
Raw Normal View History

// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
2020-09-18 09:20:55 -04:00
// @ts-check
/// <reference path="../../core/lib.deno_core.d.ts" />
/// <reference path="../web/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
/// <reference path="./11_streams_types.d.ts" />
/// <reference path="./internal.d.ts" />
/// <reference path="./lib.deno_fetch.d.ts" />
/// <reference lib="esnext" />
"use strict";
2020-09-18 09:20:55 -04:00
((window) => {
const core = window.Deno.core;
// provided by "deno_web"
const { URLSearchParams } = window.__bootstrap.url;
const { getLocationHref } = window.__bootstrap.location;
2020-09-18 09:20:55 -04:00
const { requiredArguments } = window.__bootstrap.fetchUtil;
const { ReadableStream, isReadableStreamDisturbed } =
window.__bootstrap.streams;
const { DomIterableMixin } = window.__bootstrap.domIterable;
const { Headers } = window.__bootstrap.headers;
const { Blob, _byteSequence, File } = window.__bootstrap.file;
2020-09-18 09:20:55 -04:00
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
*/
2020-09-18 09:20:55 -04:00
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)]
2020-09-18 09:20:55 -04:00
#off = 0; // read at buf[off], write at buf[buf.byteLength]
/** @param {ArrayBuffer} [ab] */
2020-09-18 09:20:55 -04:00
constructor(ab) {
if (ab == null) {
this.#buf = new Uint8Array(0);
return;
}
this.#buf = new Uint8Array(ab);
}
/**
* @returns {Uint8Array}
*/
2020-09-18 09:20:55 -04:00
bytes(options = { copy: true }) {
if (options.copy === false) return this.#buf.subarray(this.#off);
return this.#buf.slice(this.#off);
}
/**
* @returns {boolean}
*/
2020-09-18 09:20:55 -04:00
empty() {
return this.#buf.byteLength <= this.#off;
}
/**
* @returns {number}
*/
2020-09-18 09:20:55 -04:00
get length() {
return this.#buf.byteLength - this.#off;
}
/**
* @returns {number}
*/
2020-09-18 09:20:55 -04:00
get capacity() {
return this.#buf.buffer.byteLength;
}
/**
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
reset() {
this.#reslice(0);
this.#off = 0;
}
/**
* @param {number} n
* @returns {number}
*/
2020-09-18 09:20:55 -04:00
#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}
*/
2020-09-18 09:20:55 -04:00
#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}
*/
2020-09-18 09:20:55 -04:00
writeSync(p) {
const m = this.#grow(p.byteLength);
return copyBytes(p, this.#buf, m);
}
/**
* @param {Uint8Array} p
* @returns {Promise<number>}
*/
2020-09-18 09:20:55 -04:00
write(p) {
const n = this.writeSync(p);
return Promise.resolve(n);
}
/**
* @param {number} n
* @returns {number}
*/
2020-09-18 09:20:55 -04:00
#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}
*/
2020-09-18 09:20:55 -04:00
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}
*/
2020-09-18 09:20:55 -04:00
function isTypedArray(x) {
return ArrayBuffer.isView(x) && !(x instanceof DataView);
}
/**
* @param {string} s
* @param {string} value
* @returns {boolean}
*/
2020-09-18 09:20:55 -04:00
function hasHeaderValueOf(s, value) {
return new RegExp(`^${value}(?:[\\s;]|$)`).test(s);
2020-09-18 09:20:55 -04:00
}
/**
* @param {string} value
* @returns {Map<string, string>}
*/
2020-09-18 09:20:55 -04:00
function getHeaderValueParams(value) {
/** @type {Map<string, string>} */
2020-09-18 09:20:55 -04:00
const params = new Map();
// Forced to do so for some Map constructor param mismatch
value
.split(";")
.slice(1)
.map((s) => s.trim().split("="))
.filter((arr) => arr.length > 1)
.map(([k, v]) => [k, v.replace(/^"([^"]*)"$/, "$1")])
.forEach(([k, v]) => params.set(k, v));
return params;
}
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const CR = "\r".charCodeAt(0);
const LF = "\n".charCodeAt(0);
const dataSymbol = Symbol("data");
/**
* @param {Blob | string} value
* @param {string | undefined} filename
* @returns {FormDataEntryValue}
*/
2020-09-18 09:20:55 -04:00
function parseFormDataValue(value, filename) {
if (value instanceof File) {
return new File([value], filename || value.name, {
2020-09-18 09:20:55 -04:00
type: value.type,
lastModified: value.lastModified,
});
} else if (value instanceof Blob) {
return new File([value], filename || "blob", {
2020-09-18 09:20:55 -04:00
type: value.type,
});
} else {
return String(value);
}
}
class FormDataBase {
/** @type {[name: string, entry: FormDataEntryValue][]} */
2020-09-18 09:20:55 -04:00
[dataSymbol] = [];
/**
* @param {string} name
* @param {string | Blob} value
* @param {string} [filename]
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
append(name, value, filename) {
requiredArguments("FormData.append", arguments.length, 2);
name = String(name);
this[dataSymbol].push([name, parseFormDataValue(value, filename)]);
}
/**
* @param {string} name
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
delete(name) {
requiredArguments("FormData.delete", arguments.length, 1);
name = String(name);
let i = 0;
while (i < this[dataSymbol].length) {
if (this[dataSymbol][i][0] === name) {
this[dataSymbol].splice(i, 1);
} else {
i++;
}
}
}
/**
* @param {string} name
* @returns {FormDataEntryValue[]}
*/
2020-09-18 09:20:55 -04:00
getAll(name) {
requiredArguments("FormData.getAll", arguments.length, 1);
name = String(name);
const values = [];
for (const entry of this[dataSymbol]) {
if (entry[0] === name) {
values.push(entry[1]);
}
}
return values;
}
/**
* @param {string} name
* @returns {FormDataEntryValue | null}
*/
2020-09-18 09:20:55 -04:00
get(name) {
requiredArguments("FormData.get", arguments.length, 1);
name = String(name);
for (const entry of this[dataSymbol]) {
if (entry[0] === name) {
return entry[1];
}
}
return null;
}
/**
* @param {string} name
* @returns {boolean}
*/
2020-09-18 09:20:55 -04:00
has(name) {
requiredArguments("FormData.has", arguments.length, 1);
name = String(name);
return this[dataSymbol].some((entry) => entry[0] === name);
}
/**
* @param {string} name
* @param {string | Blob} value
* @param {string} [filename]
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
set(name, value, filename) {
requiredArguments("FormData.set", arguments.length, 2);
name = String(name);
// If there are any entries in the context objects entry list whose name
// is name, replace the first such entry with entry and remove the others
let found = false;
let i = 0;
while (i < this[dataSymbol].length) {
if (this[dataSymbol][i][0] === name) {
if (!found) {
this[dataSymbol][i][1] = parseFormDataValue(value, filename);
found = true;
} else {
this[dataSymbol].splice(i, 1);
continue;
}
}
i++;
}
// Otherwise, append entry to the context objects entry list.
if (!found) {
this[dataSymbol].push([name, parseFormDataValue(value, filename)]);
}
}
get [Symbol.toStringTag]() {
return "FormData";
}
}
class FormData extends DomIterableMixin(FormDataBase, dataSymbol) {}
class MultipartBuilder {
/**
* @param {FormData} formData
* @param {string} [boundary]
*/
2020-09-18 09:20:55 -04:00
constructor(formData, boundary) {
this.formData = formData;
this.boundary = boundary ?? this.#createBoundary();
this.writer = new Buffer();
}
/**
* @returns {string}
*/
2020-09-18 09:20:55 -04:00
getContentType() {
return `multipart/form-data; boundary=${this.boundary}`;
}
/**
* @returns {Uint8Array}
*/
2020-09-18 09:20:55 -04:00
getBody() {
for (const [fieldName, fieldValue] of this.formData.entries()) {
if (fieldValue instanceof File) {
2020-09-18 09:20:55 -04:00
this.#writeFile(fieldName, fieldValue);
} else this.#writeField(fieldName, fieldValue);
}
this.writer.writeSync(encoder.encode(`\r\n--${this.boundary}--`));
return this.writer.bytes();
}
#createBoundary = () => {
return (
"----------" +
Array.from(Array(32))
.map(() => Math.random().toString(36)[2] || 0)
.join("")
);
};
/**
* @param {[string, string][]} headers
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
#writeHeaders = (headers) => {
let buf = this.writer.empty() ? "" : "\r\n";
buf += `--${this.boundary}\r\n`;
for (const [key, value] of headers) {
buf += `${key}: ${value}\r\n`;
}
buf += `\r\n`;
this.writer.writeSync(encoder.encode(buf));
2020-09-18 09:20:55 -04:00
};
/**
* @param {string} field
* @param {string} filename
* @param {string} [type]
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
#writeFileHeaders = (
field,
filename,
type,
) => {
/** @type {[string, string][]} */
2020-09-18 09:20:55 -04:00
const headers = [
[
"Content-Disposition",
`form-data; name="${field}"; filename="${filename}"`,
],
["Content-Type", type || "application/octet-stream"],
];
return this.#writeHeaders(headers);
};
/**
* @param {string} field
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
#writeFieldHeaders = (field) => {
/** @type {[string, string][]} */
2020-09-18 09:20:55 -04:00
const headers = [["Content-Disposition", `form-data; name="${field}"`]];
return this.#writeHeaders(headers);
};
/**
* @param {string} field
* @param {string} value
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
#writeField = (field, value) => {
this.#writeFieldHeaders(field);
this.writer.writeSync(encoder.encode(value));
};
/**
* @param {string} field
* @param {File} value
* @returns {void}
*/
2020-09-18 09:20:55 -04:00
#writeFile = (field, value) => {
this.#writeFileHeaders(field, value.name, value.type);
this.writer.writeSync(value[_byteSequence]);
2020-09-18 09:20:55 -04:00
};
}
class MultipartParser {
/**
* @param {Uint8Array} body
* @param {string | undefined} boundary
*/
2020-09-18 09:20:55 -04:00
constructor(body, boundary) {
if (!boundary) {
throw new TypeError("multipart/form-data must provide a boundary");
}
this.boundary = `--${boundary}`;
this.body = body;
this.boundaryChars = encoder.encode(this.boundary);
}
/**
* @param {string} headersText
* @returns {{ headers: Headers, disposition: Map<string, string> }}
*/
2020-09-18 09:20:55 -04:00
#parseHeaders = (headersText) => {
const headers = new Headers();
const rawHeaders = headersText.split("\r\n");
for (const rawHeader of rawHeaders) {
const sepIndex = rawHeader.indexOf(":");
if (sepIndex < 0) {
continue; // Skip this header
}
const key = rawHeader.slice(0, sepIndex);
const value = rawHeader.slice(sepIndex + 1);
headers.set(key, value);
}
return {
headers,
disposition: getHeaderValueParams(
headers.get("Content-Disposition") ?? "",
),
};
};
/**
* @returns {FormData}
*/
2020-09-18 09:20:55 -04:00
parse() {
const formData = new FormData();
let headerText = "";
let boundaryIndex = 0;
let state = 0;
let fileStart = 0;
for (let i = 0; i < this.body.length; i++) {
const byte = this.body[i];
const prevByte = this.body[i - 1];
const isNewLine = byte === LF && prevByte === CR;
if (state === 1 || state === 2 || state == 3) {
headerText += String.fromCharCode(byte);
}
if (state === 0 && isNewLine) {
state = 1;
} else if (state === 1 && isNewLine) {
state = 2;
const headersDone = this.body[i + 1] === CR &&
this.body[i + 2] === LF;
if (headersDone) {
state = 3;
}
} else if (state === 2 && isNewLine) {
state = 3;
} else if (state === 3 && isNewLine) {
state = 4;
fileStart = i + 1;
} else if (state === 4) {
if (this.boundaryChars[boundaryIndex] !== byte) {
boundaryIndex = 0;
} else {
boundaryIndex++;
}
if (boundaryIndex >= this.boundary.length) {
const { headers, disposition } = this.#parseHeaders(headerText);
const content = this.body.subarray(
fileStart,
i - boundaryIndex - 1,
);
// https://fetch.spec.whatwg.org/#ref-for-dom-body-formdata
const filename = disposition.get("filename");
const name = disposition.get("name");
state = 5;
// Reset
boundaryIndex = 0;
headerText = "";
if (!name) {
continue; // Skip, unknown name
}
if (filename) {
const blob = new Blob([content], {
type: headers.get("Content-Type") || "application/octet-stream",
});
formData.append(name, blob, filename);
} else {
formData.append(name, decoder.decode(content));
}
}
} else if (state === 5 && isNewLine) {
state = 1;
}
}
return formData;
}
}
/**
* @param {string} name
* @param {BodyInit | null} bodySource
*/
function validateBodyType(name, bodySource) {
2020-09-18 09:20:55 -04:00
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}`,
2020-09-18 09:20:55 -04:00
);
}
/**
* @param {ReadableStreamReader<Uint8Array>} stream
* @param {number} [size]
*/
2020-09-18 09:20:55 -04:00
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
*/
2020-09-18 09:20:55 -04:00
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");
2020-09-18 09:20:55 -04:00
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;
2020-09-18 09:20:55 -04:00
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`,
);
}
2020-09-18 09:20:55 -04:00
this.#stream = new ReadableStream({
/**
* @param {ReadableStreamDefaultController<Uint8Array>} controller
*/
start(controller) {
controller.enqueue(new Uint8Array(buf));
controller.close();
},
});
2020-09-18 09:20:55 -04:00
}
}
2020-09-18 09:20:55 -04:00
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;
}
2020-09-18 09:20:55 -04:00
}
return this.#bodySource;
2020-09-18 09:20:55 -04:00
}
get bodyUsed() {
if (this.body && isReadableStreamDisturbed(this.body)) {
return true;
}
return false;
}
set bodyUsed(_) {
// this is a noop per spec
}
/** @returns {Promise<Blob>} */
2020-09-18 09:20:55 -04:00
async blob() {
return new Blob([await this.arrayBuffer()], {
type: this.#contentType,
});
}
// ref: https://fetch.spec.whatwg.org/#body-mixin
/** @returns {Promise<FormData>} */
2020-09-18 09:20:55 -04:00
async formData() {
const formData = new FormData();
if (hasHeaderValueOf(this.#contentType, "multipart/form-data")) {
const params = getHeaderValueParams(this.#contentType);
// ref: https://tools.ietf.org/html/rfc2046#section-5.1
const boundary = params.get("boundary");
const body = new Uint8Array(await this.arrayBuffer());
const multipartParser = new MultipartParser(body, boundary);
return multipartParser.parse();
} else if (
hasHeaderValueOf(this.#contentType, "application/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),
);
}
2020-09-18 09:20:55 -04:00
}
});
} catch (e) {
throw new TypeError("Invalid form urlencoded format");
}
return formData;
} else {
throw new TypeError("Invalid form data");
}
}
/** @returns {Promise<string>} */
2020-09-18 09:20:55 -04:00
async text() {
if (typeof this.#bodySource === "string") {
return this.#bodySource;
2020-09-18 09:20:55 -04:00
}
const ab = await this.arrayBuffer();
const decoder = new TextDecoder("utf-8");
return decoder.decode(ab);
}
/** @returns {Promise<any>} */
2020-09-18 09:20:55 -04:00
async json() {
const raw = await this.text();
return JSON.parse(raw);
}
/** @returns {Promise<ArrayBuffer>} */
2020-09-18 09:20:55 -04:00
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);
2020-09-18 09:20:55 -04:00
}
return Promise.resolve(bodyToArrayBuffer(this.#bodySource));
2020-09-18 09:20:55 -04:00
}
}
/**
* @param {Deno.CreateHttpClientOptions} options
* @returns {HttpClient}
*/
2020-09-18 09:20:55 -04:00
function createHttpClient(options) {
return new HttpClient(core.jsonOpSync("op_create_http_client", options));
2020-09-18 09:20:55 -04:00
}
class HttpClient {
/**
* @param {number} rid
*/
2020-09-18 09:20:55 -04:00
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 {Uint8Array | null} body
* @returns {{requestRid: number, requestBodyRid: number | null}}
*/
2020-09-18 09:20:55 -04:00
function opFetch(args, body) {
let zeroCopy;
if (body != null) {
zeroCopy = new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
}
return core.jsonOpSync("op_fetch", args, ...(zeroCopy ? [zeroCopy] : []));
}
2020-09-18 09:20:55 -04:00
/**
* @param {{rid: number}} args
* @returns {Promise<{status: number, statusText: string, headers: Record<string,string[]>, url: string, responseRid: number}>}
*/
function opFetchSend(args) {
return core.jsonOpAsync("op_fetch_send", args);
}
/**
* @param {{rid: number}} args
* @param {Uint8Array} body
* @returns {Promise<void>}
*/
function opFetchRequestWrite(args, body) {
const zeroCopy = new Uint8Array(
body.buffer,
body.byteOffset,
body.byteLength,
);
return core.jsonOpAsync("op_fetch_request_write", args, zeroCopy);
2020-09-18 09:20:55 -04:00
}
const NULL_BODY_STATUS = [101, 204, 205, 304];
const REDIRECT_STATUS = [301, 302, 303, 307, 308];
/**
* @param {string} s
* @returns {string}
*/
2020-09-18 09:20:55 -04:00
function byteUpperCase(s) {
return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) {
return c.toUpperCase();
});
}
/**
* @param {string} m
* @returns {string}
*/
2020-09-18 09:20:55 -04:00
function normalizeMethod(m) {
const u = byteUpperCase(m);
if (
u === "DELETE" ||
u === "GET" ||
u === "HEAD" ||
u === "OPTIONS" ||
u === "POST" ||
u === "PUT"
) {
return u;
}
return m;
}
class Request extends Body {
/** @type {string} */
#method = "GET";
/** @type {string} */
#url = "";
/** @type {Headers} */
#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.
2020-09-18 09:20:55 -04:00
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) {
2020-09-18 09:20:55 -04:00
if (input.bodyUsed) {
throw TypeError(BodyUsedError);
}
b = input[teeBody]();
2020-09-18 09:20:55 -04:00
} else if (typeof input === "object" && "body" in input && input.body) {
if (input.bodyUsed) {
throw TypeError(BodyUsedError);
}
b = input.body;
} else {
b = "";
}
let headers;
// prefer headers from init
if (init.headers) {
headers = new Headers(init.headers);
} else if (input instanceof Request) {
headers = input.headers;
} else {
headers = new Headers();
}
const contentType = headers.get("content-type") || "";
super(b, { contentType });
this.#headers = headers;
2020-09-18 09:20:55 -04:00
if (input instanceof Request) {
if (input.bodyUsed) {
throw TypeError(BodyUsedError);
}
this.#method = input.method;
this.#url = input.url;
this.#headers = new Headers(input.headers);
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;
}
2020-09-18 09:20:55 -04:00
}
if (init && "method" in init && init.method) {
this.#method = normalizeMethod(init.method);
2020-09-18 09:20:55 -04:00
}
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]();
2020-09-18 09:20:55 -04:00
return new Request(this.url, {
body,
2020-09-18 09:20:55 -04:00
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() {
return this.#headers;
}
set headers(_) {
// can not set headers
}
get credentials() {
return this.#credentials;
}
set credentials(_) {
// can not set credentials
}
2020-09-18 09:20:55 -04:00
}
const responseData = new WeakMap();
class Response extends Body {
/**
* @param {BodyInit | null} body
* @param {ResponseInit} [init]
*/
2020-09-18 09:20:55 -04:00
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]);
}
}
} 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]);
}
}
/* 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;
}
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]();
2020-09-18 09:20:55 -04:00
return new Response(body, {
2020-09-18 09:20:55 -04:00
status: this.status,
statusText: this.statusText,
headers: new Headers(headersList),
});
}
/**
* @param {string } url
* @param {number} status
*/
static redirect(url, status = 302) {
2020-09-18 09:20:55 -04:00
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)]],
2020-09-18 09:20:55 -04:00
});
}
}
/** @type {string | null} */
let baseUrl = null;
/** @param {string} href */
function setBaseUrl(href) {
baseUrl = href;
}
/**
* @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}>}
*/
async function sendFetchReq(url, method, headers, body, clientRid) {
/** @type {[string, string][]} */
2020-09-18 09:20:55 -04:00
let headerArray = [];
if (headers) {
headerArray = Array.from(headers.entries());
}
const { requestRid, requestBodyRid } = opFetch(
{
method,
url,
baseUrl,
headers: headerArray,
clientRid,
hasBody: !!body,
},
body instanceof Uint8Array ? body : null,
);
if (requestBodyRid) {
if (!(body instanceof ReadableStream)) {
throw new TypeError("Unreachable state (body is not ReadableStream).");
}
const writer = new WritableStream({
/**
* @param {Uint8Array} chunk
* @param {WritableStreamDefaultController} controller
*/
async write(chunk, controller) {
try {
await opFetchRequestWrite({ rid: requestBodyRid }, chunk);
} catch (err) {
controller.error(err);
}
},
close() {
core.close(requestBodyRid);
},
});
body.pipeTo(writer);
}
return await opFetchSend({ rid: requestRid });
2020-09-18 09:20:55 -04:00
}
/**
* @param {Request | URL | string} input
* @param {RequestInit & {client: Deno.HttpClient}} [init]
* @returns {Promise<Response>}
*/
2020-09-18 09:20:55 -04:00
async function fetch(input, init) {
/** @type {string | null} */
2020-09-18 09:20:55 -04:00
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
2020-09-18 09:20:55 -04:00
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];
2020-09-18 09:20:55 -04:00
contentType = init.body.type;
} else if (init.body instanceof FormData) {
let boundary;
if (headers.has("content-type")) {
const params = getHeaderValueParams("content-type");
boundary = params.get("boundary");
}
const multipartBuilder = new MultipartBuilder(
init.body,
boundary,
);
body = multipartBuilder.getBody();
contentType = multipartBuilder.getContentType();
} else if (init.body instanceof ReadableStream) {
body = init.body;
2020-09-18 09:20:55 -04:00
}
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;
if (input.body) {
body = input.body;
2020-09-18 09:20:55 -04:00
}
}
let responseBody;
let responseInit = {};
while (remRedirectCount) {
const fetchResp = await sendFetchReq(
2020-09-18 09:20:55 -04:00
url,
method ?? "GET",
headers ?? new Headers(),
2020-09-18 09:20:55 -04:00
body,
clientRid,
);
const rid = fetchResp.responseRid;
2020-09-18 09:20:55 -04:00
if (
NULL_BODY_STATUS.includes(fetchResp.status) ||
REDIRECT_STATUS.includes(fetchResp.status)
2020-09-18 09:20:55 -04:00
) {
// We won't use body of received response, so close it now
// otherwise it will be kept in resource table.
core.close(rid);
2020-09-18 09:20:55 -04:00
responseBody = null;
} else {
responseBody = new ReadableStream({
type: "bytes",
/** @param {ReadableStreamDefaultController<Uint8Array>} controller */
2020-09-18 09:20:55 -04:00
async pull(controller) {
try {
const chunk = new Uint8Array(16 * 1024 + 256);
const { read } = await core.jsonOpAsync(
"op_fetch_response_read",
{ rid },
chunk,
);
if (read != 0) {
if (chunk.length == read) {
controller.enqueue(chunk);
} else {
controller.enqueue(chunk.subarray(0, read));
}
} else {
2020-09-18 09:20:55 -04:00
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,
2020-09-18 09:20:55 -04:00
};
responseData.set(responseInit, {
redirected,
rid: fetchResp.responseRid,
status: fetchResp.status,
url: fetchResp.url,
2020-09-18 09:20:55 -04:00
});
const response = new Response(responseBody, responseInit);
if (REDIRECT_STATUS.includes(fetchResp.status)) {
2020-09-18 09:20:55 -04:00
// 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;
2020-09-18 09:20:55 -04:00
case "follow":
// fallthrough
default: {
/** @type {string | null} */
2020-09-18 09:20:55 -04:00
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;
2020-09-18 09:20:55 -04:00
}
url = redirectUrl;
redirected = true;
remRedirectCount--;
}
2020-09-18 09:20:55 -04:00
}
} else {
return response;
}
}
responseData.set(responseInit, {
type: "error",
redirected: false,
url: "",
});
return new Response(null, responseInit);
}
window.__bootstrap.fetch = {
FormData,
setBaseUrl,
2020-09-18 09:20:55 -04:00
fetch,
Request,
Response,
HttpClient,
createHttpClient,
fastBody,
dontValidateUrl,
2020-09-18 09:20:55 -04:00
};
})(this);