fetch.js 28 KB

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