@taquito/http-utils 24.2.0 → 24.3.0-beta.1
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/README.md +13 -4
- package/dist/lib/errors.js +61 -7
- package/dist/lib/taquito-http-utils.js +208 -59
- package/dist/lib/transport-errors.js +141 -0
- package/dist/lib/version.js +2 -2
- package/dist/taquito-http-utils.es6.js +408 -92
- package/dist/taquito-http-utils.es6.js.map +1 -1
- package/dist/taquito-http-utils.umd.js +846 -529
- package/dist/taquito-http-utils.umd.js.map +1 -1
- package/dist/types/errors.d.ts +36 -6
- package/dist/types/taquito-http-utils.d.ts +31 -3
- package/dist/types/transport-errors.d.ts +28 -0
- package/package.json +28 -20
- package/LICENSE +0 -202
|
@@ -1,57 +1,75 @@
|
|
|
1
1
|
import { NetworkError } from '@taquito/core';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
33
|
-
};
|
|
34
|
-
|
|
3
|
+
const MAX_CAUSE_DEPTH$1 = 10;
|
|
4
|
+
/** Walk the `.cause` chain and return the deepest Error (or object-shaped cause). */
|
|
5
|
+
function deepestCause(err) {
|
|
6
|
+
let current = err;
|
|
7
|
+
let depth = 0;
|
|
8
|
+
for (;;) {
|
|
9
|
+
if (depth >= MAX_CAUSE_DEPTH$1)
|
|
10
|
+
break;
|
|
11
|
+
const next = current['cause'];
|
|
12
|
+
if (next instanceof Error) {
|
|
13
|
+
current = next;
|
|
14
|
+
depth++;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
// Handle object-shaped causes (e.g. undici sometimes throws { message, code } objects)
|
|
18
|
+
if (typeof next === 'object' &&
|
|
19
|
+
next !== null &&
|
|
20
|
+
typeof next['message'] === 'string') {
|
|
21
|
+
const obj = next;
|
|
22
|
+
current = {
|
|
23
|
+
message: obj['message'],
|
|
24
|
+
code: typeof obj['code'] === 'string' ? obj['code'] : undefined,
|
|
25
|
+
};
|
|
26
|
+
break;
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
return current;
|
|
31
|
+
}
|
|
35
32
|
/**
|
|
36
33
|
* @category Error
|
|
37
|
-
*
|
|
34
|
+
* Error that indicates a general failure in making the HTTP request
|
|
38
35
|
*/
|
|
39
36
|
class HttpRequestFailed extends NetworkError {
|
|
40
|
-
constructor(
|
|
37
|
+
constructor(
|
|
38
|
+
/** The HTTP method that was attempted. */
|
|
39
|
+
method,
|
|
40
|
+
/** The URL that was requested. */
|
|
41
|
+
url,
|
|
42
|
+
/** The underlying error that caused the request to fail. */
|
|
43
|
+
cause, transportError) {
|
|
41
44
|
super();
|
|
42
45
|
this.method = method;
|
|
43
46
|
this.url = url;
|
|
44
47
|
this.cause = cause;
|
|
45
48
|
this.name = 'HttpRequestFailed';
|
|
46
|
-
|
|
49
|
+
const rootCause = deepestCause(cause);
|
|
50
|
+
const rootCode = rootCause.code;
|
|
51
|
+
const detail = rootCause !== cause
|
|
52
|
+
? `${rootCause.message}${rootCode ? ` [${rootCode}]` : ''}`
|
|
53
|
+
: cause.message;
|
|
54
|
+
const kindLabel = transportError ? ` (${transportError.kind})` : '';
|
|
55
|
+
this.message = `${method} ${url}${kindLabel}: ${detail}`;
|
|
56
|
+
this.transportError = transportError;
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
/**
|
|
50
60
|
* @category Error
|
|
51
|
-
*
|
|
61
|
+
* Error thrown when the endpoint returns an HTTP error to the client
|
|
52
62
|
*/
|
|
53
63
|
class HttpResponseError extends NetworkError {
|
|
54
|
-
constructor(message,
|
|
64
|
+
constructor(message,
|
|
65
|
+
/** The HTTP status code (e.g. 404, 500). */
|
|
66
|
+
status,
|
|
67
|
+
/** The HTTP status text (e.g. "Not Found"). */
|
|
68
|
+
statusText,
|
|
69
|
+
/** The raw response body text. */
|
|
70
|
+
body,
|
|
71
|
+
/** The URL that was requested. */
|
|
72
|
+
url) {
|
|
55
73
|
super();
|
|
56
74
|
this.message = message;
|
|
57
75
|
this.status = status;
|
|
@@ -63,10 +81,14 @@ class HttpResponseError extends NetworkError {
|
|
|
63
81
|
}
|
|
64
82
|
/**
|
|
65
83
|
* @category Error
|
|
66
|
-
*
|
|
84
|
+
* Error thrown when an HTTP request exceeds its configured timeout duration.
|
|
67
85
|
*/
|
|
68
86
|
class HttpTimeoutError extends NetworkError {
|
|
69
|
-
constructor(
|
|
87
|
+
constructor(
|
|
88
|
+
/** The timeout duration in milliseconds that was exceeded. */
|
|
89
|
+
timeout,
|
|
90
|
+
/** The URL that was requested. */
|
|
91
|
+
url) {
|
|
70
92
|
super();
|
|
71
93
|
this.timeout = timeout;
|
|
72
94
|
this.url = url;
|
|
@@ -75,6 +97,145 @@ class HttpTimeoutError extends NetworkError {
|
|
|
75
97
|
}
|
|
76
98
|
}
|
|
77
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Cross-runtime transport error classifier.
|
|
102
|
+
*
|
|
103
|
+
* Inspects structural properties first (err.code, err.cause.code, err.name),
|
|
104
|
+
* falls back to message substring matching only when structure isn't enough.
|
|
105
|
+
* Handles node-fetch FetchError, native fetch/undici TypeError + cause chain,
|
|
106
|
+
* browser TypeError, Deno, and Bun error shapes.
|
|
107
|
+
*/
|
|
108
|
+
const MAY_HAVE_REACHED = {
|
|
109
|
+
abort: false,
|
|
110
|
+
dns: false,
|
|
111
|
+
connect: false,
|
|
112
|
+
socket: true,
|
|
113
|
+
tls: false,
|
|
114
|
+
timeout: true,
|
|
115
|
+
network: true,
|
|
116
|
+
};
|
|
117
|
+
function extractCode(err) {
|
|
118
|
+
if (typeof err !== 'object' || err === null)
|
|
119
|
+
return undefined;
|
|
120
|
+
const code = err['code'];
|
|
121
|
+
return typeof code === 'string' ? code : undefined;
|
|
122
|
+
}
|
|
123
|
+
function extractCause(err) {
|
|
124
|
+
if (typeof err !== 'object' || err === null)
|
|
125
|
+
return undefined;
|
|
126
|
+
return err['cause'];
|
|
127
|
+
}
|
|
128
|
+
function extractCauseCode(err) {
|
|
129
|
+
return extractCode(extractCause(err));
|
|
130
|
+
}
|
|
131
|
+
function extractCauseMessage(err) {
|
|
132
|
+
const cause = extractCause(err);
|
|
133
|
+
if (typeof cause === 'object' && cause !== null) {
|
|
134
|
+
const msg = cause['message'];
|
|
135
|
+
if (typeof msg === 'string')
|
|
136
|
+
return msg.toLowerCase();
|
|
137
|
+
}
|
|
138
|
+
return '';
|
|
139
|
+
}
|
|
140
|
+
function classify(kind, err) {
|
|
141
|
+
return { kind, mayHaveReachedServer: MAY_HAVE_REACHED[kind], original: err };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Classify a thrown value as a transport-level error.
|
|
145
|
+
*
|
|
146
|
+
* Inspects structural properties first (err.code, err.cause.code, err.name),
|
|
147
|
+
* then falls back to message substring matching. Handles node-fetch FetchError,
|
|
148
|
+
* native fetch/undici TypeError + cause chain, browser TypeError, Deno, and Bun.
|
|
149
|
+
*
|
|
150
|
+
* @returns the classification, or `undefined` if the error is not transport-related.
|
|
151
|
+
*/
|
|
152
|
+
function classifyTransportError(err) {
|
|
153
|
+
if (!(err instanceof Error))
|
|
154
|
+
return undefined;
|
|
155
|
+
const code = extractCode(err);
|
|
156
|
+
const causeCode = extractCauseCode(err);
|
|
157
|
+
const msg = err.message.toLowerCase();
|
|
158
|
+
const causeMsg = extractCauseMessage(err);
|
|
159
|
+
// 1. AbortError (our timeout or caller cancel)
|
|
160
|
+
if (err.name === 'AbortError') {
|
|
161
|
+
return classify('abort', err);
|
|
162
|
+
}
|
|
163
|
+
// 2. DNS resolution failure
|
|
164
|
+
if (code === 'ENOTFOUND' ||
|
|
165
|
+
code === 'EAI_AGAIN' ||
|
|
166
|
+
causeCode === 'ENOTFOUND' ||
|
|
167
|
+
causeCode === 'EAI_AGAIN' ||
|
|
168
|
+
msg.includes('enotfound') ||
|
|
169
|
+
msg.includes('eai_again')) {
|
|
170
|
+
return classify('dns', err);
|
|
171
|
+
}
|
|
172
|
+
// 3. Connection refused / connect timeout
|
|
173
|
+
if (code === 'ECONNREFUSED' ||
|
|
174
|
+
causeCode === 'ECONNREFUSED' ||
|
|
175
|
+
causeCode === 'UND_ERR_CONNECT_TIMEOUT' ||
|
|
176
|
+
msg.includes('econnrefused')) {
|
|
177
|
+
return classify('connect', err);
|
|
178
|
+
}
|
|
179
|
+
// 4. TLS errors (check before socket/timeout since TLS errors can also set ECONNRESET)
|
|
180
|
+
// Also inspect cause.message for native fetch which wraps TLS errors in TypeError("fetch failed")
|
|
181
|
+
if (msg.includes('certificate') ||
|
|
182
|
+
msg.includes('self signed') ||
|
|
183
|
+
msg.includes('ssl') ||
|
|
184
|
+
msg.includes('handshake') ||
|
|
185
|
+
causeMsg.includes('certificate') ||
|
|
186
|
+
causeMsg.includes('self signed') ||
|
|
187
|
+
causeMsg.includes('ssl') ||
|
|
188
|
+
causeMsg.includes('handshake')) {
|
|
189
|
+
return classify('tls', err);
|
|
190
|
+
}
|
|
191
|
+
// 5. TCP/connect timeout and undici header/body timeouts
|
|
192
|
+
if (code === 'ETIMEDOUT' ||
|
|
193
|
+
causeCode === 'ETIMEDOUT' ||
|
|
194
|
+
causeCode === 'UND_ERR_HEADERS_TIMEOUT' ||
|
|
195
|
+
causeCode === 'UND_ERR_BODY_TIMEOUT' ||
|
|
196
|
+
msg.includes('etimedout') ||
|
|
197
|
+
msg.includes('timed out')) {
|
|
198
|
+
return classify('timeout', err);
|
|
199
|
+
}
|
|
200
|
+
// 6. Socket-level errors (connection was established or partially established)
|
|
201
|
+
if (code === 'ECONNRESET' ||
|
|
202
|
+
causeCode === 'ECONNRESET' ||
|
|
203
|
+
causeCode === 'UND_ERR_SOCKET' ||
|
|
204
|
+
msg.includes('econnreset') ||
|
|
205
|
+
msg.includes('socket hang up') ||
|
|
206
|
+
msg.includes('other side closed') ||
|
|
207
|
+
msg.includes('closed unexpectedly') ||
|
|
208
|
+
msg.includes('connection reset')) {
|
|
209
|
+
return classify('socket', err);
|
|
210
|
+
}
|
|
211
|
+
// 7. Generic fetch failure (browser TypeError, undici wrapper)
|
|
212
|
+
// "terminated" is undici's wrapper for body-read failures (e.g. response.json() after socket drop)
|
|
213
|
+
if (err instanceof TypeError &&
|
|
214
|
+
(msg.includes('fetch failed') ||
|
|
215
|
+
msg.includes('failed to fetch') ||
|
|
216
|
+
msg.includes('network error') ||
|
|
217
|
+
msg.includes('error sending request') ||
|
|
218
|
+
msg === 'terminated')) {
|
|
219
|
+
return classify('network', err);
|
|
220
|
+
}
|
|
221
|
+
// 8. Remaining node-fetch FetchError.
|
|
222
|
+
// Transport types: 'system' (OS-level), 'request-timeout', 'body-timeout'.
|
|
223
|
+
// Non-transport types: 'invalid-json', 'no-redirect', 'max-size'.
|
|
224
|
+
if (err.name === 'FetchError') {
|
|
225
|
+
const fetchType = err['type'];
|
|
226
|
+
if (fetchType === 'request-timeout' || fetchType === 'body-timeout') {
|
|
227
|
+
return classify('timeout', err);
|
|
228
|
+
}
|
|
229
|
+
if (fetchType === 'system') {
|
|
230
|
+
return classify('network', err);
|
|
231
|
+
}
|
|
232
|
+
// Not a transport error (e.g. JSON parse failure on a 200 response)
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
// Not a transport error
|
|
236
|
+
return undefined;
|
|
237
|
+
}
|
|
238
|
+
|
|
78
239
|
/**
|
|
79
240
|
* Hypertext Transfer Protocol (HTTP) response status codes.
|
|
80
241
|
* @see {@link https://en.wikipedia.org/wiki/List_of_HTTP_status_codes}
|
|
@@ -397,48 +558,117 @@ var STATUS_CODE;
|
|
|
397
558
|
|
|
398
559
|
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT!
|
|
399
560
|
const VERSION = {
|
|
400
|
-
"commitHash": "
|
|
401
|
-
"version": "24.
|
|
561
|
+
"commitHash": "05df48fee92f846cba793920d6fa829afd6a1847",
|
|
562
|
+
"version": "24.3.0-beta.1"
|
|
402
563
|
};
|
|
403
564
|
|
|
404
565
|
/**
|
|
405
566
|
* @packageDocumentation
|
|
406
567
|
* @module @taquito/http-utils
|
|
407
568
|
*/
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
let createAgent;
|
|
411
|
-
let useNodeFetchAgent = false;
|
|
412
|
-
const isNode = typeof process !== 'undefined' && !!((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
|
|
413
|
-
const isBrowserLike = typeof window !== 'undefined';
|
|
414
|
-
// Use native fetch in browser-like environments (they have reliable native fetch)
|
|
415
|
-
// Use node-fetch in pure Node.js CLI for better compatibility and keepAlive control
|
|
416
|
-
if (isNode && !isBrowserLike) {
|
|
417
|
-
// Handle both ESM and CJS default export patterns for webpack compatibility
|
|
418
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
419
|
-
const nodeFetch = require('node-fetch');
|
|
420
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
421
|
-
const https = require('https');
|
|
422
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
423
|
-
const http = require('http');
|
|
424
|
-
fetch = nodeFetch.default || nodeFetch;
|
|
425
|
-
useNodeFetchAgent = true;
|
|
426
|
-
if (Number(process.versions.node.split('.')[0]) >= 19) {
|
|
427
|
-
// we need agent with keepalive false for node 19 and above
|
|
428
|
-
createAgent = (url) => {
|
|
429
|
-
return url.startsWith('https')
|
|
430
|
-
? new https.Agent({ keepAlive: false })
|
|
431
|
-
: new http.Agent({ keepAlive: false });
|
|
432
|
-
};
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
else if (typeof fetch !== 'function') {
|
|
436
|
-
throw new Error('No fetch implementation available');
|
|
569
|
+
if (typeof globalThis.fetch !== 'function') {
|
|
570
|
+
throw new Error('No fetch implementation available. Requires Node.js >= 22 or a browser environment.');
|
|
437
571
|
}
|
|
572
|
+
const httpTraceEnabled = /^(1|true)$/i.test(process?.env?.TAQUITO_HTTP_TRACE ?? '') || process?.env?.RUNNER_DEBUG === '1';
|
|
573
|
+
const parsedHttpRetryCount = Number(process?.env?.TAQUITO_HTTP_RETRY_COUNT ?? '1');
|
|
574
|
+
const httpRetryCount = Number.isFinite(parsedHttpRetryCount) && parsedHttpRetryCount >= 0
|
|
575
|
+
? Math.floor(parsedHttpRetryCount)
|
|
576
|
+
: 1;
|
|
577
|
+
const parsedHttpRetryBaseMs = Number(process?.env?.TAQUITO_HTTP_RETRY_BASE_MS ?? '100');
|
|
578
|
+
const httpRetryBaseMs = Number.isFinite(parsedHttpRetryBaseMs) && parsedHttpRetryBaseMs >= 0
|
|
579
|
+
? parsedHttpRetryBaseMs
|
|
580
|
+
: 100;
|
|
581
|
+
const normalizeTraceUrl = (url) => {
|
|
582
|
+
try {
|
|
583
|
+
const parsedUrl = new URL(url);
|
|
584
|
+
return `${parsedUrl.origin}${parsedUrl.pathname}`;
|
|
585
|
+
}
|
|
586
|
+
catch {
|
|
587
|
+
return url.split('?')[0];
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
const traceHttp = (payload) => {
|
|
591
|
+
if (!httpTraceEnabled) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
// JSON logs make CI-side grepping and aggregation much easier.
|
|
595
|
+
console.log(`[taquito:http-trace] ${JSON.stringify(payload)}`);
|
|
596
|
+
};
|
|
597
|
+
const getCause = (err) => err['cause'];
|
|
598
|
+
const getCode = (err) => {
|
|
599
|
+
const code = err['code'];
|
|
600
|
+
return typeof code === 'string' ? code : undefined;
|
|
601
|
+
};
|
|
602
|
+
const MAX_CAUSE_DEPTH = 10;
|
|
603
|
+
const toErrorMessage = (error) => {
|
|
604
|
+
if (!(error instanceof Error)) {
|
|
605
|
+
return String(error);
|
|
606
|
+
}
|
|
607
|
+
const parts = [`${error.name}: ${error.message}`];
|
|
608
|
+
let current = getCause(error);
|
|
609
|
+
let depth = 0;
|
|
610
|
+
while (depth < MAX_CAUSE_DEPTH) {
|
|
611
|
+
if (current instanceof Error) {
|
|
612
|
+
const code = getCode(current);
|
|
613
|
+
const codeSuffix = code ? ` [${code}]` : '';
|
|
614
|
+
parts.push(`${current.name}: ${current.message}${codeSuffix}`);
|
|
615
|
+
current = getCause(current);
|
|
616
|
+
depth++;
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
// Handle object-shaped causes (e.g. undici { message, code } objects)
|
|
620
|
+
if (typeof current === 'object' &&
|
|
621
|
+
current !== null &&
|
|
622
|
+
typeof current['message'] === 'string') {
|
|
623
|
+
const obj = current;
|
|
624
|
+
const code = typeof obj['code'] === 'string' ? obj['code'] : undefined;
|
|
625
|
+
const codeSuffix = code ? ` [${code}]` : '';
|
|
626
|
+
parts.push(`${obj['message']}${codeSuffix}`);
|
|
627
|
+
}
|
|
628
|
+
break;
|
|
629
|
+
}
|
|
630
|
+
return parts.join(' → ');
|
|
631
|
+
};
|
|
632
|
+
const isRetriableRequest = (method, url) => {
|
|
633
|
+
const normalizedMethod = method.toUpperCase();
|
|
634
|
+
if (normalizedMethod === 'GET') {
|
|
635
|
+
return true;
|
|
636
|
+
}
|
|
637
|
+
if (normalizedMethod !== 'POST') {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
const normalizedUrl = normalizeTraceUrl(url);
|
|
641
|
+
return (normalizedUrl.endsWith('/helpers/forge/operations') ||
|
|
642
|
+
normalizedUrl.endsWith('/helpers/preapply/operations') ||
|
|
643
|
+
normalizedUrl.endsWith('/helpers/scripts/simulate_operation') ||
|
|
644
|
+
normalizedUrl.endsWith('/helpers/scripts/run_operation') ||
|
|
645
|
+
// Safe to retry: ops are content-addressed, mempool deduplicates identical bytes,
|
|
646
|
+
// and counter prevents replay. If the first request secretly succeeded but the
|
|
647
|
+
// connection dropped, the retry may get a 500 (async_injection_failed) which
|
|
648
|
+
// propagates to the caller via HttpResponseError (not retried). No silent corruption,
|
|
649
|
+
// but the caller may see an error despite the op being on-chain.
|
|
650
|
+
normalizedUrl.endsWith('/injection/operation'));
|
|
651
|
+
};
|
|
652
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
653
|
+
/**
|
|
654
|
+
* HTTP client used by Taquito to communicate with Tezos RPC nodes.
|
|
655
|
+
*
|
|
656
|
+
* Uses `globalThis.fetch` (Node.js >= 22 built-in or browser native).
|
|
657
|
+
* Retries retriable transport errors (socket resets, DNS, timeouts) with
|
|
658
|
+
* exponential backoff and jitter. Configure via environment variables:
|
|
659
|
+
*
|
|
660
|
+
* - `TAQUITO_HTTP_RETRY_COUNT` - max retries (default `1`)
|
|
661
|
+
* - `TAQUITO_HTTP_RETRY_BASE_MS` - base delay in ms (default `100`)
|
|
662
|
+
* - `TAQUITO_HTTP_TRACE` - emit JSON request logs when `true` or `1`
|
|
663
|
+
*/
|
|
438
664
|
class HttpBackend {
|
|
665
|
+
/**
|
|
666
|
+
* @param timeout - Default request timeout in milliseconds (default `30000`).
|
|
667
|
+
*/
|
|
439
668
|
constructor(timeout = 30000) {
|
|
440
669
|
this.timeout = timeout;
|
|
441
670
|
}
|
|
671
|
+
/** Serialize an object into a URL query string (including the leading `?`). */
|
|
442
672
|
serialize(obj) {
|
|
443
673
|
if (!obj) {
|
|
444
674
|
return '';
|
|
@@ -447,9 +677,14 @@ class HttpBackend {
|
|
|
447
677
|
for (const p in obj) {
|
|
448
678
|
// eslint-disable-next-line no-prototype-builtins
|
|
449
679
|
if (obj.hasOwnProperty(p) && typeof obj[p] !== 'undefined') {
|
|
450
|
-
const
|
|
680
|
+
const val = obj[p];
|
|
451
681
|
// query arguments can have no value so we need some way of handling that
|
|
452
682
|
// example https://domain.com/query?all
|
|
683
|
+
if (val === null) {
|
|
684
|
+
str.push(encodeURIComponent(p));
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
const prop = typeof val.toJSON === 'function' ? val.toJSON() : val;
|
|
453
688
|
if (prop === null) {
|
|
454
689
|
str.push(encodeURIComponent(p));
|
|
455
690
|
continue;
|
|
@@ -474,33 +709,64 @@ class HttpBackend {
|
|
|
474
709
|
}
|
|
475
710
|
}
|
|
476
711
|
/**
|
|
712
|
+
* Send an HTTP request to the given URL, with automatic retries on transport errors.
|
|
477
713
|
*
|
|
478
|
-
* @param options
|
|
479
|
-
* @
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
714
|
+
* @param options - Request configuration (URL, method, timeout, headers, etc.).
|
|
715
|
+
* @param data - Request body, serialized to JSON via `JSON.stringify`.
|
|
716
|
+
* @returns The parsed JSON response (or raw text when `json: false`).
|
|
717
|
+
* @throws {@link HttpResponseError} when the server returns HTTP status >= 400.
|
|
718
|
+
* @throws {@link HttpTimeoutError} when the request exceeds the configured timeout.
|
|
719
|
+
* @throws {@link HttpRequestFailed} for transport-level failures (network, DNS, socket, etc.).
|
|
720
|
+
*/
|
|
721
|
+
async createRequest({ url, method, timeout = this.timeout, query, headers = {}, json = true }, data) {
|
|
722
|
+
// Serializes query params
|
|
723
|
+
const urlWithQuery = url + this.serialize(query);
|
|
724
|
+
const methodValue = String(method ?? 'GET');
|
|
725
|
+
// Adds default header entry if there aren't any Content-Type header
|
|
726
|
+
if (!headers['Content-Type']) {
|
|
727
|
+
headers['Content-Type'] = 'application/json';
|
|
728
|
+
}
|
|
729
|
+
// Serialized once on first attempt, reused on retries for byte-stable requests.
|
|
730
|
+
let body;
|
|
731
|
+
let bodySerialized = false;
|
|
732
|
+
for (let attempt = 0; attempt <= httpRetryCount; attempt++) {
|
|
489
733
|
// Creates a new AbortController instance to handle timeouts
|
|
490
734
|
const controller = new AbortController();
|
|
491
735
|
const t = setTimeout(() => controller.abort(), timeout);
|
|
736
|
+
const requestStartedAt = Date.now();
|
|
492
737
|
try {
|
|
493
|
-
|
|
738
|
+
if (!bodySerialized) {
|
|
739
|
+
body = JSON.stringify(data);
|
|
740
|
+
bodySerialized = true;
|
|
741
|
+
}
|
|
742
|
+
const response = await globalThis.fetch(urlWithQuery, {
|
|
494
743
|
method,
|
|
495
|
-
headers,
|
|
744
|
+
headers,
|
|
745
|
+
body,
|
|
746
|
+
signal: controller.signal,
|
|
747
|
+
});
|
|
496
748
|
if (typeof response === 'undefined') {
|
|
497
749
|
throw new Error('Response is undefined');
|
|
498
750
|
}
|
|
499
751
|
// Handle responses with status code >= 400
|
|
500
752
|
if (response.status >= 400) {
|
|
501
|
-
const errorData =
|
|
753
|
+
const errorData = await response.text();
|
|
754
|
+
traceHttp({
|
|
755
|
+
stage: 'response-error',
|
|
756
|
+
method: methodValue,
|
|
757
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
758
|
+
status: response.status,
|
|
759
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
760
|
+
});
|
|
502
761
|
throw new HttpResponseError(`Http error response: (${response.status}) ${errorData}`, response.status, response.statusText, errorData, urlWithQuery);
|
|
503
762
|
}
|
|
763
|
+
traceHttp({
|
|
764
|
+
stage: 'response-ok',
|
|
765
|
+
method: methodValue,
|
|
766
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
767
|
+
status: response.status,
|
|
768
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
769
|
+
});
|
|
504
770
|
if (json) {
|
|
505
771
|
return response.json();
|
|
506
772
|
}
|
|
@@ -509,22 +775,72 @@ class HttpBackend {
|
|
|
509
775
|
}
|
|
510
776
|
}
|
|
511
777
|
catch (e) {
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
778
|
+
// HttpResponseError is an application-level error (status >= 400), not a transport error.
|
|
779
|
+
// Short-circuit before classification to prevent RPC body text (which may contain
|
|
780
|
+
// strings like "connection reset") from being misclassified as a transport error.
|
|
781
|
+
if (e instanceof HttpResponseError) {
|
|
782
|
+
traceHttp({
|
|
783
|
+
stage: 'http-response-error',
|
|
784
|
+
method: methodValue,
|
|
785
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
786
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
787
|
+
status: e.status,
|
|
788
|
+
});
|
|
516
789
|
throw e;
|
|
517
790
|
}
|
|
791
|
+
const classified = e instanceof Error ? classifyTransportError(e) : undefined;
|
|
792
|
+
const isRetriableTransport = classified !== undefined && classified.kind !== 'abort' && classified.kind !== 'tls';
|
|
793
|
+
const shouldRetry = attempt < httpRetryCount &&
|
|
794
|
+
isRetriableRequest(methodValue, urlWithQuery) &&
|
|
795
|
+
isRetriableTransport;
|
|
796
|
+
if (shouldRetry) {
|
|
797
|
+
const exponential = httpRetryBaseMs * Math.pow(2, attempt);
|
|
798
|
+
const jitter = Math.floor(Math.random() * httpRetryBaseMs);
|
|
799
|
+
const retryDelayMs = exponential + jitter;
|
|
800
|
+
traceHttp({
|
|
801
|
+
stage: 'request-retry',
|
|
802
|
+
method: methodValue,
|
|
803
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
804
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
805
|
+
attempt: attempt + 1,
|
|
806
|
+
maxAttempts: httpRetryCount + 1,
|
|
807
|
+
retryDelayMs,
|
|
808
|
+
error: toErrorMessage(e),
|
|
809
|
+
transportKind: classified?.kind,
|
|
810
|
+
});
|
|
811
|
+
await sleep(retryDelayMs);
|
|
812
|
+
continue;
|
|
813
|
+
}
|
|
814
|
+
if (classified?.kind === 'abort') {
|
|
815
|
+
traceHttp({
|
|
816
|
+
stage: 'timeout',
|
|
817
|
+
method: methodValue,
|
|
818
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
819
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
820
|
+
timeoutMs: timeout,
|
|
821
|
+
});
|
|
822
|
+
throw new HttpTimeoutError(timeout, urlWithQuery);
|
|
823
|
+
}
|
|
518
824
|
else {
|
|
519
|
-
|
|
825
|
+
traceHttp({
|
|
826
|
+
stage: 'request-failed',
|
|
827
|
+
method: methodValue,
|
|
828
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
829
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
830
|
+
error: toErrorMessage(e),
|
|
831
|
+
transportKind: classified?.kind,
|
|
832
|
+
});
|
|
833
|
+
const cause = e instanceof Error ? e : new Error(String(e));
|
|
834
|
+
throw new HttpRequestFailed(methodValue, urlWithQuery, cause, classified);
|
|
520
835
|
}
|
|
521
836
|
}
|
|
522
837
|
finally {
|
|
523
838
|
clearTimeout(t);
|
|
524
839
|
}
|
|
525
|
-
}
|
|
840
|
+
}
|
|
841
|
+
throw new Error('Unexpected request retry flow');
|
|
526
842
|
}
|
|
527
843
|
}
|
|
528
844
|
|
|
529
|
-
export { HttpBackend, HttpRequestFailed, HttpResponseError, HttpTimeoutError, STATUS_CODE, VERSION };
|
|
845
|
+
export { HttpBackend, HttpRequestFailed, HttpResponseError, HttpTimeoutError, STATUS_CODE, VERSION, classifyTransportError };
|
|
530
846
|
//# sourceMappingURL=taquito-http-utils.es6.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"taquito-http-utils.es6.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"taquito-http-utils.es6.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|