| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970 | 
							- /**
 
-  *  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";
 
- /**
 
-  *  An environment's implementation of ``getUrl`` must return this type.
 
-  */
 
- export type GetUrlResponse = {
 
-     statusCode: number,
 
-     statusMessage: string,
 
-     headers: Record<string, string>,
 
-     body: null | Uint8Array
 
- };
 
- /**
 
-  *  This can be used to control how throttling is handled in
 
-  *  [[FetchRequest-setThrottleParams]].
 
-  */
 
- export type FetchThrottleParams = {
 
-     maxAttempts?: number;
 
-     slotInterval?: number;
 
- };
 
- /**
 
-  *  Called before any network request, allowing updated headers (e.g. Bearer tokens), etc.
 
-  */
 
- export type FetchPreflightFunc = (req: FetchRequest) => Promise<FetchRequest>;
 
- /**
 
-  *  Called on the response, allowing client-based throttling logic or post-processing.
 
-  */
 
- export type FetchProcessFunc = (req: FetchRequest, resp: FetchResponse) => Promise<FetchResponse>;
 
- /**
 
-  *  Called prior to each retry; return true to retry, false to abort.
 
-  */
 
- export type FetchRetryFunc = (req: FetchRequest, resp: FetchResponse, attempt: number) => Promise<boolean>;
 
- /**
 
-  *  Called on Gateway URLs.
 
-  */
 
- export type FetchGatewayFunc = (url: string, signal?: FetchCancelSignal) => Promise<FetchRequest | FetchResponse>;
 
- /**
 
-  *  Used to perform a fetch; use this to override the underlying network
 
-  *  fetch layer. In NodeJS, the default uses the "http" and "https" libraries
 
-  *  and in the browser ``fetch`` is used. If you wish to use Axios, this is
 
-  *  how you would register it.
 
-  */
 
- export type FetchGetUrlFunc = (req: FetchRequest, signal?: FetchCancelSignal) => Promise<GetUrlResponse>;
 
- const MAX_ATTEMPTS = 12;
 
- const SLOT_INTERVAL = 250;
 
- // The global FetchGetUrlFunc implementation.
 
