// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // @ts-check /// /// /// /// /// /// /// /// 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( `Invalid status text: "${init.statusText}"`, ); } // 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: ${status}`); } 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 = { __proto__: null }) { 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, };