fetch.js 29 KB

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