- let defaultGetUrlFunc: FetchGetUrlFunc = 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: string, signal?: FetchCancelSignal): Promise<FetchResponse> {
 
-     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: string): FetchGatewayFunc {
 
-     async function gatewayIpfs(url: string, signal?: FetchCancelSignal): Promise<FetchRequest | FetchResponse> {
 
-         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: Record<string, FetchGatewayFunc> = {
 
-     "data": dataGatewayFunc,
 
-     "ipfs": getIpfsGatewayFunc("https:/\/gateway.ipfs.io/ipfs/")
 
- };
 
- const fetchSignals: WeakMap<FetchRequest, () => void> = new WeakMap();
 
- /**
 
-  *  @_ignore
 
-  */
 
- export class FetchCancelSignal {
 
-     #listeners: Array<() => void>;
 
-     #cancelled: boolean;
 
-     constructor(request: FetchRequest) {
 
-         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: () => void): void {
 
-         assert(!this.#cancelled, "singal already cancelled", "UNSUPPORTED_OPERATION", {
 
-             operation: "fetchCancelSignal.addCancelListener"
 
-         });
 
-         this.#listeners.push(listener);
 
-     }
 
-     get cancelled(): boolean { return this.#cancelled; }
 
-     checkSignal(): void {
 
-         assert(!this.cancelled, "cancelled", "CANCELLED", { });
 
-     }
 
- }
 
- // Check the signal, throwing if it is cancelled
 
- function checkSignal(signal?: FetchCancelSignal): FetchCancelSignal {
 
-     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 implements Iterable<[ key: string, value: string ]> {
 
-     #allowInsecure: boolean;
 
-     #gzip: boolean;
 
-     #headers: Record<string, string>;
 
-     #method: string;
 
-     #timeout: number;
 
-     #url: string;
 
-     #body?: Uint8Array;
 
-     #bodyType?: string;
 
-     #creds?: string;
 
-     // Hooks
 
-     #preflight?: null | FetchPreflightFunc;
 
-     #process?: null | FetchProcessFunc;
 
-     #retry?: null | FetchRetryFunc;
 
-     #signal?: FetchCancelSignal;
 
-     #throttle: Required<FetchThrottleParams>;
 
-     #getUrlFunc: null | FetchGetUrlFunc;
 
-     /**
 
-      *  The fetch URL to request.
 
-      */
 
-     get url(): string { return this.#url; }
 
-     set url(url: string) {
 
-         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(): null | Uint8Array {
 
-         if (this.#body == null) { return null; }
 
-         return new Uint8Array(this.#body);
 
-     }
 
-     set body(body: null | string | Readonly<object> | Readonly<Uint8Array>) {
 
-         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(): this is (FetchRequest & { body: Uint8Array }) {
 
-         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(): string {
 
-         if (this.#method) { return this.#method; }
 
-         if (this.hasBody()) { return "POST"; }
 
-         return "GET";
 
-     }
 
-     set method(method: null | string) {
 
-         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(): Record<string, string> {
 
-         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: string): string {
 
-         return this.headers[key.toLowerCase()];
 
-     }
 
-     /**
 
-      *  Set the header for %%key%% to %%value%%. All values are coerced
 
-      *  to a string.
 
-      */
 
-     setHeader(key: string, value: string | number): void {
 
-         this.#headers[String(key).toLowerCase()] = String(value);
 
-     }
 
-     /**
 
-      *  Clear all headers, resetting all intrinsic headers.
 
-      */
 
-     clearHeaders(): void {
 
-         this.#headers = { };
 
-     }
 
-     [Symbol.iterator](): Iterator<[ key: string, value: string ]> {
 
-         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(): null | string {
 
-         return this.#creds || null;
 
-     }
 
-     /**
 
-      *  Sets an ``Authorization`` for %%username%% with %%password%%.
 
-      */
 
-     setCredentials(username: string, password: string): void {
 
-         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(): boolean {
 
-         return this.#gzip;
 
-     }
 
-     set allowGzip(value: boolean) {
 
-         this.#gzip = !!value;
 
-     }
 
-     /**
 
-      *  Allow ``Authentication`` credentials to be sent over insecure
 
-      *  channels. //(default: false)//
 
-      */
 
-     get allowInsecureAuthentication(): boolean {
 
-         return !!this.#allowInsecure;
 
-     }
 
-     set allowInsecureAuthentication(value: boolean) {
 
-         this.#allowInsecure = !!value;
 
-     }
 
-     /**
 
-      *  The timeout (in milliseconds) to wait for a complete response.
 
-      *  //(default: 5 minutes)//
 
-      */
 
-     get timeout(): number { return this.#timeout; }
 
-     set timeout(timeout: number) {
 
-         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(): null | FetchPreflightFunc {
 
-         return this.#preflight || null;
 
-     }
 
-     set preflightFunc(preflight: null | FetchPreflightFunc) {
 
-         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(): null | FetchProcessFunc {
 
-         return this.#process || null;
 
-     }
 
-     set processFunc(process: null | FetchProcessFunc) {
 
-         this.#process = process;
 
-     }
 
-     /**
 
-      *  This function is called on each retry attempt.
 
-      */
 
-     get retryFunc(): null | FetchRetryFunc {
 
-         return this.#retry || null;
 
-     }
 
-     set retryFunc(retry: null | FetchRetryFunc) {
 
-         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(): FetchGetUrlFunc {
 
-         return this.#getUrlFunc || defaultGetUrlFunc;
 
-     }
 
-     set getUrlFunc(value: null | FetchGetUrlFunc) {
 
-         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: string) {
 
-         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(): string {
 
-         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: FetchThrottleParams): void {
 
-         if (params.slotInterval != null) {
 
-             this.#throttle.slotInterval = params.slotInterval;
 
-         }
 
-         if (params.maxAttempts != null) {
 
-             this.#throttle.maxAttempts = params.maxAttempts;
 
-         }
 
-     }
 
-     async #send(attempt: number, expires: number, delay: number, _request: FetchRequest, _response: FetchResponse): Promise<FetchResponse> {
 
-         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: any) {
 
-                         // 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: any) {
 
-                 // 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(): Promise<FetchResponse> {
 
-         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(): void {
 
-         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: string): FetchRequest {
 
-         // 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(): FetchRequest {
 
-         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(): void {
 
-         locked = true;
 
-     }
 
-     /**
 
-      *  Get the current Gateway function for %%scheme%%.
 
-      */
 
-     static getGateway(scheme: string): null | FetchGatewayFunc {
 
-         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: string, func: FetchGatewayFunc): void {
 
-         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: FetchGetUrlFunc): void {
 
-         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?: Record<string, any>): FetchGetUrlFunc {
 
-         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(): FetchGatewayFunc {
 
-         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: string): FetchGatewayFunc {
 
-         return getIpfsGatewayFunc(baseUrl);
 
-     }
 
- }
 
- interface ThrottleError extends Error {
 
-     stall: number;
 
-     throttle: true;
 
- };
 
- /**
 
-  *  The response for a FetchRequest.
 
-  */
 
- export class FetchResponse implements Iterable<[ key: string, value: string ]> {
 
-     #statusCode: number;
 
-     #statusMessage: string;
 
-     #headers: Record<string, string>;
 
-     #body: null | Readonly<Uint8Array>;
 
-     #request: null | FetchRequest;
 
-     #error: { error?: Error, message: string };
 
-     toString(): string {
 
-         return `<FetchResponse status=${ this.statusCode } body=${ this.#body ? hexlify(this.#body): "null" }>`;
 
-     }
 
-     /**
 
-      *  The response status code.
 
-      */
 
-     get statusCode(): number { return this.#statusCode; }
 
-     /**
 
-      *  The response status message.
 
-      */
 
-     get statusMessage(): string { return this.#statusMessage; }
 
-     /**
 
-      *  The response headers. All keys are lower-case.
 
-      */
 
-     get headers(): Record<string, string> { return Object.assign({ }, this.#headers); }
 
-     /**
 
-      *  The response body, or ``null`` if there was no body.
 
-      */
 
-     get body(): null | Readonly<Uint8Array> {
 
-         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(): string {
 
-         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(): any {
 
-         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](): Iterator<[ key: string, value: string ]> {
 
-         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: number, statusMessage: string, headers: Readonly<Record<string, string>>, body: null | Uint8Array, request?: FetchRequest) {
 
-         this.#statusCode = statusCode;
 
-         this.#statusMessage = statusMessage;
 
-         this.#headers = Object.keys(headers).reduce((accum, k) => {
 
-             accum[k.toLowerCase()] = String(headers[k]);
 
-             return accum;
 
-         }, <Record<string, string>>{ });
 
-         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?: string, error?: Error): FetchResponse {
 
-         let statusMessage: string;
 
-         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?: string, stall?: number): never {
 
-         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(<ThrottleError>error, { stall, throttle: true });
 
-         throw error;
 
-     }
 
-     /**
 
-      *  Get the header value for %%key%%, ignoring case.
 
-      */
 
-     getHeader(key: string): string {
 
-         return this.headers[key.toLowerCase()];
 
-     }
 
-     /**
 
-      *  Returns true if the response has a body.
 
-      */
 
-     hasBody(): this is (FetchResponse & { body: Uint8Array }) {
 
-         return (this.#body != null);
 
-     }
 
-     /**
 
-      *  The request made for this response.
 
-      */
 
-     get request(): null | FetchRequest { return this.#request; }
 
-     /**
 
-      *  Returns true if this response was a success statusCode.
 
-      */
 
-     ok(): boolean {
 
-         return (this.#error.message === "" && this.statusCode >= 200 && this.statusCode < 300);
 
-     }
 
-     /**
 
-      *  Throws a ``SERVER_ERROR`` if this response is not ok.
 
-      */
 
-     assertOk(): void {
 
-         if (this.ok()) { return; }
 
-         let { message, error } = this.#error;
 
-         if (message === "") {
 
-             message = `server response ${ this.statusCode } ${ this.statusMessage }`;
 
-         }
 
-         let requestUrl: null | string = null;
 
-         if (this.request) { requestUrl = this.request.url; }
 
-         let responseBody: null | string = 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(): number { return (new Date()).getTime(); }
 
- function unpercent(value: string): Uint8Array {
 
-     return toUtf8Bytes(value.replace(/%([0-9a-f][0-9a-f])/gi, (all, code) => {
 
-         return String.fromCharCode(parseInt(code, 16));
 
-     }));
 
- }
 
- function wait(delay: number): Promise<void> {
 
-     return new Promise((resolve) => setTimeout(resolve, delay));
 
- }
 
 
  |