@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
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
## General Information
|
|
7
7
|
|
|
8
|
-
The `HttpBackend` class contains a `createRequest` method which accepts options to be passed for the HTTP request (url, method, timeout, json, query, headers
|
|
8
|
+
The `HttpBackend` class contains a `createRequest` method which accepts options to be passed for the HTTP request (url, method, timeout, json, query, headers). This method will help users interact with the RPC with a more familiar HTTP format.
|
|
9
9
|
|
|
10
10
|
Parameters for `createRequest`:
|
|
11
11
|
|
|
@@ -14,8 +14,7 @@ Parameters for `createRequest`:
|
|
|
14
14
|
`timeout`(number): request timeout
|
|
15
15
|
`json`(boolean): Parse response into JSON when set to `true`; defaults to `true`
|
|
16
16
|
`query`(object): Query that we would like to pass as an HTTP request
|
|
17
|
-
`headers`(object): HTTP request header
|
|
18
|
-
`mimeType`(string): Sets the MIME type of the request; see [MIME types](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
|
|
17
|
+
`headers`(object): HTTP request header
|
|
19
18
|
|
|
20
19
|
|
|
21
20
|
## Install
|
|
@@ -33,13 +32,23 @@ const httpBackend = new HttpBackend();
|
|
|
33
32
|
const response = httpBackend.createRequest<string>({
|
|
34
33
|
url: `/chains/${chain}/blocks/${block}/context/contracts/${address}/script`,
|
|
35
34
|
method: 'GET',
|
|
36
|
-
|
|
35
|
+
headers: { 'Content-Type': 'text/plain; charset=utf-8' },
|
|
37
36
|
json: false
|
|
38
37
|
});
|
|
39
38
|
|
|
40
39
|
```
|
|
41
40
|
|
|
42
41
|
## Additional Info
|
|
42
|
+
Taquito uses the built-in `globalThis.fetch` (requires Node.js >= 22 or a browser environment).
|
|
43
|
+
|
|
44
|
+
For diagnostics, you can emit request timing logs with:
|
|
45
|
+
|
|
46
|
+
`TAQUITO_HTTP_TRACE=true`
|
|
47
|
+
|
|
48
|
+
Optionally adjust the slow-request threshold (milliseconds):
|
|
49
|
+
|
|
50
|
+
`TAQUITO_HTTP_TRACE_SLOW_MS=1500`
|
|
51
|
+
|
|
43
52
|
See the top-level https://github.com/ecadlabs/taquito file for details on reporting issues, contributing, and versioning.
|
|
44
53
|
|
|
45
54
|
|
package/dist/lib/errors.js
CHANGED
|
@@ -2,27 +2,77 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HttpTimeoutError = exports.HttpResponseError = exports.HttpRequestFailed = void 0;
|
|
4
4
|
const core_1 = require("@taquito/core");
|
|
5
|
+
const MAX_CAUSE_DEPTH = 10;
|
|
6
|
+
/** Walk the `.cause` chain and return the deepest Error (or object-shaped cause). */
|
|
7
|
+
function deepestCause(err) {
|
|
8
|
+
let current = err;
|
|
9
|
+
let depth = 0;
|
|
10
|
+
for (;;) {
|
|
11
|
+
if (depth >= MAX_CAUSE_DEPTH)
|
|
12
|
+
break;
|
|
13
|
+
const next = current['cause'];
|
|
14
|
+
if (next instanceof Error) {
|
|
15
|
+
current = next;
|
|
16
|
+
depth++;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
// Handle object-shaped causes (e.g. undici sometimes throws { message, code } objects)
|
|
20
|
+
if (typeof next === 'object' &&
|
|
21
|
+
next !== null &&
|
|
22
|
+
typeof next['message'] === 'string') {
|
|
23
|
+
const obj = next;
|
|
24
|
+
current = {
|
|
25
|
+
message: obj['message'],
|
|
26
|
+
code: typeof obj['code'] === 'string' ? obj['code'] : undefined,
|
|
27
|
+
};
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
5
34
|
/**
|
|
6
35
|
* @category Error
|
|
7
|
-
*
|
|
36
|
+
* Error that indicates a general failure in making the HTTP request
|
|
8
37
|
*/
|
|
9
38
|
class HttpRequestFailed extends core_1.NetworkError {
|
|
10
|
-
constructor(
|
|
39
|
+
constructor(
|
|
40
|
+
/** The HTTP method that was attempted. */
|
|
41
|
+
method,
|
|
42
|
+
/** The URL that was requested. */
|
|
43
|
+
url,
|
|
44
|
+
/** The underlying error that caused the request to fail. */
|
|
45
|
+
cause, transportError) {
|
|
11
46
|
super();
|
|
12
47
|
this.method = method;
|
|
13
48
|
this.url = url;
|
|
14
49
|
this.cause = cause;
|
|
15
50
|
this.name = 'HttpRequestFailed';
|
|
16
|
-
|
|
51
|
+
const rootCause = deepestCause(cause);
|
|
52
|
+
const rootCode = rootCause.code;
|
|
53
|
+
const detail = rootCause !== cause
|
|
54
|
+
? `${rootCause.message}${rootCode ? ` [${rootCode}]` : ''}`
|
|
55
|
+
: cause.message;
|
|
56
|
+
const kindLabel = transportError ? ` (${transportError.kind})` : '';
|
|
57
|
+
this.message = `${method} ${url}${kindLabel}: ${detail}`;
|
|
58
|
+
this.transportError = transportError;
|
|
17
59
|
}
|
|
18
60
|
}
|
|
19
61
|
exports.HttpRequestFailed = HttpRequestFailed;
|
|
20
62
|
/**
|
|
21
63
|
* @category Error
|
|
22
|
-
*
|
|
64
|
+
* Error thrown when the endpoint returns an HTTP error to the client
|
|
23
65
|
*/
|
|
24
66
|
class HttpResponseError extends core_1.NetworkError {
|
|
25
|
-
constructor(message,
|
|
67
|
+
constructor(message,
|
|
68
|
+
/** The HTTP status code (e.g. 404, 500). */
|
|
69
|
+
status,
|
|
70
|
+
/** The HTTP status text (e.g. "Not Found"). */
|
|
71
|
+
statusText,
|
|
72
|
+
/** The raw response body text. */
|
|
73
|
+
body,
|
|
74
|
+
/** The URL that was requested. */
|
|
75
|
+
url) {
|
|
26
76
|
super();
|
|
27
77
|
this.message = message;
|
|
28
78
|
this.status = status;
|
|
@@ -35,10 +85,14 @@ class HttpResponseError extends core_1.NetworkError {
|
|
|
35
85
|
exports.HttpResponseError = HttpResponseError;
|
|
36
86
|
/**
|
|
37
87
|
* @category Error
|
|
38
|
-
*
|
|
88
|
+
* Error thrown when an HTTP request exceeds its configured timeout duration.
|
|
39
89
|
*/
|
|
40
90
|
class HttpTimeoutError extends core_1.NetworkError {
|
|
41
|
-
constructor(
|
|
91
|
+
constructor(
|
|
92
|
+
/** The timeout duration in milliseconds that was exceeded. */
|
|
93
|
+
timeout,
|
|
94
|
+
/** The URL that was requested. */
|
|
95
|
+
url) {
|
|
42
96
|
super();
|
|
43
97
|
this.timeout = timeout;
|
|
44
98
|
this.url = url;
|
|
@@ -17,48 +17,22 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
|
|
|
17
17
|
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
18
18
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
19
19
|
};
|
|
20
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
21
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
22
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
23
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
24
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
25
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
26
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
27
|
-
});
|
|
28
|
-
};
|
|
29
|
-
var _a;
|
|
30
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
31
|
-
exports.HttpBackend = exports.HttpTimeoutError = exports.HttpResponseError = exports.HttpRequestFailed = exports.VERSION = void 0;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
let useNodeFetchAgent = false;
|
|
35
|
-
const isNode = typeof process !== 'undefined' && !!((_a = process === null || process === void 0 ? void 0 : process.versions) === null || _a === void 0 ? void 0 : _a.node);
|
|
36
|
-
const isBrowserLike = typeof window !== 'undefined';
|
|
37
|
-
// Use native fetch in browser-like environments (they have reliable native fetch)
|
|
38
|
-
// Use node-fetch in pure Node.js CLI for better compatibility and keepAlive control
|
|
39
|
-
if (isNode && !isBrowserLike) {
|
|
40
|
-
// Handle both ESM and CJS default export patterns for webpack compatibility
|
|
41
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
42
|
-
const nodeFetch = require('node-fetch');
|
|
43
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
44
|
-
const https = require('https');
|
|
45
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
46
|
-
const http = require('http');
|
|
47
|
-
fetch = nodeFetch.default || nodeFetch;
|
|
48
|
-
useNodeFetchAgent = true;
|
|
49
|
-
if (Number(process.versions.node.split('.')[0]) >= 19) {
|
|
50
|
-
// we need agent with keepalive false for node 19 and above
|
|
51
|
-
createAgent = (url) => {
|
|
52
|
-
return url.startsWith('https')
|
|
53
|
-
? new https.Agent({ keepAlive: false })
|
|
54
|
-
: new http.Agent({ keepAlive: false });
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
else if (typeof fetch !== 'function') {
|
|
59
|
-
throw new Error('No fetch implementation available');
|
|
21
|
+
exports.HttpBackend = exports.classifyTransportError = exports.HttpTimeoutError = exports.HttpResponseError = exports.HttpRequestFailed = exports.VERSION = void 0;
|
|
22
|
+
if (typeof globalThis.fetch !== 'function') {
|
|
23
|
+
throw new Error('No fetch implementation available. Requires Node.js >= 22 or a browser environment.');
|
|
60
24
|
}
|
|
25
|
+
const httpTraceEnabled = /^(1|true)$/i.test(process?.env?.TAQUITO_HTTP_TRACE ?? '') || process?.env?.RUNNER_DEBUG === '1';
|
|
26
|
+
const parsedHttpRetryCount = Number(process?.env?.TAQUITO_HTTP_RETRY_COUNT ?? '1');
|
|
27
|
+
const httpRetryCount = Number.isFinite(parsedHttpRetryCount) && parsedHttpRetryCount >= 0
|
|
28
|
+
? Math.floor(parsedHttpRetryCount)
|
|
29
|
+
: 1;
|
|
30
|
+
const parsedHttpRetryBaseMs = Number(process?.env?.TAQUITO_HTTP_RETRY_BASE_MS ?? '100');
|
|
31
|
+
const httpRetryBaseMs = Number.isFinite(parsedHttpRetryBaseMs) && parsedHttpRetryBaseMs >= 0
|
|
32
|
+
? parsedHttpRetryBaseMs
|
|
33
|
+
: 100;
|
|
61
34
|
const errors_1 = require("./errors");
|
|
35
|
+
const transport_errors_1 = require("./transport-errors");
|
|
62
36
|
__exportStar(require("./status_code"), exports);
|
|
63
37
|
var version_1 = require("./version");
|
|
64
38
|
Object.defineProperty(exports, "VERSION", { enumerable: true, get: function () { return version_1.VERSION; } });
|
|
@@ -66,10 +40,99 @@ var errors_2 = require("./errors");
|
|
|
66
40
|
Object.defineProperty(exports, "HttpRequestFailed", { enumerable: true, get: function () { return errors_2.HttpRequestFailed; } });
|
|
67
41
|
Object.defineProperty(exports, "HttpResponseError", { enumerable: true, get: function () { return errors_2.HttpResponseError; } });
|
|
68
42
|
Object.defineProperty(exports, "HttpTimeoutError", { enumerable: true, get: function () { return errors_2.HttpTimeoutError; } });
|
|
43
|
+
var transport_errors_2 = require("./transport-errors");
|
|
44
|
+
Object.defineProperty(exports, "classifyTransportError", { enumerable: true, get: function () { return transport_errors_2.classifyTransportError; } });
|
|
45
|
+
const normalizeTraceUrl = (url) => {
|
|
46
|
+
try {
|
|
47
|
+
const parsedUrl = new URL(url);
|
|
48
|
+
return `${parsedUrl.origin}${parsedUrl.pathname}`;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return url.split('?')[0];
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
const traceHttp = (payload) => {
|
|
55
|
+
if (!httpTraceEnabled) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// JSON logs make CI-side grepping and aggregation much easier.
|
|
59
|
+
console.log(`[taquito:http-trace] ${JSON.stringify(payload)}`);
|
|
60
|
+
};
|
|
61
|
+
const getCause = (err) => err['cause'];
|
|
62
|
+
const getCode = (err) => {
|
|
63
|
+
const code = err['code'];
|
|
64
|
+
return typeof code === 'string' ? code : undefined;
|
|
65
|
+
};
|
|
66
|
+
const MAX_CAUSE_DEPTH = 10;
|
|
67
|
+
const toErrorMessage = (error) => {
|
|
68
|
+
if (!(error instanceof Error)) {
|
|
69
|
+
return String(error);
|
|
70
|
+
}
|
|
71
|
+
const parts = [`${error.name}: ${error.message}`];
|
|
72
|
+
let current = getCause(error);
|
|
73
|
+
let depth = 0;
|
|
74
|
+
while (depth < MAX_CAUSE_DEPTH) {
|
|
75
|
+
if (current instanceof Error) {
|
|
76
|
+
const code = getCode(current);
|
|
77
|
+
const codeSuffix = code ? ` [${code}]` : '';
|
|
78
|
+
parts.push(`${current.name}: ${current.message}${codeSuffix}`);
|
|
79
|
+
current = getCause(current);
|
|
80
|
+
depth++;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
// Handle object-shaped causes (e.g. undici { message, code } objects)
|
|
84
|
+
if (typeof current === 'object' &&
|
|
85
|
+
current !== null &&
|
|
86
|
+
typeof current['message'] === 'string') {
|
|
87
|
+
const obj = current;
|
|
88
|
+
const code = typeof obj['code'] === 'string' ? obj['code'] : undefined;
|
|
89
|
+
const codeSuffix = code ? ` [${code}]` : '';
|
|
90
|
+
parts.push(`${obj['message']}${codeSuffix}`);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
return parts.join(' → ');
|
|
95
|
+
};
|
|
96
|
+
const isRetriableRequest = (method, url) => {
|
|
97
|
+
const normalizedMethod = method.toUpperCase();
|
|
98
|
+
if (normalizedMethod === 'GET') {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (normalizedMethod !== 'POST') {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const normalizedUrl = normalizeTraceUrl(url);
|
|
105
|
+
return (normalizedUrl.endsWith('/helpers/forge/operations') ||
|
|
106
|
+
normalizedUrl.endsWith('/helpers/preapply/operations') ||
|
|
107
|
+
normalizedUrl.endsWith('/helpers/scripts/simulate_operation') ||
|
|
108
|
+
normalizedUrl.endsWith('/helpers/scripts/run_operation') ||
|
|
109
|
+
// Safe to retry: ops are content-addressed, mempool deduplicates identical bytes,
|
|
110
|
+
// and counter prevents replay. If the first request secretly succeeded but the
|
|
111
|
+
// connection dropped, the retry may get a 500 (async_injection_failed) which
|
|
112
|
+
// propagates to the caller via HttpResponseError (not retried). No silent corruption,
|
|
113
|
+
// but the caller may see an error despite the op being on-chain.
|
|
114
|
+
normalizedUrl.endsWith('/injection/operation'));
|
|
115
|
+
};
|
|
116
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
117
|
+
/**
|
|
118
|
+
* HTTP client used by Taquito to communicate with Tezos RPC nodes.
|
|
119
|
+
*
|
|
120
|
+
* Uses `globalThis.fetch` (Node.js >= 22 built-in or browser native).
|
|
121
|
+
* Retries retriable transport errors (socket resets, DNS, timeouts) with
|
|
122
|
+
* exponential backoff and jitter. Configure via environment variables:
|
|
123
|
+
*
|
|
124
|
+
* - `TAQUITO_HTTP_RETRY_COUNT` - max retries (default `1`)
|
|
125
|
+
* - `TAQUITO_HTTP_RETRY_BASE_MS` - base delay in ms (default `100`)
|
|
126
|
+
* - `TAQUITO_HTTP_TRACE` - emit JSON request logs when `true` or `1`
|
|
127
|
+
*/
|
|
69
128
|
class HttpBackend {
|
|
129
|
+
/**
|
|
130
|
+
* @param timeout - Default request timeout in milliseconds (default `30000`).
|
|
131
|
+
*/
|
|
70
132
|
constructor(timeout = 30000) {
|
|
71
133
|
this.timeout = timeout;
|
|
72
134
|
}
|
|
135
|
+
/** Serialize an object into a URL query string (including the leading `?`). */
|
|
73
136
|
serialize(obj) {
|
|
74
137
|
if (!obj) {
|
|
75
138
|
return '';
|
|
@@ -78,9 +141,14 @@ class HttpBackend {
|
|
|
78
141
|
for (const p in obj) {
|
|
79
142
|
// eslint-disable-next-line no-prototype-builtins
|
|
80
143
|
if (obj.hasOwnProperty(p) && typeof obj[p] !== 'undefined') {
|
|
81
|
-
const
|
|
144
|
+
const val = obj[p];
|
|
82
145
|
// query arguments can have no value so we need some way of handling that
|
|
83
146
|
// example https://domain.com/query?all
|
|
147
|
+
if (val === null) {
|
|
148
|
+
str.push(encodeURIComponent(p));
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const prop = typeof val.toJSON === 'function' ? val.toJSON() : val;
|
|
84
152
|
if (prop === null) {
|
|
85
153
|
str.push(encodeURIComponent(p));
|
|
86
154
|
continue;
|
|
@@ -105,33 +173,64 @@ class HttpBackend {
|
|
|
105
173
|
}
|
|
106
174
|
}
|
|
107
175
|
/**
|
|
176
|
+
* Send an HTTP request to the given URL, with automatic retries on transport errors.
|
|
108
177
|
*
|
|
109
|
-
* @param options
|
|
110
|
-
* @
|
|
178
|
+
* @param options - Request configuration (URL, method, timeout, headers, etc.).
|
|
179
|
+
* @param data - Request body, serialized to JSON via `JSON.stringify`.
|
|
180
|
+
* @returns The parsed JSON response (or raw text when `json: false`).
|
|
181
|
+
* @throws {@link HttpResponseError} when the server returns HTTP status >= 400.
|
|
182
|
+
* @throws {@link HttpTimeoutError} when the request exceeds the configured timeout.
|
|
183
|
+
* @throws {@link HttpRequestFailed} for transport-level failures (network, DNS, socket, etc.).
|
|
111
184
|
*/
|
|
112
|
-
createRequest(
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
185
|
+
async createRequest({ url, method, timeout = this.timeout, query, headers = {}, json = true }, data) {
|
|
186
|
+
// Serializes query params
|
|
187
|
+
const urlWithQuery = url + this.serialize(query);
|
|
188
|
+
const methodValue = String(method ?? 'GET');
|
|
189
|
+
// Adds default header entry if there aren't any Content-Type header
|
|
190
|
+
if (!headers['Content-Type']) {
|
|
191
|
+
headers['Content-Type'] = 'application/json';
|
|
192
|
+
}
|
|
193
|
+
// Serialized once on first attempt, reused on retries for byte-stable requests.
|
|
194
|
+
let body;
|
|
195
|
+
let bodySerialized = false;
|
|
196
|
+
for (let attempt = 0; attempt <= httpRetryCount; attempt++) {
|
|
120
197
|
// Creates a new AbortController instance to handle timeouts
|
|
121
198
|
const controller = new AbortController();
|
|
122
199
|
const t = setTimeout(() => controller.abort(), timeout);
|
|
200
|
+
const requestStartedAt = Date.now();
|
|
123
201
|
try {
|
|
124
|
-
|
|
202
|
+
if (!bodySerialized) {
|
|
203
|
+
body = JSON.stringify(data);
|
|
204
|
+
bodySerialized = true;
|
|
205
|
+
}
|
|
206
|
+
const response = await globalThis.fetch(urlWithQuery, {
|
|
125
207
|
method,
|
|
126
|
-
headers,
|
|
208
|
+
headers,
|
|
209
|
+
body,
|
|
210
|
+
signal: controller.signal,
|
|
211
|
+
});
|
|
127
212
|
if (typeof response === 'undefined') {
|
|
128
213
|
throw new Error('Response is undefined');
|
|
129
214
|
}
|
|
130
215
|
// Handle responses with status code >= 400
|
|
131
216
|
if (response.status >= 400) {
|
|
132
|
-
const errorData =
|
|
217
|
+
const errorData = await response.text();
|
|
218
|
+
traceHttp({
|
|
219
|
+
stage: 'response-error',
|
|
220
|
+
method: methodValue,
|
|
221
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
222
|
+
status: response.status,
|
|
223
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
224
|
+
});
|
|
133
225
|
throw new errors_1.HttpResponseError(`Http error response: (${response.status}) ${errorData}`, response.status, response.statusText, errorData, urlWithQuery);
|
|
134
226
|
}
|
|
227
|
+
traceHttp({
|
|
228
|
+
stage: 'response-ok',
|
|
229
|
+
method: methodValue,
|
|
230
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
231
|
+
status: response.status,
|
|
232
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
233
|
+
});
|
|
135
234
|
if (json) {
|
|
136
235
|
return response.json();
|
|
137
236
|
}
|
|
@@ -140,20 +239,70 @@ class HttpBackend {
|
|
|
140
239
|
}
|
|
141
240
|
}
|
|
142
241
|
catch (e) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
242
|
+
// HttpResponseError is an application-level error (status >= 400), not a transport error.
|
|
243
|
+
// Short-circuit before classification to prevent RPC body text (which may contain
|
|
244
|
+
// strings like "connection reset") from being misclassified as a transport error.
|
|
245
|
+
if (e instanceof errors_1.HttpResponseError) {
|
|
246
|
+
traceHttp({
|
|
247
|
+
stage: 'http-response-error',
|
|
248
|
+
method: methodValue,
|
|
249
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
250
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
251
|
+
status: e.status,
|
|
252
|
+
});
|
|
147
253
|
throw e;
|
|
148
254
|
}
|
|
255
|
+
const classified = e instanceof Error ? (0, transport_errors_1.classifyTransportError)(e) : undefined;
|
|
256
|
+
const isRetriableTransport = classified !== undefined && classified.kind !== 'abort' && classified.kind !== 'tls';
|
|
257
|
+
const shouldRetry = attempt < httpRetryCount &&
|
|
258
|
+
isRetriableRequest(methodValue, urlWithQuery) &&
|
|
259
|
+
isRetriableTransport;
|
|
260
|
+
if (shouldRetry) {
|
|
261
|
+
const exponential = httpRetryBaseMs * Math.pow(2, attempt);
|
|
262
|
+
const jitter = Math.floor(Math.random() * httpRetryBaseMs);
|
|
263
|
+
const retryDelayMs = exponential + jitter;
|
|
264
|
+
traceHttp({
|
|
265
|
+
stage: 'request-retry',
|
|
266
|
+
method: methodValue,
|
|
267
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
268
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
269
|
+
attempt: attempt + 1,
|
|
270
|
+
maxAttempts: httpRetryCount + 1,
|
|
271
|
+
retryDelayMs,
|
|
272
|
+
error: toErrorMessage(e),
|
|
273
|
+
transportKind: classified?.kind,
|
|
274
|
+
});
|
|
275
|
+
await sleep(retryDelayMs);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (classified?.kind === 'abort') {
|
|
279
|
+
traceHttp({
|
|
280
|
+
stage: 'timeout',
|
|
281
|
+
method: methodValue,
|
|
282
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
283
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
284
|
+
timeoutMs: timeout,
|
|
285
|
+
});
|
|
286
|
+
throw new errors_1.HttpTimeoutError(timeout, urlWithQuery);
|
|
287
|
+
}
|
|
149
288
|
else {
|
|
150
|
-
|
|
289
|
+
traceHttp({
|
|
290
|
+
stage: 'request-failed',
|
|
291
|
+
method: methodValue,
|
|
292
|
+
url: normalizeTraceUrl(urlWithQuery),
|
|
293
|
+
elapsedMs: Date.now() - requestStartedAt,
|
|
294
|
+
error: toErrorMessage(e),
|
|
295
|
+
transportKind: classified?.kind,
|
|
296
|
+
});
|
|
297
|
+
const cause = e instanceof Error ? e : new Error(String(e));
|
|
298
|
+
throw new errors_1.HttpRequestFailed(methodValue, urlWithQuery, cause, classified);
|
|
151
299
|
}
|
|
152
300
|
}
|
|
153
301
|
finally {
|
|
154
302
|
clearTimeout(t);
|
|
155
303
|
}
|
|
156
|
-
}
|
|
304
|
+
}
|
|
305
|
+
throw new Error('Unexpected request retry flow');
|
|
157
306
|
}
|
|
158
307
|
}
|
|
159
308
|
exports.HttpBackend = HttpBackend;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cross-runtime transport error classifier.
|
|
4
|
+
*
|
|
5
|
+
* Inspects structural properties first (err.code, err.cause.code, err.name),
|
|
6
|
+
* falls back to message substring matching only when structure isn't enough.
|
|
7
|
+
* Handles node-fetch FetchError, native fetch/undici TypeError + cause chain,
|
|
8
|
+
* browser TypeError, Deno, and Bun error shapes.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.classifyTransportError = classifyTransportError;
|
|
12
|
+
const MAY_HAVE_REACHED = {
|
|
13
|
+
abort: false,
|
|
14
|
+
dns: false,
|
|
15
|
+
connect: false,
|
|
16
|
+
socket: true,
|
|
17
|
+
tls: false,
|
|
18
|
+
timeout: true,
|
|
19
|
+
network: true,
|
|
20
|
+
};
|
|
21
|
+
function extractCode(err) {
|
|
22
|
+
if (typeof err !== 'object' || err === null)
|
|
23
|
+
return undefined;
|
|
24
|
+
const code = err['code'];
|
|
25
|
+
return typeof code === 'string' ? code : undefined;
|
|
26
|
+
}
|
|
27
|
+
function extractCause(err) {
|
|
28
|
+
if (typeof err !== 'object' || err === null)
|
|
29
|
+
return undefined;
|
|
30
|
+
return err['cause'];
|
|
31
|
+
}
|
|
32
|
+
function extractCauseCode(err) {
|
|
33
|
+
return extractCode(extractCause(err));
|
|
34
|
+
}
|
|
35
|
+
function extractCauseMessage(err) {
|
|
36
|
+
const cause = extractCause(err);
|
|
37
|
+
if (typeof cause === 'object' && cause !== null) {
|
|
38
|
+
const msg = cause['message'];
|
|
39
|
+
if (typeof msg === 'string')
|
|
40
|
+
return msg.toLowerCase();
|
|
41
|
+
}
|
|
42
|
+
return '';
|
|
43
|
+
}
|
|
44
|
+
function classify(kind, err) {
|
|
45
|
+
return { kind, mayHaveReachedServer: MAY_HAVE_REACHED[kind], original: err };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Classify a thrown value as a transport-level error.
|
|
49
|
+
*
|
|
50
|
+
* Inspects structural properties first (err.code, err.cause.code, err.name),
|
|
51
|
+
* then falls back to message substring matching. Handles node-fetch FetchError,
|
|
52
|
+
* native fetch/undici TypeError + cause chain, browser TypeError, Deno, and Bun.
|
|
53
|
+
*
|
|
54
|
+
* @returns the classification, or `undefined` if the error is not transport-related.
|
|
55
|
+
*/
|
|
56
|
+
function classifyTransportError(err) {
|
|
57
|
+
if (!(err instanceof Error))
|
|
58
|
+
return undefined;
|
|
59
|
+
const code = extractCode(err);
|
|
60
|
+
const causeCode = extractCauseCode(err);
|
|
61
|
+
const msg = err.message.toLowerCase();
|
|
62
|
+
const causeMsg = extractCauseMessage(err);
|
|
63
|
+
// 1. AbortError (our timeout or caller cancel)
|
|
64
|
+
if (err.name === 'AbortError') {
|
|
65
|
+
return classify('abort', err);
|
|
66
|
+
}
|
|
67
|
+
// 2. DNS resolution failure
|
|
68
|
+
if (code === 'ENOTFOUND' ||
|
|
69
|
+
code === 'EAI_AGAIN' ||
|
|
70
|
+
causeCode === 'ENOTFOUND' ||
|
|
71
|
+
causeCode === 'EAI_AGAIN' ||
|
|
72
|
+
msg.includes('enotfound') ||
|
|
73
|
+
msg.includes('eai_again')) {
|
|
74
|
+
return classify('dns', err);
|
|
75
|
+
}
|
|
76
|
+
// 3. Connection refused / connect timeout
|
|
77
|
+
if (code === 'ECONNREFUSED' ||
|
|
78
|
+
causeCode === 'ECONNREFUSED' ||
|
|
79
|
+
causeCode === 'UND_ERR_CONNECT_TIMEOUT' ||
|
|
80
|
+
msg.includes('econnrefused')) {
|
|
81
|
+
return classify('connect', err);
|
|
82
|
+
}
|
|
83
|
+
// 4. TLS errors (check before socket/timeout since TLS errors can also set ECONNRESET)
|
|
84
|
+
// Also inspect cause.message for native fetch which wraps TLS errors in TypeError("fetch failed")
|
|
85
|
+
if (msg.includes('certificate') ||
|
|
86
|
+
msg.includes('self signed') ||
|
|
87
|
+
msg.includes('ssl') ||
|
|
88
|
+
msg.includes('handshake') ||
|
|
89
|
+
causeMsg.includes('certificate') ||
|
|
90
|
+
causeMsg.includes('self signed') ||
|
|
91
|
+
causeMsg.includes('ssl') ||
|
|
92
|
+
causeMsg.includes('handshake')) {
|
|
93
|
+
return classify('tls', err);
|
|
94
|
+
}
|
|
95
|
+
// 5. TCP/connect timeout and undici header/body timeouts
|
|
96
|
+
if (code === 'ETIMEDOUT' ||
|
|
97
|
+
causeCode === 'ETIMEDOUT' ||
|
|
98
|
+
causeCode === 'UND_ERR_HEADERS_TIMEOUT' ||
|
|
99
|
+
causeCode === 'UND_ERR_BODY_TIMEOUT' ||
|
|
100
|
+
msg.includes('etimedout') ||
|
|
101
|
+
msg.includes('timed out')) {
|
|
102
|
+
return classify('timeout', err);
|
|
103
|
+
}
|
|
104
|
+
// 6. Socket-level errors (connection was established or partially established)
|
|
105
|
+
if (code === 'ECONNRESET' ||
|
|
106
|
+
causeCode === 'ECONNRESET' ||
|
|
107
|
+
causeCode === 'UND_ERR_SOCKET' ||
|
|
108
|
+
msg.includes('econnreset') ||
|
|
109
|
+
msg.includes('socket hang up') ||
|
|
110
|
+
msg.includes('other side closed') ||
|
|
111
|
+
msg.includes('closed unexpectedly') ||
|
|
112
|
+
msg.includes('connection reset')) {
|
|
113
|
+
return classify('socket', err);
|
|
114
|
+
}
|
|
115
|
+
// 7. Generic fetch failure (browser TypeError, undici wrapper)
|
|
116
|
+
// "terminated" is undici's wrapper for body-read failures (e.g. response.json() after socket drop)
|
|
117
|
+
if (err instanceof TypeError &&
|
|
118
|
+
(msg.includes('fetch failed') ||
|
|
119
|
+
msg.includes('failed to fetch') ||
|
|
120
|
+
msg.includes('network error') ||
|
|
121
|
+
msg.includes('error sending request') ||
|
|
122
|
+
msg === 'terminated')) {
|
|
123
|
+
return classify('network', err);
|
|
124
|
+
}
|
|
125
|
+
// 8. Remaining node-fetch FetchError.
|
|
126
|
+
// Transport types: 'system' (OS-level), 'request-timeout', 'body-timeout'.
|
|
127
|
+
// Non-transport types: 'invalid-json', 'no-redirect', 'max-size'.
|
|
128
|
+
if (err.name === 'FetchError') {
|
|
129
|
+
const fetchType = err['type'];
|
|
130
|
+
if (fetchType === 'request-timeout' || fetchType === 'body-timeout') {
|
|
131
|
+
return classify('timeout', err);
|
|
132
|
+
}
|
|
133
|
+
if (fetchType === 'system') {
|
|
134
|
+
return classify('network', err);
|
|
135
|
+
}
|
|
136
|
+
// Not a transport error (e.g. JSON parse failure on a 200 response)
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
// Not a transport error
|
|
140
|
+
return undefined;
|
|
141
|
+
}
|
package/dist/lib/version.js
CHANGED
|
@@ -3,6 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.VERSION = void 0;
|
|
4
4
|
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT!
|
|
5
5
|
exports.VERSION = {
|
|
6
|
-
"commitHash": "
|
|
7
|
-
"version": "24.
|
|
6
|
+
"commitHash": "05df48fee92f846cba793920d6fa829afd6a1847",
|
|
7
|
+
"version": "24.3.0-beta.1"
|
|
8
8
|
};
|