1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-12-27 17:49:08 -05:00
denoland-deno/op_crates/fetch/26_fetch.js
Luca Casonato 353e79c796
chore: align FormData to spec (#10169)
This PR aligns `FormData` to spec. All WPT tests are passing.
2021-04-14 22:49:16 +02:00

1145 lines
31 KiB
JavaScript

// 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="./11_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;
// 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));
}
}
/**
* @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 {Uint8Array | null} body
* @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);
}
/**
* @param {number} rid
* @returns {Promise<{status: number, statusText: string, headers: Record<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) {
const zeroCopy = new Uint8Array(
body.buffer,
body.byteOffset,
body.byteLength,
);
return core.opAsync("op_fetch_request_write", rid, zeroCopy);
}
const NULL_BODY_STATUS = [101, 204, 205, 304];
const REDIRECT_STATUS = [301, 302, 303, 307, 308];
/**
* @param {string} s
* @returns {string}
*/
function byteUpperCase(s) {
return String(s).replace(/[a-z]/g, function byteUpperCaseReplace(c) {
return c.toUpperCase();
});
}
/**
* @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 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];
}
}
} else {
headers = new Headers(init.headers);
contentType = headers.get("content-type") || "";
}
} 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]);
}
}
} 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]();
return new Response(body, {
status: this.status,
statusText: this.statusText,
headers: new Headers(headersList),
});
}
/**
* @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;
}
/**
* @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][]} */
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(requestBodyRid, chunk);
} catch (err) {
controller.error(err);
}
},
close() {
core.close(requestBodyRid);
},
});
body.pipeTo(writer);
}
return await opFetchSend(requestRid);
}
/**
* @param {Request | URL | string} input
* @param {RequestInit & {client: Deno.HttpClient}} [init]
* @returns {Promise<Response>}
*/
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;
if (input.body) {
body = input.body;
}
}
let responseBody;
let responseInit = {};
while (remRedirectCount) {
const fetchResp = await sendFetchReq(
url,
method ?? "GET",
headers ?? new Headers(),
body,
clientRid,
);
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);
}
window.__bootstrap.fetch = {
FormData,
setBaseUrl,
fetch,
Request,
Response,
HttpClient,
createHttpClient,
fastBody,
dontValidateUrl,
lazyHeaders,
};
})(this);