fetch.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970
  1. /**
  2. * Fetching content from the web is environment-specific, so Ethers
  3. * provides an abstraction that each environment can implement to provide
  4. * this service.
  5. *
  6. * On [Node.js](link-node), the ``http`` and ``https`` libs are used to
  7. * create a request object, register event listeners and process data
  8. * and populate the [[FetchResponse]].
  9. *
  10. * In a browser, the [DOM fetch](link-js-fetch) is used, and the resulting
  11. * ``Promise`` is waited on to retrieve the payload.
  12. *
  13. * The [[FetchRequest]] is responsible for handling many common situations,
  14. * such as redirects, server throttling, authentication, etc.
  15. *
  16. * It also handles common gateways, such as IPFS and data URIs.
  17. *
  18. * @_section api/utils/fetching:Fetching Web Content [about-fetch]
  19. */
  20. import { decodeBase64, encodeBase64 } from "./base64.js";
  21. import { hexlify } from "./data.js";
  22. import { assert, assertArgument } from "./errors.js";
  23. import { defineProperties } from "./properties.js";
  24. import { toUtf8Bytes, toUtf8String } from "./utf8.js";
  25. import { createGetUrl } from "./geturl.js";
  26. /**
  27. * An environment's implementation of ``getUrl`` must return this type.
  28. */
  29. export type GetUrlResponse = {
  30. statusCode: number,
  31. statusMessage: string,
  32. headers: Record<string, string>,
  33. body: null | Uint8Array
  34. };
  35. /**
  36. * This can be used to control how throttling is handled in
  37. * [[FetchRequest-setThrottleParams]].
  38. */
  39. export type FetchThrottleParams = {
  40. maxAttempts?: number;
  41. slotInterval?: number;
  42. };
  43. /**
  44. * Called before any network request, allowing updated headers (e.g. Bearer tokens), etc.
  45. */
  46. export type FetchPreflightFunc = (req: FetchRequest) => Promise<FetchRequest>;
  47. /**
  48. * Called on the response, allowing client-based throttling logic or post-processing.
  49. */
  50. export type FetchProcessFunc = (req: FetchRequest, resp: FetchResponse) => Promise<FetchResponse>;
  51. /**
  52. * Called prior to each retry; return true to retry, false to abort.
  53. */
  54. export type FetchRetryFunc = (req: FetchRequest, resp: FetchResponse, attempt: number) => Promise<boolean>;
  55. /**
  56. * Called on Gateway URLs.
  57. */
  58. export type FetchGatewayFunc = (url: string, signal?: FetchCancelSignal) => Promise<FetchRequest | FetchResponse>;
  59. /**
  60. * Used to perform a fetch; use this to override the underlying network
  61. * fetch layer. In NodeJS, the default uses the "http" and "https" libraries
  62. * and in the browser ``fetch`` is used. If you wish to use Axios, this is
  63. * how you would register it.
  64. */
  65. export type FetchGetUrlFunc = (req: FetchRequest, signal?: FetchCancelSignal) => Promise<GetUrlResponse>;
  66. const MAX_ATTEMPTS = 12;
  67. const SLOT_INTERVAL = 250;
  68. // The global FetchGetUrlFunc implementation.
  69. let defaultGetUrlFunc: FetchGetUrlFunc = createGetUrl();
  70. const reData = new RegExp("^data:([^;:]*)?(;base64)?,(.*)$", "i");
  71. const reIpfs = new RegExp("^ipfs:/\/(ipfs/)?(.*)$", "i");
  72. // If locked, new Gateways cannot be added
  73. let locked = false;
  74. // https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URLs
  75. async function dataGatewayFunc(url: string, signal?: FetchCancelSignal): Promise<FetchResponse> {
  76. try {
  77. const match = url.match(reData);
  78. if (!match) { throw new Error("invalid data"); }
  79. return new FetchResponse(200, "OK", {
  80. "content-type": (match[1] || "text/plain"),
  81. }, (match[2] ? decodeBase64(match[3]): unpercent(match[3])));
  82. } catch (error) {
  83. return new FetchResponse(599, "BAD REQUEST (invalid data: URI)", { }, null, new FetchRequest(url));
  84. }
  85. }
  86. /**
  87. * Returns a [[FetchGatewayFunc]] for fetching content from a standard
  88. * IPFS gateway hosted at %%baseUrl%%.
  89. */
  90. function getIpfsGatewayFunc(baseUrl: string): FetchGatewayFunc {
  91. async function gatewayIpfs(url: string, signal?: FetchCancelSignal): Promise<FetchRequest | FetchResponse> {
  92. try {
  93. const match = url.match(reIpfs);
  94. if (!match) { throw new Error("invalid link"); }
  95. return new FetchRequest(`${ baseUrl }${ match[2] }`);
  96. } catch (error) {
  97. return new FetchResponse(599, "BAD REQUEST (invalid IPFS URI)", { }, null, new FetchRequest(url));
  98. }
  99. }
  100. return gatewayIpfs;
  101. }
  102. const Gateways: Record<string, FetchGatewayFunc> = {
  103. "data": dataGatewayFunc,
  104. "ipfs": getIpfsGatewayFunc("https:/\/gateway.ipfs.io/ipfs/")
  105. };
  106. const fetchSignals: WeakMap<FetchRequest, () => void> = new WeakMap();
  107. /**
  108. * @_ignore
  109. */
  110. export class FetchCancelSignal {
  111. #listeners: Array<() => void>;
  112. #cancelled: boolean;
  113. constructor(request: FetchRequest) {
  114. this.#listeners = [ ];
  115. this.#cancelled = false;
  116. fetchSignals.set(request, () => {
  117. if (this.#cancelled) { return; }
  118. this.#cancelled = true;
  119. for (const listener of this.#listeners) {
  120. setTimeout(() => { listener(); }, 0);
  121. }
  122. this.#listeners = [ ];
  123. });
  124. }
  125. addListener(listener: () => void): void {
  126. assert(!this.#cancelled, "singal already cancelled", "UNSUPPORTED_OPERATION", {
  127. operation: "fetchCancelSignal.addCancelListener"
  128. });
  129. this.#listeners.push(listener);
  130. }
  131. get cancelled(): boolean { return this.#cancelled; }
  132. checkSignal(): void {
  133. assert(!this.cancelled, "cancelled", "CANCELLED", { });
  134. }
  135. }
  136. // Check the signal, throwing if it is cancelled
  137. function checkSignal(signal?: FetchCancelSignal): FetchCancelSignal {
  138. if (signal == null) { throw new Error("missing signal; should not happen"); }
  139. signal.checkSignal();
  140. return signal;
  141. }
  142. /**
  143. * Represents a request for a resource using a URI.
  144. *
  145. * By default, the supported schemes are ``HTTP``, ``HTTPS``, ``data:``,
  146. * and ``IPFS:``.
  147. *
  148. * Additional schemes can be added globally using [[registerGateway]].
  149. *
  150. * @example:
  151. * req = new FetchRequest("https://www.ricmoo.com")
  152. * resp = await req.send()
  153. * resp.body.length
  154. * //_result:
  155. */
  156. export class FetchRequest implements Iterable<[ key: string, value: string ]> {
  157. #allowInsecure: boolean;
  158. #gzip: boolean;
  159. #headers: Record<string, string>;
  160. #method: string;
  161. #timeout: number;
  162. #url: string;
  163. #body?: Uint8Array;
  164. #bodyType?: string;
  165. #creds?: string;
  166. // Hooks
  167. #preflight?: null | FetchPreflightFunc;
  168. #process?: null | FetchProcessFunc;
  169. #retry?: null | FetchRetryFunc;
  170. #signal?: FetchCancelSignal;
  171. #throttle: Required<FetchThrottleParams>;
  172. #getUrlFunc: null | FetchGetUrlFunc;
  173. /**
  174. * The fetch URL to request.
  175. */
  176. get url(): string { return this.#url; }
  177. set url(url: string) {
  178. this.#url = String(url);
  179. }
  180. /**
  181. * The fetch body, if any, to send as the request body. //(default: null)//
  182. *
  183. * When setting a body, the intrinsic ``Content-Type`` is automatically
  184. * set and will be used if **not overridden** by setting a custom
  185. * header.
  186. *
  187. * If %%body%% is null, the body is cleared (along with the
  188. * intrinsic ``Content-Type``).
  189. *
  190. * If %%body%% is a string, the intrinsic ``Content-Type`` is set to
  191. * ``text/plain``.
  192. *
  193. * If %%body%% is a Uint8Array, the intrinsic ``Content-Type`` is set to
  194. * ``application/octet-stream``.
  195. *
  196. * If %%body%% is any other object, the intrinsic ``Content-Type`` is
  197. * set to ``application/json``.
  198. */
  199. get body(): null | Uint8Array {
  200. if (this.#body == null) { return null; }
  201. return new Uint8Array(this.#body);
  202. }
  203. set body(body: null | string | Readonly<object> | Readonly<Uint8Array>) {
  204. if (body == null) {
  205. this.#body = undefined;
  206. this.#bodyType = undefined;
  207. } else if (typeof(body) === "string") {
  208. this.#body = toUtf8Bytes(body);
  209. this.#bodyType = "text/plain";
  210. } else if (body instanceof Uint8Array) {
  211. this.#body = body;
  212. this.#bodyType = "application/octet-stream";
  213. } else if (typeof(body) === "object") {
  214. this.#body = toUtf8Bytes(JSON.stringify(body));
  215. this.#bodyType = "application/json";
  216. } else {
  217. throw new Error("invalid body");
  218. }
  219. }
  220. /**
  221. * Returns true if the request has a body.
  222. */
  223. hasBody(): this is (FetchRequest & { body: Uint8Array }) {
  224. return (this.#body != null);
  225. }
  226. /**
  227. * The HTTP method to use when requesting the URI. If no method
  228. * has been explicitly set, then ``GET`` is used if the body is
  229. * null and ``POST`` otherwise.
  230. */
  231. get method(): string {
  232. if (this.#method) { return this.#method; }
  233. if (this.hasBody()) { return "POST"; }
  234. return "GET";
  235. }
  236. set method(method: null | string) {
  237. if (method == null) { method = ""; }
  238. this.#method = String(method).toUpperCase();
  239. }
  240. /**
  241. * The headers that will be used when requesting the URI. All
  242. * keys are lower-case.
  243. *
  244. * This object is a copy, so any changes will **NOT** be reflected
  245. * in the ``FetchRequest``.
  246. *
  247. * To set a header entry, use the ``setHeader`` method.
  248. */
  249. get headers(): Record<string, string> {
  250. const headers = Object.assign({ }, this.#headers);
  251. if (this.#creds) {
  252. headers["authorization"] = `Basic ${ encodeBase64(toUtf8Bytes(this.#creds)) }`;
  253. };
  254. if (this.allowGzip) {
  255. headers["accept-encoding"] = "gzip";
  256. }
  257. if (headers["content-type"] == null && this.#bodyType) {
  258. headers["content-type"] = this.#bodyType;
  259. }
  260. if (this.body) { headers["content-length"] = String(this.body.length); }
  261. return headers;
  262. }
  263. /**
  264. * Get the header for %%key%%, ignoring case.
  265. */
  266. getHeader(key: string): string {
  267. return this.headers[key.toLowerCase()];
  268. }
  269. /**
  270. * Set the header for %%key%% to %%value%%. All values are coerced
  271. * to a string.
  272. */
  273. setHeader(key: string, value: string | number): void {
  274. this.#headers[String(key).toLowerCase()] = String(value);
  275. }
  276. /**
  277. * Clear all headers, resetting all intrinsic headers.
  278. */
  279. clearHeaders(): void {
  280. this.#headers = { };
  281. }
  282. [Symbol.iterator](): Iterator<[ key: string, value: string ]> {
  283. const headers = this.headers;
  284. const keys = Object.keys(headers);
  285. let index = 0;
  286. return {
  287. next: () => {
  288. if (index < keys.length) {
  289. const key = keys[index++];
  290. return {
  291. value: [ key, headers[key] ], done: false
  292. }
  293. }
  294. return { value: undefined, done: true };
  295. }
  296. };
  297. }
  298. /**
  299. * The value that will be sent for the ``Authorization`` header.
  300. *
  301. * To set the credentials, use the ``setCredentials`` method.
  302. */
  303. get credentials(): null | string {
  304. return this.#creds || null;
  305. }
  306. /**
  307. * Sets an ``Authorization`` for %%username%% with %%password%%.
  308. */
  309. setCredentials(username: string, password: string): void {
  310. assertArgument(!username.match(/:/), "invalid basic authentication username", "username", "[REDACTED]");
  311. this.#creds = `${ username }:${ password }`;
  312. }
  313. /**
  314. * Enable and request gzip-encoded responses. The response will
  315. * automatically be decompressed. //(default: true)//
  316. */
  317. get allowGzip(): boolean {
  318. return this.#gzip;
  319. }
  320. set allowGzip(value: boolean) {
  321. this.#gzip = !!value;
  322. }
  323. /**
  324. * Allow ``Authentication`` credentials to be sent over insecure
  325. * channels. //(default: false)//
  326. */
  327. get allowInsecureAuthentication(): boolean {
  328. return !!this.#allowInsecure;
  329. }
  330. set allowInsecureAuthentication(value: boolean) {
  331. this.#allowInsecure = !!value;
  332. }
  333. /**
  334. * The timeout (in milliseconds) to wait for a complete response.
  335. * //(default: 5 minutes)//
  336. */
  337. get timeout(): number { return this.#timeout; }
  338. set timeout(timeout: number) {
  339. assertArgument(timeout >= 0, "timeout must be non-zero", "timeout", timeout);
  340. this.#timeout = timeout;
  341. }
  342. /**
  343. * This function is called prior to each request, for example
  344. * during a redirection or retry in case of server throttling.
  345. *
  346. * This offers an opportunity to populate headers or update
  347. * content before sending a request.
  348. */
  349. get preflightFunc(): null | FetchPreflightFunc {
  350. return this.#preflight || null;
  351. }
  352. set preflightFunc(preflight: null | FetchPreflightFunc) {
  353. this.#preflight = preflight;
  354. }
  355. /**
  356. * This function is called after each response, offering an
  357. * opportunity to provide client-level throttling or updating
  358. * response data.
  359. *
  360. * Any error thrown in this causes the ``send()`` to throw.
  361. *
  362. * To schedule a retry attempt (assuming the maximum retry limit
  363. * has not been reached), use [[response.throwThrottleError]].
  364. */
  365. get processFunc(): null | FetchProcessFunc {
  366. return this.#process || null;
  367. }
  368. set processFunc(process: null | FetchProcessFunc) {
  369. this.#process = process;
  370. }
  371. /**
  372. * This function is called on each retry attempt.
  373. */
  374. get retryFunc(): null | FetchRetryFunc {
  375. return this.#retry || null;
  376. }
  377. set retryFunc(retry: null | FetchRetryFunc) {
  378. this.#retry = retry;
  379. }
  380. /**
  381. * This function is called to fetch content from HTTP and
  382. * HTTPS URLs and is platform specific (e.g. nodejs vs
  383. * browsers).
  384. *
  385. * This is by default the currently registered global getUrl
  386. * function, which can be changed using [[registerGetUrl]].
  387. * If this has been set, setting is to ``null`` will cause
  388. * this FetchRequest (and any future clones) to revert back to
  389. * using the currently registered global getUrl function.
  390. *
  391. * Setting this is generally not necessary, but may be useful
  392. * for developers that wish to intercept requests or to
  393. * configurege a proxy or other agent.
  394. */
  395. get getUrlFunc(): FetchGetUrlFunc {
  396. return this.#getUrlFunc || defaultGetUrlFunc;
  397. }
  398. set getUrlFunc(value: null | FetchGetUrlFunc) {
  399. this.#getUrlFunc = value;
  400. }
  401. /**
  402. * Create a new FetchRequest instance with default values.
  403. *
  404. * Once created, each property may be set before issuing a
  405. * ``.send()`` to make the request.
  406. */
  407. constructor(url: string) {
  408. this.#url = String(url);
  409. this.#allowInsecure = false;
  410. this.#gzip = true;
  411. this.#headers = { };
  412. this.#method = "";
  413. this.#timeout = 300000;
  414. this.#throttle = {
  415. slotInterval: SLOT_INTERVAL,
  416. maxAttempts: MAX_ATTEMPTS
  417. };
  418. this.#getUrlFunc = null;
  419. }
  420. toString(): string {
  421. return `<FetchRequest method=${ JSON.stringify(this.method) } url=${ JSON.stringify(this.url) } headers=${ JSON.stringify(this.headers) } body=${ this.#body ? hexlify(this.#body): "null" }>`;
  422. }
  423. /**
  424. * Update the throttle parameters used to determine maximum
  425. * attempts and exponential-backoff properties.
  426. */
  427. setThrottleParams(params: FetchThrottleParams): void {
  428. if (params.slotInterval != null) {
  429. this.#throttle.slotInterval = params.slotInterval;
  430. }
  431. if (params.maxAttempts != null) {
  432. this.#throttle.maxAttempts = params.maxAttempts;
  433. }
  434. }
  435. async #send(attempt: number, expires: number, delay: number, _request: FetchRequest, _response: FetchResponse): Promise<FetchResponse> {
  436. if (attempt >= this.#throttle.maxAttempts) {
  437. return _response.makeServerError("exceeded maximum retry limit");
  438. }
  439. assert(getTime() <= expires, "timeout", "TIMEOUT", {
  440. operation: "request.send", reason: "timeout", request: _request
  441. });
  442. if (delay > 0) { await wait(delay); }
  443. let req = this.clone();
  444. const scheme = (req.url.split(":")[0] || "").toLowerCase();
  445. // Process any Gateways
  446. if (scheme in Gateways) {
  447. const result = await Gateways[scheme](req.url, checkSignal(_request.#signal));
  448. if (result instanceof FetchResponse) {
  449. let response = result;
  450. if (this.processFunc) {
  451. checkSignal(_request.#signal);
  452. try {
  453. response = await this.processFunc(req, response);
  454. } catch (error: any) {
  455. // Something went wrong during processing; throw a 5xx server error
  456. if (error.throttle == null || typeof(error.stall) !== "number") {
  457. response.makeServerError("error in post-processing function", error).assertOk();
  458. }
  459. // Ignore throttling
  460. }
  461. }
  462. return response;
  463. }
  464. req = result;
  465. }
  466. // We have a preflight function; update the request
  467. if (this.preflightFunc) { req = await this.preflightFunc(req); }
  468. const resp = await this.getUrlFunc(req, checkSignal(_request.#signal));
  469. let response = new FetchResponse(resp.statusCode, resp.statusMessage, resp.headers, resp.body, _request);
  470. if (response.statusCode === 301 || response.statusCode === 302) {
  471. // Redirect
  472. try {
  473. const location = response.headers.location || "";
  474. return req.redirect(location).#send(attempt + 1, expires, 0, _request, response);
  475. } catch (error) { }
  476. // Things won't get any better on another attempt; abort
  477. return response;
  478. } else if (response.statusCode === 429) {
  479. // Throttle
  480. if (this.retryFunc == null || (await this.retryFunc(req, response, attempt))) {
  481. const retryAfter = response.headers["retry-after"];
  482. let delay = this.#throttle.slotInterval * Math.trunc(Math.random() * Math.pow(2, attempt));
  483. if (typeof(retryAfter) === "string" && retryAfter.match(/^[1-9][0-9]*$/)) {
  484. delay = parseInt(retryAfter);
  485. }
  486. return req.clone().#send(attempt + 1, expires, delay, _request, response);
  487. }
  488. }
  489. if (this.processFunc) {
  490. checkSignal(_request.#signal);
  491. try {
  492. response = await this.processFunc(req, response);
  493. } catch (error: any) {
  494. // Something went wrong during processing; throw a 5xx server error
  495. if (error.throttle == null || typeof(error.stall) !== "number") {
  496. response.makeServerError("error in post-processing function", error).assertOk();
  497. }
  498. // Throttle
  499. let delay = this.#throttle.slotInterval * Math.trunc(Math.random() * Math.pow(2, attempt));;
  500. if (error.stall >= 0) { delay = error.stall; }
  501. return req.clone().#send(attempt + 1, expires, delay, _request, response);
  502. }
  503. }
  504. return response;
  505. }
  506. /**
  507. * Resolves to the response by sending the request.
  508. */
  509. send(): Promise<FetchResponse> {
  510. assert(this.#signal == null, "request already sent", "UNSUPPORTED_OPERATION", { operation: "fetchRequest.send" });
  511. this.#signal = new FetchCancelSignal(this);
  512. return this.#send(0, getTime() + this.timeout, 0, this, new FetchResponse(0, "", { }, null, this));
  513. }
  514. /**
  515. * Cancels the inflight response, causing a ``CANCELLED``
  516. * error to be rejected from the [[send]].
  517. */
  518. cancel(): void {
  519. assert(this.#signal != null, "request has not been sent", "UNSUPPORTED_OPERATION", { operation: "fetchRequest.cancel" });
  520. const signal = fetchSignals.get(this);
  521. if (!signal) { throw new Error("missing signal; should not happen"); }
  522. signal();
  523. }
  524. /**
  525. * Returns a new [[FetchRequest]] that represents the redirection
  526. * to %%location%%.
  527. */
  528. redirect(location: string): FetchRequest {
  529. // Redirection; for now we only support absolute locations
  530. const current = this.url.split(":")[0].toLowerCase();
  531. const target = location.split(":")[0].toLowerCase();
  532. // Don't allow redirecting:
  533. // - non-GET requests
  534. // - downgrading the security (e.g. https => http)
  535. // - to non-HTTP (or non-HTTPS) protocols [this could be relaxed?]
  536. assert(this.method === "GET" && (current !== "https" || target !== "http") && location.match(/^https?:/), `unsupported redirect`, "UNSUPPORTED_OPERATION", {
  537. operation: `redirect(${ this.method } ${ JSON.stringify(this.url) } => ${ JSON.stringify(location) })`
  538. });
  539. // Create a copy of this request, with a new URL
  540. const req = new FetchRequest(location);
  541. req.method = "GET";
  542. req.allowGzip = this.allowGzip;
  543. req.timeout = this.timeout;
  544. req.#headers = Object.assign({ }, this.#headers);
  545. if (this.#body) { req.#body = new Uint8Array(this.#body); }
  546. req.#bodyType = this.#bodyType;
  547. // Do not forward credentials unless on the same domain; only absolute
  548. //req.allowInsecure = false;
  549. // paths are currently supported; may want a way to specify to forward?
  550. //setStore(req.#props, "creds", getStore(this.#pros, "creds"));
  551. return req;
  552. }
  553. /**
  554. * Create a new copy of this request.
  555. */
  556. clone(): FetchRequest {
  557. const clone = new FetchRequest(this.url);
  558. // Preserve "default method" (i.e. null)
  559. clone.#method = this.#method;
  560. // Preserve "default body" with type, copying the Uint8Array is present
  561. if (this.#body) { clone.#body = this.#body; }
  562. clone.#bodyType = this.#bodyType;
  563. // Preserve "default headers"
  564. clone.#headers = Object.assign({ }, this.#headers);
  565. // Credentials is readonly, so we copy internally
  566. clone.#creds = this.#creds;
  567. if (this.allowGzip) { clone.allowGzip = true; }
  568. clone.timeout = this.timeout;
  569. if (this.allowInsecureAuthentication) { clone.allowInsecureAuthentication = true; }
  570. clone.#preflight = this.#preflight;
  571. clone.#process = this.#process;
  572. clone.#retry = this.#retry;
  573. clone.#throttle = Object.assign({ }, this.#throttle);
  574. clone.#getUrlFunc = this.#getUrlFunc;
  575. return clone;
  576. }
  577. /**
  578. * Locks all static configuration for gateways and FetchGetUrlFunc
  579. * registration.
  580. */
  581. static lockConfig(): void {
  582. locked = true;
  583. }
  584. /**
  585. * Get the current Gateway function for %%scheme%%.
  586. */
  587. static getGateway(scheme: string): null | FetchGatewayFunc {
  588. return Gateways[scheme.toLowerCase()] || null;
  589. }
  590. /**
  591. * Use the %%func%% when fetching URIs using %%scheme%%.
  592. *
  593. * This method affects all requests globally.
  594. *
  595. * If [[lockConfig]] has been called, no change is made and this
  596. * throws.
  597. */
  598. static registerGateway(scheme: string, func: FetchGatewayFunc): void {
  599. scheme = scheme.toLowerCase();
  600. if (scheme === "http" || scheme === "https") {
  601. throw new Error(`cannot intercept ${ scheme }; use registerGetUrl`);
  602. }
  603. if (locked) { throw new Error("gateways locked"); }
  604. Gateways[scheme] = func;
  605. }
  606. /**
  607. * Use %%getUrl%% when fetching URIs over HTTP and HTTPS requests.
  608. *
  609. * This method affects all requests globally.
  610. *
  611. * If [[lockConfig]] has been called, no change is made and this
  612. * throws.
  613. */
  614. static registerGetUrl(getUrl: FetchGetUrlFunc): void {
  615. if (locked) { throw new Error("gateways locked"); }
  616. defaultGetUrlFunc = getUrl;
  617. }
  618. /**
  619. * Creates a getUrl function that fetches content from HTTP and
  620. * HTTPS URLs.
  621. *
  622. * The available %%options%% are dependent on the platform
  623. * implementation of the default getUrl function.
  624. *
  625. * This is not generally something that is needed, but is useful
  626. * when trying to customize simple behaviour when fetching HTTP
  627. * content.
  628. */
  629. static createGetUrlFunc(options?: Record<string, any>): FetchGetUrlFunc {
  630. return createGetUrl(options);
  631. }
  632. /**
  633. * Creates a function that can "fetch" data URIs.
  634. *
  635. * Note that this is automatically done internally to support
  636. * data URIs, so it is not necessary to register it.
  637. *
  638. * This is not generally something that is needed, but may
  639. * be useful in a wrapper to perfom custom data URI functionality.
  640. */
  641. static createDataGateway(): FetchGatewayFunc {
  642. return dataGatewayFunc;
  643. }
  644. /**
  645. * Creates a function that will fetch IPFS (unvalidated) from
  646. * a custom gateway baseUrl.
  647. *
  648. * The default IPFS gateway used internally is
  649. * ``"https:/\/gateway.ipfs.io/ipfs/"``.
  650. */
  651. static createIpfsGatewayFunc(baseUrl: string): FetchGatewayFunc {
  652. return getIpfsGatewayFunc(baseUrl);
  653. }
  654. }
  655. interface ThrottleError extends Error {
  656. stall: number;
  657. throttle: true;
  658. };
  659. /**
  660. * The response for a FetchRequest.
  661. */
  662. export class FetchResponse implements Iterable<[ key: string, value: string ]> {
  663. #statusCode: number;
  664. #statusMessage: string;
  665. #headers: Record<string, string>;
  666. #body: null | Readonly<Uint8Array>;
  667. #request: null | FetchRequest;
  668. #error: { error?: Error, message: string };
  669. toString(): string {
  670. return `<FetchResponse status=${ this.statusCode } body=${ this.#body ? hexlify(this.#body): "null" }>`;
  671. }
  672. /**
  673. * The response status code.
  674. */
  675. get statusCode(): number { return this.#statusCode; }
  676. /**
  677. * The response status message.
  678. */
  679. get statusMessage(): string { return this.#statusMessage; }
  680. /**
  681. * The response headers. All keys are lower-case.
  682. */
  683. get headers(): Record<string, string> { return Object.assign({ }, this.#headers); }
  684. /**
  685. * The response body, or ``null`` if there was no body.
  686. */
  687. get body(): null | Readonly<Uint8Array> {
  688. return (this.#body == null) ? null: new Uint8Array(this.#body);
  689. }
  690. /**
  691. * The response body as a UTF-8 encoded string, or the empty
  692. * string (i.e. ``""``) if there was no body.
  693. *
  694. * An error is thrown if the body is invalid UTF-8 data.
  695. */
  696. get bodyText(): string {
  697. try {
  698. return (this.#body == null) ? "": toUtf8String(this.#body);
  699. } catch (error) {
  700. assert(false, "response body is not valid UTF-8 data", "UNSUPPORTED_OPERATION", {
  701. operation: "bodyText", info: { response: this }
  702. });
  703. }
  704. }
  705. /**
  706. * The response body, decoded as JSON.
  707. *
  708. * An error is thrown if the body is invalid JSON-encoded data
  709. * or if there was no body.
  710. */
  711. get bodyJson(): any {
  712. try {
  713. return JSON.parse(this.bodyText);
  714. } catch (error) {
  715. assert(false, "response body is not valid JSON", "UNSUPPORTED_OPERATION", {
  716. operation: "bodyJson", info: { response: this }
  717. });
  718. }
  719. }
  720. [Symbol.iterator](): Iterator<[ key: string, value: string ]> {
  721. const headers = this.headers;
  722. const keys = Object.keys(headers);
  723. let index = 0;
  724. return {
  725. next: () => {
  726. if (index < keys.length) {
  727. const key = keys[index++];
  728. return {
  729. value: [ key, headers[key] ], done: false
  730. }
  731. }
  732. return { value: undefined, done: true };
  733. }
  734. };
  735. }
  736. constructor(statusCode: number, statusMessage: string, headers: Readonly<Record<string, string>>, body: null | Uint8Array, request?: FetchRequest) {
  737. this.#statusCode = statusCode;
  738. this.#statusMessage = statusMessage;
  739. this.#headers = Object.keys(headers).reduce((accum, k) => {
  740. accum[k.toLowerCase()] = String(headers[k]);
  741. return accum;
  742. }, <Record<string, string>>{ });
  743. this.#body = ((body == null) ? null: new Uint8Array(body));
  744. this.#request = (request || null);
  745. this.#error = { message: "" };
  746. }
  747. /**
  748. * Return a Response with matching headers and body, but with
  749. * an error status code (i.e. 599) and %%message%% with an
  750. * optional %%error%%.
  751. */
  752. makeServerError(message?: string, error?: Error): FetchResponse {
  753. let statusMessage: string;
  754. if (!message) {
  755. message = `${ this.statusCode } ${ this.statusMessage }`;
  756. statusMessage = `CLIENT ESCALATED SERVER ERROR (${ message })`;
  757. } else {
  758. statusMessage = `CLIENT ESCALATED SERVER ERROR (${ this.statusCode } ${ this.statusMessage }; ${ message })`;
  759. }
  760. const response = new FetchResponse(599, statusMessage, this.headers,
  761. this.body, this.#request || undefined);
  762. response.#error = { message, error };
  763. return response;
  764. }
  765. /**
  766. * If called within a [request.processFunc](FetchRequest-processFunc)
  767. * call, causes the request to retry as if throttled for %%stall%%
  768. * milliseconds.
  769. */
  770. throwThrottleError(message?: string, stall?: number): never {
  771. if (stall == null) {
  772. stall = -1;
  773. } else {
  774. assertArgument(Number.isInteger(stall) && stall >= 0, "invalid stall timeout", "stall", stall);
  775. }
  776. const error = new Error(message || "throttling requests");
  777. defineProperties(<ThrottleError>error, { stall, throttle: true });
  778. throw error;
  779. }
  780. /**
  781. * Get the header value for %%key%%, ignoring case.
  782. */
  783. getHeader(key: string): string {
  784. return this.headers[key.toLowerCase()];
  785. }
  786. /**
  787. * Returns true if the response has a body.
  788. */
  789. hasBody(): this is (FetchResponse & { body: Uint8Array }) {
  790. return (this.#body != null);
  791. }
  792. /**
  793. * The request made for this response.
  794. */
  795. get request(): null | FetchRequest { return this.#request; }
  796. /**
  797. * Returns true if this response was a success statusCode.
  798. */
  799. ok(): boolean {
  800. return (this.#error.message === "" && this.statusCode >= 200 && this.statusCode < 300);
  801. }
  802. /**
  803. * Throws a ``SERVER_ERROR`` if this response is not ok.
  804. */
  805. assertOk(): void {
  806. if (this.ok()) { return; }
  807. let { message, error } = this.#error;
  808. if (message === "") {
  809. message = `server response ${ this.statusCode } ${ this.statusMessage }`;
  810. }
  811. let requestUrl: null | string = null;
  812. if (this.request) { requestUrl = this.request.url; }
  813. let responseBody: null | string = null;
  814. try {
  815. if (this.#body) { responseBody = toUtf8String(this.#body); }
  816. } catch (e) { }
  817. assert(false, message, "SERVER_ERROR", {
  818. request: (this.request || "unknown request"), response: this, error,
  819. info: {
  820. requestUrl, responseBody,
  821. responseStatus: `${ this.statusCode } ${ this.statusMessage }` }
  822. });
  823. }
  824. }
  825. function getTime(): number { return (new Date()).getTime(); }
  826. function unpercent(value: string): Uint8Array {
  827. return toUtf8Bytes(value.replace(/%([0-9a-f][0-9a-f])/gi, (all, code) => {
  828. return String.fromCharCode(parseInt(code, 16));
  829. }));
  830. }
  831. function wait(delay: number): Promise<void> {
  832. return new Promise((resolve) => setTimeout(resolve, delay));
  833. }