deno/ext/fetch/23_response.js
Kenta Moriuchi 84db80642a
chore: replace 'call' to 'execute' in error messages (#22579)
Since both "call" and "execute" were used in error messages, I replaced them with "execute," which is more used.
2024-03-10 02:23:14 +00:00

517 lines
13 KiB
JavaScript

// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// @ts-check
/// <reference path="../webidl/internal.d.ts" />
/// <reference path="../web/internal.d.ts" />
/// <reference path="../url/internal.d.ts" />
/// <reference path="../web/lib.deno_web.d.ts" />
/// <reference path="./internal.d.ts" />
/// <reference path="../web/06_streams_types.d.ts" />
/// <reference path="./lib.deno_fetch.d.ts" />
/// <reference lib="esnext" />
import { core, primordials } from "ext:core/mod.js";
import * as webidl from "ext:deno_webidl/00_webidl.js";
import { createFilteredInspectProxy } from "ext:deno_console/01_console.js";
import {
byteLowerCase,
HTTP_TAB_OR_SPACE,
regexMatcher,
serializeJSValueToJSONString,
} from "ext:deno_web/00_infra.js";
import { extractBody, mixinBody } from "ext:deno_fetch/22_body.js";
import { getLocationHref } from "ext:deno_web/12_location.js";
import { extractMimeType } from "ext:deno_web/01_mimesniff.js";
import { URL } from "ext:deno_url/00_url.js";
import {
fillHeaders,
getDecodeSplitHeader,
guardFromHeaders,
headerListFromHeaders,
headersFromHeaderList,
} from "ext:deno_fetch/20_headers.js";
const {
ArrayPrototypeMap,
ArrayPrototypePush,
ObjectDefineProperties,
ObjectPrototypeIsPrototypeOf,
RangeError,
RegExpPrototypeExec,
SafeArrayIterator,
SafeRegExp,
Symbol,
SymbolFor,
TypeError,
} = primordials;
const VCHAR = ["\x21-\x7E"];
const OBS_TEXT = ["\x80-\xFF"];
const REASON_PHRASE = [
...new SafeArrayIterator(HTTP_TAB_OR_SPACE),
...new SafeArrayIterator(VCHAR),
...new SafeArrayIterator(OBS_TEXT),
];
const REASON_PHRASE_MATCHER = regexMatcher(REASON_PHRASE);
const REASON_PHRASE_RE = new SafeRegExp(`^[${REASON_PHRASE_MATCHER}]*$`);
const _response = Symbol("response");
const _headers = Symbol("headers");
const _mimeType = Symbol("mime type");
const _body = Symbol("body");
const _brand = webidl.brand;
/**
* @typedef InnerResponse
* @property {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"} type
* @property {() => string | null} url
* @property {string[]} urlList
* @property {number} status
* @property {string} statusMessage
* @property {[string, string][]} headerList
* @property {null | typeof __window.bootstrap.fetchBody.InnerBody} body
* @property {boolean} aborted
* @property {string} [error]
*/
/**
* @param {number} status
* @returns {boolean}
*/
function nullBodyStatus(status) {
return status === 101 || status === 204 || status === 205 || status === 304;
}
/**
* @param {number} status
* @returns {boolean}
*/
function redirectStatus(status) {
return status === 301 || status === 302 || status === 303 ||
status === 307 || status === 308;
}
/**
* https://fetch.spec.whatwg.org/#concept-response-clone
* @param {InnerResponse} response
* @returns {InnerResponse}
*/
function cloneInnerResponse(response) {
const urlList = [...new SafeArrayIterator(response.urlList)];
const headerList = ArrayPrototypeMap(
response.headerList,
(x) => [x[0], x[1]],
);
let body = null;
if (response.body !== null) {
body = response.body.clone();
}
return {
type: response.type,
body,
headerList,
urlList,
status: response.status,
statusMessage: response.statusMessage,
aborted: response.aborted,
url() {
if (this.urlList.length == 0) return null;
return this.urlList[this.urlList.length - 1];
},
};
}
/**
* @returns {InnerResponse}
*/
function newInnerResponse(status = 200, statusMessage = "") {
return {
type: "default",
body: null,
headerList: [],
urlList: [],
status,
statusMessage,
aborted: false,
url() {
if (this.urlList.length == 0) return null;
return this.urlList[this.urlList.length - 1];
},
};
}
/**
* @param {string} error
* @returns {InnerResponse}
*/
function networkError(error) {
const resp = newInnerResponse(0);
resp.type = "error";
resp.error = error;
return resp;
}
/**
* @returns {InnerResponse}
*/
function abortedNetworkError() {
const resp = networkError("aborted");
resp.aborted = true;
return resp;
}
/**
* https://fetch.spec.whatwg.org#initialize-a-response
* @param {Response} response
* @param {ResponseInit} init
* @param {{ body: fetchBody.InnerBody, contentType: string | null } | null} bodyWithType
*/
function initializeAResponse(response, init, bodyWithType) {
// 1.
if ((init.status < 200 || init.status > 599) && init.status != 101) {
throw new RangeError(
`The status provided (${init.status}) is not equal to 101 and outside the range [200, 599].`,
);
}
// 2.
if (
init.statusText &&
RegExpPrototypeExec(REASON_PHRASE_RE, init.statusText) === null
) {
throw new TypeError("Status text is not valid.");
}
// 3.
response[_response].status = init.status;
// 4.
response[_response].statusMessage = init.statusText;
// 5.
/** @type {headers.Headers} */
const headers = response[_headers];
if (init.headers) {
fillHeaders(headers, init.headers);
}
// 6.
if (bodyWithType !== null) {
if (nullBodyStatus(response[_response].status)) {
throw new TypeError(
"Response with null body status cannot have body",
);
}
const { body, contentType } = bodyWithType;
response[_response].body = body;
if (contentType !== null) {
let hasContentType = false;
const list = headerListFromHeaders(headers);
for (let i = 0; i < list.length; i++) {
if (byteLowerCase(list[i][0]) === "content-type") {
hasContentType = true;
break;
}
}
if (!hasContentType) {
ArrayPrototypePush(list, ["Content-Type", contentType]);
}
}
}
}
class Response {
get [_mimeType]() {
const values = getDecodeSplitHeader(
headerListFromHeaders(this[_headers]),
"Content-Type",
);
return extractMimeType(values);
}
get [_body]() {
return this[_response].body;
}
/**
* @returns {Response}
*/
static error() {
const inner = newInnerResponse(0);
inner.type = "error";
const response = webidl.createBranded(Response);
response[_response] = inner;
response[_headers] = headersFromHeaderList(
response[_response].headerList,
"immutable",
);
return response;
}
/**
* @param {string} url
* @param {number} status
* @returns {Response}
*/
static redirect(url, status = 302) {
const prefix = "Failed to execute 'Response.redirect'";
url = webidl.converters["USVString"](url, prefix, "Argument 1");
status = webidl.converters["unsigned short"](status, prefix, "Argument 2");
const baseURL = getLocationHref();
const parsedURL = new URL(url, baseURL);
if (!redirectStatus(status)) {
throw new RangeError("Invalid redirect status code.");
}
const inner = newInnerResponse(status);
inner.type = "default";
ArrayPrototypePush(inner.headerList, ["Location", parsedURL.href]);
const response = webidl.createBranded(Response);
response[_response] = inner;
response[_headers] = headersFromHeaderList(
response[_response].headerList,
"immutable",
);
return response;
}
/**
* @param {any} data
* @param {ResponseInit} init
* @returns {Response}
*/
static json(data = undefined, init = {}) {
const prefix = "Failed to execute 'Response.json'";
data = webidl.converters.any(data);
init = webidl.converters["ResponseInit_fast"](init, prefix, "Argument 2");
const str = serializeJSValueToJSONString(data);
const res = extractBody(str);
res.contentType = "application/json";
const response = webidl.createBranded(Response);
response[_response] = newInnerResponse();
response[_headers] = headersFromHeaderList(
response[_response].headerList,
"response",
);
initializeAResponse(response, init, res);
return response;
}
/**
* @param {BodyInit | null} body
* @param {ResponseInit} init
*/
constructor(body = null, init = undefined) {
if (body === _brand) {
this[_brand] = _brand;
return;
}
const prefix = "Failed to construct 'Response'";
body = webidl.converters["BodyInit_DOMString?"](body, prefix, "Argument 1");
init = webidl.converters["ResponseInit_fast"](init, prefix, "Argument 2");
this[_response] = newInnerResponse();
this[_headers] = headersFromHeaderList(
this[_response].headerList,
"response",
);
let bodyWithType = null;
if (body !== null) {
bodyWithType = extractBody(body);
}
initializeAResponse(this, init, bodyWithType);
this[_brand] = _brand;
}
/**
* @returns {"basic" | "cors" | "default" | "error" | "opaque" | "opaqueredirect"}
*/
get type() {
webidl.assertBranded(this, ResponsePrototype);
return this[_response].type;
}
/**
* @returns {string}
*/
get url() {
webidl.assertBranded(this, ResponsePrototype);
const url = this[_response].url();
if (url === null) return "";
const newUrl = new URL(url);
newUrl.hash = "";
return newUrl.href;
}
/**
* @returns {boolean}
*/
get redirected() {
webidl.assertBranded(this, ResponsePrototype);
return this[_response].urlList.length > 1;
}
/**
* @returns {number}
*/
get status() {
webidl.assertBranded(this, ResponsePrototype);
return this[_response].status;
}
/**
* @returns {boolean}
*/
get ok() {
webidl.assertBranded(this, ResponsePrototype);
const status = this[_response].status;
return status >= 200 && status <= 299;
}
/**
* @returns {string}
*/
get statusText() {
webidl.assertBranded(this, ResponsePrototype);
return this[_response].statusMessage;
}
/**
* @returns {Headers}
*/
get headers() {
webidl.assertBranded(this, ResponsePrototype);
return this[_headers];
}
/**
* @returns {Response}
*/
clone() {
webidl.assertBranded(this, ResponsePrototype);
if (this[_body] && this[_body].unusable()) {
throw new TypeError("Body is unusable.");
}
const second = webidl.createBranded(Response);
const newRes = cloneInnerResponse(this[_response]);
second[_response] = newRes;
second[_headers] = headersFromHeaderList(
newRes.headerList,
guardFromHeaders(this[_headers]),
);
return second;
}
[SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) {
return inspect(
createFilteredInspectProxy({
object: this,
evaluate: ObjectPrototypeIsPrototypeOf(ResponsePrototype, this),
keys: [
"body",
"bodyUsed",
"headers",
"ok",
"redirected",
"status",
"statusText",
"url",
],
}),
inspectOptions,
);
}
}
webidl.configureInterface(Response);
ObjectDefineProperties(Response, {
json: { enumerable: true },
redirect: { enumerable: true },
error: { enumerable: true },
});
const ResponsePrototype = Response.prototype;
mixinBody(ResponsePrototype, _body, _mimeType);
webidl.converters["Response"] = webidl.createInterfaceConverter(
"Response",
ResponsePrototype,
);
webidl.converters["ResponseInit"] = webidl.createDictionaryConverter(
"ResponseInit",
[{
key: "status",
defaultValue: 200,
converter: webidl.converters["unsigned short"],
}, {
key: "statusText",
defaultValue: "",
converter: webidl.converters["ByteString"],
}, {
key: "headers",
converter: webidl.converters["HeadersInit"],
}],
);
webidl.converters["ResponseInit_fast"] = function (
init,
prefix,
context,
opts,
) {
if (init === undefined || init === null) {
return { status: 200, statusText: "", headers: undefined };
}
// Fast path, if not a proxy
if (typeof init === "object" && !core.isProxy(init)) {
// Not a proxy fast path
const status = init.status !== undefined
? webidl.converters["unsigned short"](init.status)
: 200;
const statusText = init.statusText !== undefined
? webidl.converters["ByteString"](init.statusText)
: "";
const headers = init.headers !== undefined
? webidl.converters["HeadersInit"](init.headers)
: undefined;
return { status, statusText, headers };
}
// Slow default path
return webidl.converters["ResponseInit"](init, prefix, context, opts);
};
/**
* @param {Response} response
* @returns {InnerResponse}
*/
function toInnerResponse(response) {
return response[_response];
}
/**
* @param {InnerResponse} inner
* @param {"request" | "immutable" | "request-no-cors" | "response" | "none"} guard
* @returns {Response}
*/
function fromInnerResponse(inner, guard) {
const response = new Response(_brand);
response[_response] = inner;
response[_headers] = headersFromHeaderList(inner.headerList, guard);
return response;
}
export {
abortedNetworkError,
fromInnerResponse,
networkError,
newInnerResponse,
nullBodyStatus,
redirectStatus,
Response,
ResponsePrototype,
toInnerResponse,
};