@theholocron/http-client 1.54.0 → 1.54.2
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/dist/index.d.mts +8 -0
- package/dist/index.mjs +58 -2
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -94,6 +94,14 @@ interface RestClientConfig {
|
|
|
94
94
|
* Defaults to a no-op.
|
|
95
95
|
*/
|
|
96
96
|
errors?: ErrorSink;
|
|
97
|
+
/**
|
|
98
|
+
* How many times to send a request again after a transport failure that is safe to repeat (see `worthRetrying`):
|
|
99
|
+
* the connection never opened, or a read was dropped. HTTP error statuses are never retried. Defaults to 2; 0
|
|
100
|
+
* turns it off.
|
|
101
|
+
*/
|
|
102
|
+
retries?: number;
|
|
103
|
+
/** Milliseconds before the first retry; each later one waits three times as long. Defaults to 300. */
|
|
104
|
+
retryDelayMs?: number;
|
|
97
105
|
}
|
|
98
106
|
interface RequestOptions {
|
|
99
107
|
method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
|
package/dist/index.mjs
CHANGED
|
@@ -47,12 +47,55 @@ var ProviderApiError = class extends Error {
|
|
|
47
47
|
}
|
|
48
48
|
};
|
|
49
49
|
//#endregion
|
|
50
|
+
//#region src/transport.ts
|
|
51
|
+
/**
|
|
52
|
+
* Node's `fetch` rejects with a bare `TypeError: fetch failed`; the reason (a DNS miss, a connect timeout, a reset) is on
|
|
53
|
+
* `cause`, and for a host with both IPv4 and IPv6 addresses it is one level deeper, in an `AggregateError`.
|
|
54
|
+
*/
|
|
55
|
+
function transportCode(err) {
|
|
56
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
57
|
+
if (!cause || typeof cause !== "object") return void 0;
|
|
58
|
+
const { code, errors } = cause;
|
|
59
|
+
if (typeof code === "string") return code;
|
|
60
|
+
if (Array.isArray(errors)) {
|
|
61
|
+
const inner = errors.find((e) => typeof e?.code === "string");
|
|
62
|
+
if (inner) return inner.code;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Failures that happen before a request reaches the server, so retrying cannot apply it twice, whatever the method. */
|
|
66
|
+
const NEVER_SENT = /* @__PURE__ */ new Set([
|
|
67
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
68
|
+
"ENOTFOUND",
|
|
69
|
+
"EAI_AGAIN",
|
|
70
|
+
"ECONNREFUSED",
|
|
71
|
+
"ENETUNREACH",
|
|
72
|
+
"EHOSTUNREACH"
|
|
73
|
+
]);
|
|
74
|
+
/** Failures after the connection was made, where the server may or may not have seen the request. */
|
|
75
|
+
const DROPPED = /* @__PURE__ */ new Set([
|
|
76
|
+
"ECONNRESET",
|
|
77
|
+
"EPIPE",
|
|
78
|
+
"UND_ERR_SOCKET",
|
|
79
|
+
"ETIMEDOUT"
|
|
80
|
+
]);
|
|
81
|
+
/**
|
|
82
|
+
* Whether a failed request is worth sending again: always when it never left, and for a read when the connection
|
|
83
|
+
* dropped (a repeat read changes nothing). A write that may have arrived is never repeated.
|
|
84
|
+
*/
|
|
85
|
+
function worthRetrying(code, method) {
|
|
86
|
+
if (!code) return false;
|
|
87
|
+
if (NEVER_SENT.has(code)) return true;
|
|
88
|
+
return DROPPED.has(code) && (method === "GET" || method === "HEAD");
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
50
91
|
//#region src/rest-client.ts
|
|
51
92
|
function createRestClient(config) {
|
|
52
93
|
const fetchImpl = config.fetch ?? globalThis.fetch;
|
|
53
94
|
const vendor = config.vendor ?? "";
|
|
54
95
|
const logger = config.logger ?? new NoopLogger();
|
|
55
96
|
const errors = config.errors ?? new NoopErrorSink();
|
|
97
|
+
const maxRetries = config.retries ?? 2;
|
|
98
|
+
const retryDelay = config.retryDelayMs ?? 300;
|
|
56
99
|
let baseUrl = config.baseUrl;
|
|
57
100
|
while (baseUrl.endsWith("/")) baseUrl = baseUrl.slice(0, -1);
|
|
58
101
|
const staticHeaders = { accept: "application/json" };
|
|
@@ -82,10 +125,23 @@ function createRestClient(config) {
|
|
|
82
125
|
path
|
|
83
126
|
}, "request");
|
|
84
127
|
let res;
|
|
85
|
-
try {
|
|
128
|
+
for (let attempt = 0;; attempt++) try {
|
|
86
129
|
res = await fetchImpl(url.toString(), init);
|
|
130
|
+
break;
|
|
87
131
|
} catch (err) {
|
|
88
|
-
const
|
|
132
|
+
const code = transportCode(err);
|
|
133
|
+
if (attempt < maxRetries && worthRetrying(code, init.method)) {
|
|
134
|
+
logger.debug({
|
|
135
|
+
vendor,
|
|
136
|
+
method: init.method,
|
|
137
|
+
path,
|
|
138
|
+
code,
|
|
139
|
+
attempt: attempt + 1
|
|
140
|
+
}, "retrying");
|
|
141
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay * 3 ** attempt));
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const apiErr = new ProviderApiError(`${tag} failed: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}${code ? ` (${code})` : ""}`, 0, void 0);
|
|
89
145
|
errors.captureException(apiErr);
|
|
90
146
|
throw apiErr;
|
|
91
147
|
}
|