@taquito/http-utils 24.2.0 → 24.3.0-beta.0

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 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, mimeType). This method will help users interact with the RPC with a more familiar HTTP format.
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
- mimeType: "text; charset=utf-8",
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
 
@@ -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
- * @description Error that indicates a general failure in making the HTTP request
36
+ * Error that indicates a general failure in making the HTTP request
8
37
  */
9
38
  class HttpRequestFailed extends core_1.NetworkError {
10
- constructor(method, url, cause) {
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
- this.message = `${method} ${url} ${String(cause)}`;
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
- * @description Error thrown when the endpoint returns an HTTP error to the client
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, status, statusText, body, url) {
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
- * @description Error
88
+ * Error thrown when an HTTP request exceeds its configured timeout duration.
39
89
  */
40
90
  class HttpTimeoutError extends core_1.NetworkError {
41
- constructor(timeout, url) {
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
- let fetch = globalThis === null || globalThis === void 0 ? void 0 : globalThis.fetch;
33
- let createAgent;
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 prop = typeof obj[p].toJSON === 'function' ? obj[p].toJSON() : obj[p];
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 contains options to be passed for the HTTP request (url, method and timeout)
110
- * @throws {@link HttpRequestFailed} | {@link HttpResponseError} | {@link HttpTimeoutError}
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(_a, data_1) {
113
- return __awaiter(this, arguments, void 0, function* ({ url, method, timeout = this.timeout, query, headers = {}, json = true }, data) {
114
- // Serializes query params
115
- const urlWithQuery = url + this.serialize(query);
116
- // Adds default header entry if there aren't any Content-Type header
117
- if (!headers['Content-Type']) {
118
- headers['Content-Type'] = 'application/json';
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
- const response = yield fetch(urlWithQuery, Object.assign({ keepalive: false, // Disable keepalive (keepalive defaults to true starting from Node 19 & 20)
202
+ if (!bodySerialized) {
203
+ body = JSON.stringify(data);
204
+ bodySerialized = true;
205
+ }
206
+ const response = await globalThis.fetch(urlWithQuery, {
125
207
  method,
126
- headers, body: JSON.stringify(data), signal: controller.signal }, (useNodeFetchAgent && createAgent ? { agent: createAgent(urlWithQuery) } : {})));
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 = yield response.text();
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
- if (e instanceof Error && e.name === 'AbortError') {
144
- throw new errors_1.HttpTimeoutError(timeout, urlWithQuery);
145
- }
146
- else if (e instanceof errors_1.HttpResponseError) {
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
- throw new errors_1.HttpRequestFailed(String(method), urlWithQuery, e);
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
+ }
@@ -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": "105a7b15cfb862a0732c204e0e9741098d697775",
7
- "version": "24.2.0"
6
+ "commitHash": "27675679db6515e8b092195ef5c58c2c0ea5f5c8",
7
+ "version": "24.3.0-beta.0"
8
8
  };