123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852 |
- /**
- * Fetching content from the web is environment-specific, so Ethers
- * provides an abstraction that each environment can implement to provide
- * this service.
- *
- * On [Node.js](link-node), the ``http`` and ``https`` libs are used to
- * create a request object, register event listeners and process data
- * and populate the [[FetchResponse]].
- *
- * In a browser, the [DOM fetch](link-js-fetch) is used, and the resulting
- * ``Promise`` is waited on to retrieve the payload.
- *
- * The [[FetchRequest]] is responsible for handling many common situations,
- * such as redirects, server throttling, authentication, etc.
- *
- * It also handles common gateways, such as IPFS and data URIs.
- *
- * @_section api/utils/fetching:Fetching Web Content [about-fetch]
- */
- import { decodeBase64, encodeBase64 } from "./base64.js";
- import { hexlify } from "./data.js";
- import { assert, assertArgument } from "./errors.js";
- import { defineProperties } from "./properties.js";
- import { toUtf8Bytes, toUtf8String } from "./utf8.js";
- import { createGetUrl } from "./geturl.js";
- const MAX_ATTEMPTS = 12;
- const SLOT_INTERVAL = 250;
- // The global FetchGetUrlFunc implementation.
- let defaultGetUrlFunc = createGetUrl();
- const reData = new RegExp("^data:([^;:]*)?(;base64)?,(.*)$", "i");
- const reIpfs = new RegExp("^ipfs:/\/(ipfs/)?(.*)$", "i");
- // If locked, new Gateways cannot be added
- let locked = false;
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
- async function dataGatewayFunc(url, signal) {
- try {
- const match = url.match(reData);
- if (!match) {
- throw new Error("invalid data");
- }
- return new FetchResponse(200, "OK", {
- "content-type": (match[1] || "text/plain"),
- }, (match[2] ? decodeBase64(match[3]) : unpercent(match[3])));
- }
- catch (error) {
- return new FetchResponse(599, "BAD REQUEST (invalid data: URI)", {}, null, new FetchRequest(url));
- }
- }
- /**
- * Returns a [[FetchGatewayFunc]] for fetching content from a standard
- * IPFS gateway hosted at %%baseUrl%%.
- */
- function getIpfsGatewayFunc(baseUrl) {
- async function gatewayIpfs(url, signal) {
- try {
- const match = url.match(reIpfs);
- if (!match) {
- throw new Error("invalid link");
- }
- return new FetchRequest(`${baseUrl}${match[2]}`);
- }
- catch (error) {
- return new FetchResponse(599, "BAD REQUEST (invalid IPFS URI)", {}, null, new FetchRequest(url));
- }
- }
- return gatewayIpfs;
- }
- const Gateways = {
- "data": dataGatewayFunc,
- "ipfs": getIpfsGatewayFunc("https:/\/gateway.ipfs.io/ipfs/")
- };
- const fetchSignals = new WeakMap();
- /**
- * @_ignore
- */
- export class FetchCancelSignal {
- #listeners;
- #cancelled;
- constructor(request) {
- this.#listeners = [];
- this.#cancelled = false;
- fetchSignals.set(request, () => {
- if (this.#cancelled) {
- return;
- }
- this.#cancelled = true;
- for (const listener of this.#listeners) {
- setTimeout(() => { listener(); }, 0);
- }
- this.#listeners = [];
- });
- }
- addListener(listener) {
- assert(!this.#cancelled, "singal already cancelled", "UNSUPPORTED_OPERATION", {
- operation: "fetchCancelSignal.addCancelListener"
- });
- this.#listeners.push(listener);
- }
- get cancelled() { return this.#cancelled; }
- checkSignal() {
- assert(!this.cancelled, "cancelled", "CANCELLED", {});
- }
- }
- // Check the signal, throwing if it is cancelled
- function checkSignal(signal) {
- if (signal == null) {
- throw new Error("missing signal; should not happen");
- }
- signal.checkSignal();
- return signal;
- }
- /**
- * Represents a request for a resource using a URI.
- *
- * By default, the supported schemes are ``HTTP``, ``HTTPS``, ``data:``,
- * and ``IPFS:``.
- *
- * Additional schemes can be added globally using [[registerGateway]].
- *
- * @example:
- * req = new FetchRequest("https://www.ricmoo.com")
- * resp = await req.send()
- * resp.body.length
- * //_result:
- */
- export class FetchRequest {
- #allowInsecure;
- #gzip;
- #headers;
- #method;
- #timeout;
- #url;
- #body;
- #bodyType;
- #creds;
- // Hooks
- #preflight;
- #process;
- #retry;
- #signal;
- #throttle;
- #getUrlFunc;
- /**
- * The fetch URL to request.
- */
- get url() { return this.#url; }
- set url(url) {
- this.#url = String(url);
- }
- /**
- * The fetch body, if any, to send as the request body. //(default: null)//
- *
- * When setting a body, the intrinsic ``Content-Type`` is automatically
- * set and will be used if **not overridden** by setting a custom
- * header.
- *
- * If %%body%% is null, the body is cleared (along with the
- * intrinsic ``Content-Type``).
- *
- * If %%body%% is a string, the intrinsic ``Content-Type`` is set to
- * ``text/plain``.
- *
- * If %%body%% is a Uint8Array, the intrinsic ``Content-Type`` is set to
- * ``application/octet-stream``.
- *
- * If %%body%% is any other object, the intrinsic ``Content-Type`` is
- * set to ``application/json``.
- */
- get body() {
- if (this.#body == null) {
- return null;
- }
- return new Uint8Array(this.#body);
- }
- set body(body) {
- if (body == null) {
- this.#body = undefined;
- this.#bodyType = undefined;
- }
- else if (typeof (body) === "string") {
- this.#body = toUtf8Bytes(body);
- this.#bodyType = "text/plain";
- }
- else if (body instanceof Uint8Array) {
- this.#body = body;
- this.#bodyType = "application/octet-stream";
- }
- else if (typeof (body) === "object") {
- this.#body = toUtf8Bytes(JSON.stringify(body));
- this.#bodyType = "application/json";
- }
- else {
- throw new Error("invalid body");
- }
- }
- /**
- * Returns true if the request has a body.
- */
- hasBody() {
- return (this.#body != null);
- }
- /**
- * The HTTP method to use when requesting the URI. If no method
- * has been explicitly set, then ``GET`` is used if the body is
- * null and ``POST`` otherwise.
- */
- get method() {
- if (this.#method) {
- return this.#method;
- }
- if (this.hasBody()) {
- return "POST";
- }
- return "GET";
- }
- set method(method) {
- if (method == null) {
- method = "";
- }
- this.#method = String(method).toUpperCase();
- }
- /**
- * The headers that will be used when requesting the URI. All
- * keys are lower-case.
- *
- * This object is a copy, so any changes will **NOT** be reflected
- * in the ``FetchRequest``.
- *
- * To set a header entry, use the ``setHeader`` method.
- */
- get headers() {
- const headers = Object.assign({}, this.#headers);
- if (this.#creds) {
- headers["authorization"] = `Basic ${encodeBase64(toUtf8Bytes(this.#creds))}`;
- }
- ;
- if (this.allowGzip) {
- headers["accept-encoding"] = "gzip";
- }
- if (headers["content-type"] == null && this.#bodyType) {
- headers["content-type"] = this.#bodyType;
- }
- if (this.body) {
- headers["content-length"] = String(this.body.length);
- }
- return headers;
- }
- /**
- * Get the header for %%key%%, ignoring case.
- */
- getHeader(key) {
- return this.headers[key.toLowerCase()];
- }
- /**
- * Set the header for %%key%% to %%value%%. All values are coerced
- * to a string.
- */
- setHeader(key, value) {
- this.#headers[String(key).toLowerCase()] = String(value);
- }
- /**
- * Clear all headers, resetting all intrinsic headers.
- */
- clearHeaders() {
- this.#headers = {};
- }
- [Symbol.iterator]() {
- const headers = this.headers;
- const keys = Object.keys(headers);
- let index = 0;
- return {
- next: () => {
- if (index < keys.length) {
- const key = keys[index++];
- return {
- value: [key, headers[key]], done: false
- };
- }
- return { value: undefined, done: true };
- }
- };
- }
- /**
- * The value that will be sent for the ``Authorization`` header.
- *
- * To set the credentials, use the ``setCredentials`` method.
- */
- get credentials() {
- return this.#creds || null;
- }
- /**
- * Sets an ``Authorization`` for %%username%% with %%password%%.
- */
- setCredentials(username, password) {
- assertArgument(!username.match(/:/), "invalid basic authentication username", "username", "[REDACTED]");
- this.#creds = `${username}:${password}`;
- }
- /**
- * Enable and request gzip-encoded responses. The response will
- * automatically be decompressed. //(default: true)//
- */
- get allowGzip() {
- return this.#gzip;
- }
- set allowGzip(value) {
- this.#gzip = !!value;
- }
- /**
- * Allow ``Authentication`` credentials to be sent over insecure
- * channels. //(default: false)//
- */
- get allowInsecureAuthentication() {
- return !!this.#allowInsecure;
- }
- set allowInsecureAuthentication(value) {
- this.#allowInsecure = !!value;
- }
- /**
- * The timeout (in milliseconds) to wait for a complete response.
- * //(default: 5 minutes)//
- */
- get timeout() { return this.#timeout; }
- set timeout(timeout) {
- assertArgument(timeout >= 0, "timeout must be non-zero", "timeout", timeout);
- this.#timeout = timeout;
- }
- /**
- * This function is called prior to each request, for example
- * during a redirection or retry in case of server throttling.
- *
- * This offers an opportunity to populate headers or update
- * content before sending a request.
- */
- get preflightFunc() {
- return this.#preflight || null;
- }
- set preflightFunc(preflight) {
- this.#preflight = preflight;
- }
- /**
- * This function is called after each response, offering an
- * opportunity to provide client-level throttling or updating
- * response data.
- *
- * Any error thrown in this causes the ``send()`` to throw.
- *
- * To schedule a retry attempt (assuming the maximum retry limit
- * has not been reached), use [[response.throwThrottleError]].
- */
- get processFunc() {
- return this.#process || null;
- }
- set processFunc(process) {
- this.#process = process;
- }
- /**
- * This function is called on each retry attempt.
- */
- get retryFunc() {
- return this.#retry || null;
- }
- set retryFunc(retry) {
- this.#retry = retry;
- }
- /**
- * This function is called to fetch content from HTTP and
- * HTTPS URLs and is platform specific (e.g. nodejs vs
- * browsers).
- *
- * This is by default the currently registered global getUrl
- * function, which can be changed using [[registerGetUrl]].
- * If this has been set, setting is to ``null`` will cause
- * this FetchRequest (and any future clones) to revert back to
- * using the currently registered global getUrl function.
- *
- * Setting this is generally not necessary, but may be useful
- * for developers that wish to intercept requests or to
- * configurege a proxy or other agent.
- */
- get getUrlFunc() {
- return this.#getUrlFunc || defaultGetUrlFunc;
- }
- set getUrlFunc(value) {
- this.#getUrlFunc = value;
- }
- /**
- * Create a new FetchRequest instance with default values.
- *
- * Once created, each property may be set before issuing a
- * ``.send()`` to make the request.
- */
- constructor(url) {
- this.#url = String(url);
- this.#allowInsecure = false;
- this.#gzip = true;
- this.#headers = {};
- this.#method = "";
- this.#timeout = 300000;
- this.#throttle = {
- slotInterval: SLOT_INTERVAL,
- maxAttempts: MAX_ATTEMPTS
- };
- this.#getUrlFunc = null;
- }
- toString() {
- return `<FetchRequest method=${JSON.stringify(this.method)} url=${JSON.stringify(this.url)} headers=${JSON.stringify(this.headers)} body=${this.#body ? hexlify(this.#body) : "null"}>`;
- }
- /**
- * Update the throttle parameters used to determine maximum
- * attempts and exponential-backoff properties.
- */
- setThrottleParams(params) {
- if (params.slotInterval != null) {
- this.#throttle.slotInterval = params.slotInterval;
- }
- if (params.maxAttempts != null) {
- this.#throttle.maxAttempts = params.maxAttempts;
- }
- }
- async #send(attempt, expires, delay, _request, _response) {
- if (attempt >= this.#throttle.maxAttempts) {
- return _response.makeServerError("exceeded maximum retry limit");
- }
- assert(getTime() <= expires, "timeout", "TIMEOUT", {
- operation: "request.send", reason: "timeout", request: _request
- });
- if (delay > 0) {
- await wait(delay);
- }
- let req = this.clone();
- const scheme = (req.url.split(":")[0] || "").toLowerCase();
- // Process any Gateways
- if (scheme in Gateways) {
- const result = await Gateways[scheme](req.url, checkSignal(_request.#signal));
- if (result instanceof FetchResponse) {
- let response = result;
- if (this.processFunc) {
- checkSignal(_request.#signal);
- try {
- response = await this.processFunc(req, response);
- }
- catch (error) {
- // Something went wrong during processing; throw a 5xx server error
- if (error.throttle == null || typeof (error.stall) !== "number") {
- response.makeServerError("error in post-processing function", error).assertOk();
- }
- // Ignore throttling
- }
- }
- return response;
- }
- req = result;
- }
- // We have a preflight function; update the request
- if (this.preflightFunc) {
- req = await this.preflightFunc(req);
- }
- const resp = await this.getUrlFunc(req, checkSignal(_request.#signal));
- let response = new FetchResponse(resp.statusCode, resp.statusMessage, resp.headers, resp.body, _request);
- if (response.statusCode === 301 || response.statusCode === 302) {
- // Redirect
- try {
- const location = response.headers.location || "";
- return req.redirect(location).#send(attempt + 1, expires, 0, _request, response);
- }
- catch (error) { }
- // Things won't get any better on another attempt; abort
- return response;
- }
- else if (response.statusCode === 429) {
- // Throttle
- if (this.retryFunc == null || (await this.retryFunc(req, response, attempt))) {
- const retryAfter = response.headers["retry-after"];
- let delay = this.#throttle.slotInterval * Math.trunc(Math.random() * Math.pow(2, attempt));
- if (typeof (retryAfter) === "string" && retryAfter.match(/^[1-9][0-9]*$/)) {
- delay = parseInt(retryAfter);
- }
- return req.clone().#send(attempt + 1, expires, delay, _request, response);
- }
- }
- if (this.processFunc) {
- checkSignal(_request.#signal);
- try {
- response = await this.processFunc(req, response);
- }
- catch (error) {
- // Something went wrong during processing; throw a 5xx server error
- if (error.throttle == null || typeof (error.stall) !== "number") {
- response.makeServerError("error in post-processing function", error).assertOk();
- }
- // Throttle
- let delay = this.#throttle.slotInterval * Math.trunc(Math.random() * Math.pow(2, attempt));
- ;
- if (error.stall >= 0) {
- delay = error.stall;
- }
- return req.clone().#send(attempt + 1, expires, delay, _request, response);
- }
- }
- return response;
- }
- /**
- * Resolves to the response by sending the request.
- */
- send() {
- assert(this.#signal == null, "request already sent", "UNSUPPORTED_OPERATION", { operation: "fetchRequest.send" });
- this.#signal = new FetchCancelSignal(this);
- return this.#send(0, getTime() + this.timeout, 0, this, new FetchResponse(0, "", {}, null, this));
- }
- /**
- * Cancels the inflight response, causing a ``CANCELLED``
- * error to be rejected from the [[send]].
- */
- cancel() {
- assert(this.#signal != null, "request has not been sent", "UNSUPPORTED_OPERATION", { operation: "fetchRequest.cancel" });
- const signal = fetchSignals.get(this);
- if (!signal) {
- throw new Error("missing signal; should not happen");
- }
- signal();
- }
- /**
- * Returns a new [[FetchRequest]] that represents the redirection
- * to %%location%%.
- */
- redirect(location) {
- // Redirection; for now we only support absolute locations
- const current = this.url.split(":")[0].toLowerCase();
- const target = location.split(":")[0].toLowerCase();
- // Don't allow redirecting:
- // - non-GET requests
- // - downgrading the security (e.g. https => http)
- // - to non-HTTP (or non-HTTPS) protocols [this could be relaxed?]
- assert(this.method === "GET" && (current !== "https" || target !== "http") && location.match(/^https?:/), `unsupported redirect`, "UNSUPPORTED_OPERATION", {
- operation: `redirect(${this.method} ${JSON.stringify(this.url)} => ${JSON.stringify(location)})`
- });
- // Create a copy of this request, with a new URL
- const req = new FetchRequest(location);
- req.method = "GET";
- req.allowGzip = this.allowGzip;
- req.timeout = this.timeout;
- req.#headers = Object.assign({}, this.#headers);
- if (this.#body) {
- req.#body = new Uint8Array(this.#body);
- }
- req.#bodyType = this.#bodyType;
- // Do not forward credentials unless on the same domain; only absolute
- //req.allowInsecure = false;
- // paths are currently supported; may want a way to specify to forward?
- //setStore(req.#props, "creds", getStore(this.#pros, "creds"));
- return req;
- }
- /**
- * Create a new copy of this request.
- */
- clone() {
- const clone = new FetchRequest(this.url);
- // Preserve "default method" (i.e. null)
- clone.#method = this.#method;
- // Preserve "default body" with type, copying the Uint8Array is present
- if (this.#body) {
- clone.#body = this.#body;
- }
- clone.#bodyType = this.#bodyType;
- // Preserve "default headers"
- clone.#headers = Object.assign({}, this.#headers);
- // Credentials is readonly, so we copy internally
- clone.#creds = this.#creds;
- if (this.allowGzip) {
- clone.allowGzip = true;
- }
- clone.timeout = this.timeout;
- if (this.allowInsecureAuthentication) {
- clone.allowInsecureAuthentication = true;
- }
- clone.#preflight = this.#preflight;
- clone.#process = this.#process;
- clone.#retry = this.#retry;
- clone.#throttle = Object.assign({}, this.#throttle);
- clone.#getUrlFunc = this.#getUrlFunc;
- return clone;
- }
- /**
- * Locks all static configuration for gateways and FetchGetUrlFunc
- * registration.
- */
- static lockConfig() {
- locked = true;
- }
- /**
- * Get the current Gateway function for %%scheme%%.
- */
- static getGateway(scheme) {
- return Gateways[scheme.toLowerCase()] || null;
- }
- /**
- * Use the %%func%% when fetching URIs using %%scheme%%.
- *
- * This method affects all requests globally.
- *
- * If [[lockConfig]] has been called, no change is made and this
- * throws.
- */
- static registerGateway(scheme, func) {
- scheme = scheme.toLowerCase();
- if (scheme === "http" || scheme === "https") {
- throw new Error(`cannot intercept ${scheme}; use registerGetUrl`);
- }
- if (locked) {
- throw new Error("gateways locked");
- }
- Gateways[scheme] = func;
- }
- /**
- * Use %%getUrl%% when fetching URIs over HTTP and HTTPS requests.
- *
- * This method affects all requests globally.
- *
- * If [[lockConfig]] has been called, no change is made and this
- * throws.
- */
- static registerGetUrl(getUrl) {
- if (locked) {
- throw new Error("gateways locked");
- }
- defaultGetUrlFunc = getUrl;
- }
- /**
- * Creates a getUrl function that fetches content from HTTP and
- * HTTPS URLs.
- *
- * The available %%options%% are dependent on the platform
- * implementation of the default getUrl function.
- *
- * This is not generally something that is needed, but is useful
- * when trying to customize simple behaviour when fetching HTTP
- * content.
- */
- static createGetUrlFunc(options) {
- return createGetUrl(options);
- }
- /**
- * Creates a function that can "fetch" data URIs.
- *
- * Note that this is automatically done internally to support
- * data URIs, so it is not necessary to register it.
- *
- * This is not generally something that is needed, but may
- * be useful in a wrapper to perfom custom data URI functionality.
- */
- static createDataGateway() {
- return dataGatewayFunc;
- }
- /**
- * Creates a function that will fetch IPFS (unvalidated) from
- * a custom gateway baseUrl.
- *
- * The default IPFS gateway used internally is
- * ``"https:/\/gateway.ipfs.io/ipfs/"``.
- */
- static createIpfsGatewayFunc(baseUrl) {
- return getIpfsGatewayFunc(baseUrl);
- }
- }
- ;
- /**
- * The response for a FetchRequest.
- */
- export class FetchResponse {
- #statusCode;
- #statusMessage;
- #headers;
- #body;
- #request;
- #error;
- toString() {
- return `<FetchResponse status=${this.statusCode} body=${this.#body ? hexlify(this.#body) : "null"}>`;
- }
- /**
- * The response status code.
- */
- get statusCode() { return this.#statusCode; }
- /**
- * The response status message.
- */
- get statusMessage() { return this.#statusMessage; }
- /**
- * The response headers. All keys are lower-case.
- */
- get headers() { return Object.assign({}, this.#headers); }
- /**
- * The response body, or ``null`` if there was no body.
- */
- get body() {
- return (this.#body == null) ? null : new Uint8Array(this.#body);
- }
- /**
- * The response body as a UTF-8 encoded string, or the empty
- * string (i.e. ``""``) if there was no body.
- *
- * An error is thrown if the body is invalid UTF-8 data.
- */
- get bodyText() {
- try {
- return (this.#body == null) ? "" : toUtf8String(this.#body);
- }
- catch (error) {
- assert(false, "response body is not valid UTF-8 data", "UNSUPPORTED_OPERATION", {
- operation: "bodyText", info: { response: this }
- });
- }
- }
- /**
- * The response body, decoded as JSON.
- *
- * An error is thrown if the body is invalid JSON-encoded data
- * or if there was no body.
- */
- get bodyJson() {
- try {
- return JSON.parse(this.bodyText);
- }
- catch (error) {
- assert(false, "response body is not valid JSON", "UNSUPPORTED_OPERATION", {
- operation: "bodyJson", info: { response: this }
- });
- }
- }
- [Symbol.iterator]() {
- const headers = this.headers;
- const keys = Object.keys(headers);
- let index = 0;
- return {
- next: () => {
- if (index < keys.length) {
- const key = keys[index++];
- return {
- value: [key, headers[key]], done: false
- };
- }
- return { value: undefined, done: true };
- }
- };
- }
- constructor(statusCode, statusMessage, headers, body, request) {
- this.#statusCode = statusCode;
- this.#statusMessage = statusMessage;
- this.#headers = Object.keys(headers).reduce((accum, k) => {
- accum[k.toLowerCase()] = String(headers[k]);
- return accum;
- }, {});
- this.#body = ((body == null) ? null : new Uint8Array(body));
- this.#request = (request || null);
- this.#error = { message: "" };
- }
- /**
- * Return a Response with matching headers and body, but with
- * an error status code (i.e. 599) and %%message%% with an
- * optional %%error%%.
- */
- makeServerError(message, error) {
- let statusMessage;
- if (!message) {
- message = `${this.statusCode} ${this.statusMessage}`;
- statusMessage = `CLIENT ESCALATED SERVER ERROR (${message})`;
- }
- else {
- statusMessage = `CLIENT ESCALATED SERVER ERROR (${this.statusCode} ${this.statusMessage}; ${message})`;
- }
- const response = new FetchResponse(599, statusMessage, this.headers, this.body, this.#request || undefined);
- response.#error = { message, error };
- return response;
- }
- /**
- * If called within a [request.processFunc](FetchRequest-processFunc)
- * call, causes the request to retry as if throttled for %%stall%%
- * milliseconds.
- */
- throwThrottleError(message, stall) {
- if (stall == null) {
- stall = -1;
- }
- else {
- assertArgument(Number.isInteger(stall) && stall >= 0, "invalid stall timeout", "stall", stall);
- }
- const error = new Error(message || "throttling requests");
- defineProperties(error, { stall, throttle: true });
- throw error;
- }
- /**
- * Get the header value for %%key%%, ignoring case.
- */
- getHeader(key) {
- return this.headers[key.toLowerCase()];
- }
- /**
- * Returns true if the response has a body.
- */
- hasBody() {
- return (this.#body != null);
- }
- /**
- * The request made for this response.
- */
- get request() { return this.#request; }
- /**
- * Returns true if this response was a success statusCode.
- */
- ok() {
- return (this.#error.message === "" && this.statusCode >= 200 && this.statusCode < 300);
- }
- /**
- * Throws a ``SERVER_ERROR`` if this response is not ok.
- */
- assertOk() {
- if (this.ok()) {
- return;
- }
- let { message, error } = this.#error;
- if (message === "") {
- message = `server response ${this.statusCode} ${this.statusMessage}`;
- }
- let requestUrl = null;
- if (this.request) {
- requestUrl = this.request.url;
- }
- let responseBody = null;
- try {
- if (this.#body) {
- responseBody = toUtf8String(this.#body);
- }
- }
- catch (e) { }
- assert(false, message, "SERVER_ERROR", {
- request: (this.request || "unknown request"), response: this, error,
- info: {
- requestUrl, responseBody,
- responseStatus: `${this.statusCode} ${this.statusMessage}`
- }
- });
- }
- }
- function getTime() { return (new Date()).getTime(); }
- function unpercent(value) {
- return toUtf8Bytes(value.replace(/%([0-9a-f][0-9a-f])/gi, (all, code) => {
- return String.fromCharCode(parseInt(code, 16));
- }));
- }
- function wait(delay) {
- return new Promise((resolve) => setTimeout(resolve, delay));
- }
- //# sourceMappingURL=fetch.js.map
|