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