@twinedo/app-error 1.0.0 → 1.0.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 +137 -79
- package/dist/index.cjs +30 -0
- package/dist/index.d.cts +7 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.js +29 -0
- package/package.json +2 -2
- package/dist/index.mjs +0 -510
package/README.md
CHANGED
|
@@ -1,8 +1,51 @@
|
|
|
1
1
|
# @twinedo/app-error
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Framework-agnostic error normalization for fetch, axios-like clients, and runtime failures.
|
|
4
|
+
|
|
5
|
+
## Why this exists
|
|
6
|
+
|
|
7
|
+
Most teams talk to more than one backend.
|
|
8
|
+
Each backend returns a different error shape (Project A vs Project B).
|
|
9
|
+
fetch and axios surface errors in different ways.
|
|
10
|
+
So every project rewrites the same parsing, message, and retry rules.
|
|
11
|
+
This library produces one predictable AppError for UI, logging, and retries.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Predictable `AppError` shape for UI, logging, and retry logic
|
|
16
|
+
- Works with `fetch` responses and axios-like errors
|
|
17
|
+
- Configurable per backend via `defineErrorPolicy`
|
|
18
|
+
- Framework-agnostic (React, React Native, Vue, Angular, Node)
|
|
19
|
+
- TypeScript-first with exported types
|
|
20
|
+
- Defensive normalization that never throws
|
|
21
|
+
- Retry decision helpers like `isRetryable`
|
|
22
|
+
- Zero dependencies
|
|
23
|
+
|
|
24
|
+
## Guarantees & Non-Goals
|
|
25
|
+
|
|
26
|
+
### Guarantees
|
|
27
|
+
This library guarantees that:
|
|
28
|
+
|
|
29
|
+
- Every normalization function (`toAppError`, `fromFetch`, `fromFetchResponse`) **never throws**
|
|
30
|
+
- You always receive a **predictable `AppError` shape**
|
|
31
|
+
- `message` is always safe to display in UI
|
|
32
|
+
- Original errors are preserved via `cause` for debugging
|
|
33
|
+
- No input error is mutated
|
|
34
|
+
- Behavior is deterministic and side-effect free
|
|
35
|
+
- Fully TypeScript-friendly with stable public types
|
|
36
|
+
|
|
37
|
+
### Non-Goals
|
|
38
|
+
This library intentionally does NOT:
|
|
39
|
+
|
|
40
|
+
- Automatically guess backend-specific error schemas
|
|
41
|
+
- Perform logging, reporting, or analytics
|
|
42
|
+
- Display UI or toast notifications
|
|
43
|
+
- Enforce localization or translations
|
|
44
|
+
- Replace HTTP clients like fetch or axios
|
|
45
|
+
- Hide errors or swallow failures silently
|
|
46
|
+
|
|
47
|
+
If your project has backend-specific error formats, use
|
|
48
|
+
`defineErrorPolicy` to explicitly describe how errors should be interpreted.
|
|
6
49
|
|
|
7
50
|
## Install
|
|
8
51
|
|
|
@@ -10,119 +53,134 @@ standardize UI messaging, retry logic, and logging across mixed stacks.
|
|
|
10
53
|
npm install @twinedo/app-error
|
|
11
54
|
```
|
|
12
55
|
|
|
13
|
-
|
|
56
|
+
Published as the scoped package `@twinedo/app-error`. Ships ESM + CJS builds
|
|
57
|
+
with TypeScript types and works in Node and browser runtimes.
|
|
14
58
|
|
|
15
|
-
|
|
16
|
-
normalizes them into a single `AppError` so your app can consistently:
|
|
17
|
-
- show a safe UI message
|
|
18
|
-
- decide whether to retry
|
|
19
|
-
- log a stable fingerprint
|
|
59
|
+
## Examples
|
|
20
60
|
|
|
21
|
-
|
|
61
|
+
### Example 1 — Axios with try/catch
|
|
22
62
|
|
|
23
63
|
```ts
|
|
24
64
|
import axios from "axios";
|
|
25
|
-
import { toAppError } from "@twinedo/app-error";
|
|
65
|
+
import { defineErrorPolicy, isRetryable, toAppError } from "@twinedo/app-error";
|
|
66
|
+
|
|
67
|
+
const policy = defineErrorPolicy();
|
|
26
68
|
|
|
27
69
|
try {
|
|
28
|
-
await axios.get("/api/user");
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
70
|
+
const response = await axios.get<{ id: string; name: string }>("/api/user");
|
|
71
|
+
console.log("User:", response.data.name);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
const appError = toAppError(err, policy);
|
|
74
|
+
console.error(appError.message);
|
|
75
|
+
|
|
76
|
+
if (isRetryable(appError)) {
|
|
77
|
+
// show a retry action or schedule a retry
|
|
78
|
+
}
|
|
32
79
|
}
|
|
33
80
|
```
|
|
34
81
|
|
|
35
|
-
|
|
82
|
+
### Example 2 — Fetch handling non-OK responses
|
|
36
83
|
|
|
37
84
|
```ts
|
|
38
|
-
import {
|
|
85
|
+
import { defineErrorPolicy, fromFetchResponse, toAppError } from "@twinedo/app-error";
|
|
86
|
+
|
|
87
|
+
const policy = defineErrorPolicy();
|
|
39
88
|
|
|
40
|
-
|
|
41
|
-
const
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch("/api/user");
|
|
91
|
+
|
|
92
|
+
if (!res.ok) {
|
|
93
|
+
throw await fromFetchResponse(res, policy);
|
|
94
|
+
}
|
|
42
95
|
|
|
43
|
-
|
|
44
|
-
|
|
96
|
+
const data = await res.json();
|
|
97
|
+
console.log("User:", data);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const appError = toAppError(err, policy);
|
|
100
|
+
console.error(appError.message);
|
|
45
101
|
}
|
|
46
102
|
```
|
|
47
103
|
|
|
48
|
-
|
|
104
|
+
### Example 3 — Project A vs Project B backend policies
|
|
49
105
|
|
|
50
106
|
```ts
|
|
51
|
-
import
|
|
107
|
+
import axios from "axios";
|
|
108
|
+
import { defineErrorPolicy, toAppError } from "@twinedo/app-error";
|
|
109
|
+
|
|
110
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
111
|
+
typeof value === "object" && value !== null;
|
|
52
112
|
|
|
113
|
+
const readString = (value: unknown): string | undefined =>
|
|
114
|
+
typeof value === "string" ? value : undefined;
|
|
115
|
+
|
|
116
|
+
const readHeader = (headers: unknown, name: string): string | undefined => {
|
|
117
|
+
if (!headers) return undefined;
|
|
118
|
+
const getter = (headers as { get?: (key: string) => string | null | undefined })
|
|
119
|
+
.get;
|
|
120
|
+
if (typeof getter === "function") {
|
|
121
|
+
return getter.call(headers, name) ?? undefined;
|
|
122
|
+
}
|
|
123
|
+
return readString((headers as Record<string, unknown>)[name]);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Project A (Tony backend): { error: { message, code } }, header x-request-id
|
|
53
127
|
const projectAPolicy = defineErrorPolicy({
|
|
54
128
|
http: {
|
|
55
|
-
message: (data) =>
|
|
56
|
-
|
|
57
|
-
|
|
129
|
+
message: (data) =>
|
|
130
|
+
isRecord(data) && isRecord(data.error)
|
|
131
|
+
? readString(data.error.message)
|
|
132
|
+
: undefined,
|
|
133
|
+
code: (data) =>
|
|
134
|
+
isRecord(data) && isRecord(data.error)
|
|
135
|
+
? readString(data.error.code)
|
|
136
|
+
: undefined,
|
|
137
|
+
requestId: (headers) => readHeader(headers, "x-request-id"),
|
|
58
138
|
},
|
|
59
139
|
});
|
|
60
140
|
|
|
141
|
+
// Project B (Bobby backend): { message | msg, code }, header x-correlation-id
|
|
61
142
|
const projectBPolicy = defineErrorPolicy({
|
|
62
143
|
http: {
|
|
63
144
|
message: (data) =>
|
|
64
|
-
|
|
65
|
-
?
|
|
66
|
-
: undefined,
|
|
67
|
-
code: (data) =>
|
|
68
|
-
typeof data === "object" && data && "errorCode" in data
|
|
69
|
-
? String((data as { errorCode: unknown }).errorCode)
|
|
145
|
+
isRecord(data)
|
|
146
|
+
? readString(data.message) ?? readString(data.msg)
|
|
70
147
|
: undefined,
|
|
71
|
-
|
|
72
|
-
|
|
148
|
+
code: (data) => (isRecord(data) ? readString(data.code) : undefined),
|
|
149
|
+
requestId: (headers) => readHeader(headers, "x-correlation-id"),
|
|
73
150
|
},
|
|
74
151
|
});
|
|
75
|
-
```
|
|
76
152
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (!mounted) return;
|
|
96
|
-
setError(toAppError(err));
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
return () => {
|
|
100
|
-
mounted = false;
|
|
101
|
-
};
|
|
102
|
-
}, []);
|
|
103
|
-
|
|
104
|
-
if (!error) return null;
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<>
|
|
108
|
-
<Text>{error.message}</Text>
|
|
109
|
-
{isRetryable(error) ? <Button title="Retry" /> : null}
|
|
110
|
-
</>
|
|
111
|
-
);
|
|
153
|
+
const handleError = (
|
|
154
|
+
err: unknown,
|
|
155
|
+
policy: ReturnType<typeof defineErrorPolicy>
|
|
156
|
+
) => {
|
|
157
|
+
const appError = toAppError(err, policy);
|
|
158
|
+
console.error(appError.message, appError.code, appError.requestId);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
await axios.get("/api/user");
|
|
163
|
+
} catch (err) {
|
|
164
|
+
handleError(err, projectAPolicy);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
await axios.get("/api/user");
|
|
169
|
+
} catch (err) {
|
|
170
|
+
handleError(err, projectBPolicy);
|
|
112
171
|
}
|
|
113
172
|
```
|
|
114
173
|
|
|
115
|
-
|
|
174
|
+
### Example 4 — attempt() helper
|
|
116
175
|
|
|
117
176
|
```ts
|
|
118
|
-
import {
|
|
177
|
+
import { attempt } from "@twinedo/app-error";
|
|
119
178
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
}
|
|
179
|
+
const result = await attempt(() => apiCall());
|
|
180
|
+
|
|
181
|
+
if (result.ok) {
|
|
182
|
+
console.log("Data:", result.data);
|
|
183
|
+
} else {
|
|
184
|
+
console.error(result.error.message);
|
|
127
185
|
}
|
|
128
186
|
```
|
package/dist/index.cjs
CHANGED
|
@@ -24,6 +24,7 @@ __export(index_exports, {
|
|
|
24
24
|
defineErrorPolicy: () => defineErrorPolicy,
|
|
25
25
|
errorKey: () => errorKey,
|
|
26
26
|
fromFetch: () => fromFetch,
|
|
27
|
+
fromFetchResponse: () => fromFetchResponse,
|
|
27
28
|
isAppError: () => isAppError,
|
|
28
29
|
isRetryable: () => isRetryable,
|
|
29
30
|
toAppError: () => toAppError
|
|
@@ -237,6 +238,34 @@ var fromFetch = (response, body, policy) => {
|
|
|
237
238
|
};
|
|
238
239
|
};
|
|
239
240
|
|
|
241
|
+
// src/fromFetchResponse.ts
|
|
242
|
+
var readResponseBody = async (response) => {
|
|
243
|
+
const reader = response.text;
|
|
244
|
+
if (typeof reader !== "function") return void 0;
|
|
245
|
+
if (response.bodyUsed) return void 0;
|
|
246
|
+
let text;
|
|
247
|
+
try {
|
|
248
|
+
text = await reader.call(response);
|
|
249
|
+
} catch {
|
|
250
|
+
return void 0;
|
|
251
|
+
}
|
|
252
|
+
if (text.trim().length === 0) return void 0;
|
|
253
|
+
try {
|
|
254
|
+
return JSON.parse(text);
|
|
255
|
+
} catch {
|
|
256
|
+
return text;
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
var fromFetchResponse = async (response, policy) => {
|
|
260
|
+
const safeResponse = response ?? {};
|
|
261
|
+
try {
|
|
262
|
+
const body = await readResponseBody(safeResponse);
|
|
263
|
+
return fromFetch(safeResponse, body, policy);
|
|
264
|
+
} catch {
|
|
265
|
+
return fromFetch(safeResponse, void 0, policy);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
240
269
|
// src/adapters/axiosLike.ts
|
|
241
270
|
var TIMEOUT_CODES = /* @__PURE__ */ new Set(["ECONNABORTED", "ETIMEDOUT", "ESOCKETTIMEDOUT"]);
|
|
242
271
|
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
@@ -546,6 +575,7 @@ var attempt = async (fn, policy) => {
|
|
|
546
575
|
defineErrorPolicy,
|
|
547
576
|
errorKey,
|
|
548
577
|
fromFetch,
|
|
578
|
+
fromFetchResponse,
|
|
549
579
|
isAppError,
|
|
550
580
|
isRetryable,
|
|
551
581
|
toAppError
|
package/dist/index.d.cts
CHANGED
|
@@ -41,6 +41,12 @@ type FetchResponseLike = {
|
|
|
41
41
|
};
|
|
42
42
|
declare const fromFetch: (response: FetchResponseLike, body?: unknown, policy?: ErrorPolicy) => AppError;
|
|
43
43
|
|
|
44
|
+
type FetchResponseWithBody = FetchResponseLike & {
|
|
45
|
+
text?: () => Promise<string>;
|
|
46
|
+
bodyUsed?: boolean;
|
|
47
|
+
};
|
|
48
|
+
declare const fromFetchResponse: (response: FetchResponseWithBody, policy?: ErrorPolicy) => Promise<AppError>;
|
|
49
|
+
|
|
44
50
|
declare const toAppError: (error: unknown, policy?: ErrorPolicy) => AppError;
|
|
45
51
|
|
|
46
52
|
type AttemptResult<T> = {
|
|
@@ -55,4 +61,4 @@ declare const errorKey: (error: AppError | unknown) => string;
|
|
|
55
61
|
declare const isRetryable: (error: AppError | unknown) => boolean;
|
|
56
62
|
declare const attempt: <T>(fn: () => T | Promise<T>, policy?: ErrorPolicy) => Promise<AttemptResult<T>>;
|
|
57
63
|
|
|
58
|
-
export { type AppError, type AppErrorKind, type ErrorPolicy, type HeadersLike, type HttpPolicy, type HttpResponseLike, type NormalizedErrorPolicy, attempt, defineErrorPolicy, errorKey, fromFetch, isAppError, isRetryable, toAppError };
|
|
64
|
+
export { type AppError, type AppErrorKind, type ErrorPolicy, type HeadersLike, type HttpPolicy, type HttpResponseLike, type NormalizedErrorPolicy, attempt, defineErrorPolicy, errorKey, fromFetch, fromFetchResponse, isAppError, isRetryable, toAppError };
|
package/dist/index.d.ts
CHANGED
|
@@ -41,6 +41,12 @@ type FetchResponseLike = {
|
|
|
41
41
|
};
|
|
42
42
|
declare const fromFetch: (response: FetchResponseLike, body?: unknown, policy?: ErrorPolicy) => AppError;
|
|
43
43
|
|
|
44
|
+
type FetchResponseWithBody = FetchResponseLike & {
|
|
45
|
+
text?: () => Promise<string>;
|
|
46
|
+
bodyUsed?: boolean;
|
|
47
|
+
};
|
|
48
|
+
declare const fromFetchResponse: (response: FetchResponseWithBody, policy?: ErrorPolicy) => Promise<AppError>;
|
|
49
|
+
|
|
44
50
|
declare const toAppError: (error: unknown, policy?: ErrorPolicy) => AppError;
|
|
45
51
|
|
|
46
52
|
type AttemptResult<T> = {
|
|
@@ -55,4 +61,4 @@ declare const errorKey: (error: AppError | unknown) => string;
|
|
|
55
61
|
declare const isRetryable: (error: AppError | unknown) => boolean;
|
|
56
62
|
declare const attempt: <T>(fn: () => T | Promise<T>, policy?: ErrorPolicy) => Promise<AttemptResult<T>>;
|
|
57
63
|
|
|
58
|
-
export { type AppError, type AppErrorKind, type ErrorPolicy, type HeadersLike, type HttpPolicy, type HttpResponseLike, type NormalizedErrorPolicy, attempt, defineErrorPolicy, errorKey, fromFetch, isAppError, isRetryable, toAppError };
|
|
64
|
+
export { type AppError, type AppErrorKind, type ErrorPolicy, type HeadersLike, type HttpPolicy, type HttpResponseLike, type NormalizedErrorPolicy, attempt, defineErrorPolicy, errorKey, fromFetch, fromFetchResponse, isAppError, isRetryable, toAppError };
|
package/dist/index.js
CHANGED
|
@@ -205,6 +205,34 @@ var fromFetch = (response, body, policy) => {
|
|
|
205
205
|
};
|
|
206
206
|
};
|
|
207
207
|
|
|
208
|
+
// src/fromFetchResponse.ts
|
|
209
|
+
var readResponseBody = async (response) => {
|
|
210
|
+
const reader = response.text;
|
|
211
|
+
if (typeof reader !== "function") return void 0;
|
|
212
|
+
if (response.bodyUsed) return void 0;
|
|
213
|
+
let text;
|
|
214
|
+
try {
|
|
215
|
+
text = await reader.call(response);
|
|
216
|
+
} catch {
|
|
217
|
+
return void 0;
|
|
218
|
+
}
|
|
219
|
+
if (text.trim().length === 0) return void 0;
|
|
220
|
+
try {
|
|
221
|
+
return JSON.parse(text);
|
|
222
|
+
} catch {
|
|
223
|
+
return text;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
var fromFetchResponse = async (response, policy) => {
|
|
227
|
+
const safeResponse = response ?? {};
|
|
228
|
+
try {
|
|
229
|
+
const body = await readResponseBody(safeResponse);
|
|
230
|
+
return fromFetch(safeResponse, body, policy);
|
|
231
|
+
} catch {
|
|
232
|
+
return fromFetch(safeResponse, void 0, policy);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
208
236
|
// src/adapters/axiosLike.ts
|
|
209
237
|
var TIMEOUT_CODES = /* @__PURE__ */ new Set(["ECONNABORTED", "ETIMEDOUT", "ESOCKETTIMEDOUT"]);
|
|
210
238
|
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
@@ -513,6 +541,7 @@ export {
|
|
|
513
541
|
defineErrorPolicy,
|
|
514
542
|
errorKey,
|
|
515
543
|
fromFetch,
|
|
544
|
+
fromFetchResponse,
|
|
516
545
|
isAppError,
|
|
517
546
|
isRetryable,
|
|
518
547
|
toAppError
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@twinedo/app-error",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "A configurable error normalization layer for fetch, axios-like, and runtime errors.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"module": "dist/index.js",
|
|
@@ -40,4 +40,4 @@
|
|
|
40
40
|
"url": "https://github.com/twinedo/app-error/issues"
|
|
41
41
|
},
|
|
42
42
|
"homepage": "https://github.com/twinedo/app-error#readme"
|
|
43
|
-
}
|
|
43
|
+
}
|
package/dist/index.mjs
DELETED
|
@@ -1,510 +0,0 @@
|
|
|
1
|
-
// src/policy.ts
|
|
2
|
-
var DEFAULT_REQUEST_ID_HEADERS = [
|
|
3
|
-
"x-request-id",
|
|
4
|
-
"x-correlation-id",
|
|
5
|
-
"x-trace-id",
|
|
6
|
-
"traceparent",
|
|
7
|
-
"x-amzn-trace-id"
|
|
8
|
-
];
|
|
9
|
-
var isRecord = (value) => typeof value === "object" && value !== null;
|
|
10
|
-
var normalizeString = (value) => {
|
|
11
|
-
if (typeof value !== "string") return void 0;
|
|
12
|
-
const trimmed = value.trim();
|
|
13
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
14
|
-
};
|
|
15
|
-
var normalizeHeaderValue = (value) => {
|
|
16
|
-
if (typeof value === "number") return String(value);
|
|
17
|
-
if (typeof value === "string") return value;
|
|
18
|
-
return void 0;
|
|
19
|
-
};
|
|
20
|
-
var getHeaderValue = (headers, name) => {
|
|
21
|
-
if (!headers) return void 0;
|
|
22
|
-
const lowerName = name.toLowerCase();
|
|
23
|
-
const getter = headers.get;
|
|
24
|
-
if (typeof getter === "function") {
|
|
25
|
-
const value = getter.call(headers, name) ?? getter.call(headers, lowerName);
|
|
26
|
-
const normalized = normalizeString(value);
|
|
27
|
-
if (normalized) return normalized;
|
|
28
|
-
}
|
|
29
|
-
if (isRecord(headers)) {
|
|
30
|
-
for (const key of Object.keys(headers)) {
|
|
31
|
-
if (key.toLowerCase() !== lowerName) continue;
|
|
32
|
-
const raw = headers[key];
|
|
33
|
-
if (Array.isArray(raw)) {
|
|
34
|
-
const first = normalizeHeaderValue(raw[0]);
|
|
35
|
-
const normalized = normalizeString(first);
|
|
36
|
-
if (normalized) return normalized;
|
|
37
|
-
} else {
|
|
38
|
-
const normalized = normalizeString(normalizeHeaderValue(raw));
|
|
39
|
-
if (normalized) return normalized;
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return void 0;
|
|
44
|
-
};
|
|
45
|
-
var extractString = (value) => normalizeString(value);
|
|
46
|
-
var extractFromArray = (value) => {
|
|
47
|
-
for (const item of value) {
|
|
48
|
-
const message = extractMessageFromData(item);
|
|
49
|
-
if (message) return message;
|
|
50
|
-
}
|
|
51
|
-
return void 0;
|
|
52
|
-
};
|
|
53
|
-
var extractMessageFromData = (data) => {
|
|
54
|
-
const direct = extractString(data);
|
|
55
|
-
if (direct) return direct;
|
|
56
|
-
if (Array.isArray(data)) return extractFromArray(data);
|
|
57
|
-
if (!isRecord(data)) return void 0;
|
|
58
|
-
const directKeys = ["message", "error", "detail", "title", "description"];
|
|
59
|
-
for (const key of directKeys) {
|
|
60
|
-
const value = extractString(data[key]);
|
|
61
|
-
if (value) return value;
|
|
62
|
-
}
|
|
63
|
-
const errorValue = data.error;
|
|
64
|
-
if (isRecord(errorValue)) {
|
|
65
|
-
const nested = extractString(errorValue.message) ?? extractString(errorValue.detail);
|
|
66
|
-
if (nested) return nested;
|
|
67
|
-
}
|
|
68
|
-
const errorsValue = data.errors;
|
|
69
|
-
if (Array.isArray(errorsValue)) {
|
|
70
|
-
const nested = extractFromArray(errorsValue);
|
|
71
|
-
if (nested) return nested;
|
|
72
|
-
}
|
|
73
|
-
if (isRecord(errorsValue)) {
|
|
74
|
-
for (const key of Object.keys(errorsValue)) {
|
|
75
|
-
const fieldValue = errorsValue[key];
|
|
76
|
-
if (Array.isArray(fieldValue)) {
|
|
77
|
-
const nested = extractFromArray(fieldValue);
|
|
78
|
-
if (nested) return nested;
|
|
79
|
-
} else {
|
|
80
|
-
const nested = extractString(fieldValue);
|
|
81
|
-
if (nested) return nested;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
return void 0;
|
|
86
|
-
};
|
|
87
|
-
var extractCodeFromData = (data) => {
|
|
88
|
-
if (Array.isArray(data)) {
|
|
89
|
-
for (const item of data) {
|
|
90
|
-
const code = extractCodeFromData(item);
|
|
91
|
-
if (code) return code;
|
|
92
|
-
}
|
|
93
|
-
return void 0;
|
|
94
|
-
}
|
|
95
|
-
if (!isRecord(data)) return void 0;
|
|
96
|
-
const directKeys = ["code", "errorCode", "error_code"];
|
|
97
|
-
for (const key of directKeys) {
|
|
98
|
-
const value = extractString(data[key]);
|
|
99
|
-
if (value) return value;
|
|
100
|
-
}
|
|
101
|
-
const errorValue = data.error;
|
|
102
|
-
if (isRecord(errorValue)) {
|
|
103
|
-
const nested = extractString(errorValue.code) ?? extractString(errorValue.errorCode);
|
|
104
|
-
if (nested) return nested;
|
|
105
|
-
}
|
|
106
|
-
return void 0;
|
|
107
|
-
};
|
|
108
|
-
var defaultHttpMessage = (data, response) => {
|
|
109
|
-
const fromData = extractMessageFromData(data);
|
|
110
|
-
if (fromData) return fromData;
|
|
111
|
-
return extractString(response?.statusText);
|
|
112
|
-
};
|
|
113
|
-
var defaultHttpCode = (data) => extractCodeFromData(data);
|
|
114
|
-
var defaultRequestId = (headers) => {
|
|
115
|
-
for (const header of DEFAULT_REQUEST_ID_HEADERS) {
|
|
116
|
-
const value = getHeaderValue(headers, header);
|
|
117
|
-
if (value) return value;
|
|
118
|
-
}
|
|
119
|
-
return void 0;
|
|
120
|
-
};
|
|
121
|
-
var defaultHttpRetryable = (status) => {
|
|
122
|
-
if (typeof status !== "number") return false;
|
|
123
|
-
return status >= 500 && status <= 599;
|
|
124
|
-
};
|
|
125
|
-
var DEFAULT_HTTP_POLICY = {
|
|
126
|
-
message: defaultHttpMessage,
|
|
127
|
-
code: defaultHttpCode,
|
|
128
|
-
requestId: defaultRequestId,
|
|
129
|
-
retryable: defaultHttpRetryable
|
|
130
|
-
};
|
|
131
|
-
var defineErrorPolicy = (...configs) => {
|
|
132
|
-
const merged = {};
|
|
133
|
-
for (const config of configs) {
|
|
134
|
-
if (!config?.http) continue;
|
|
135
|
-
Object.assign(merged, config.http);
|
|
136
|
-
}
|
|
137
|
-
return {
|
|
138
|
-
http: {
|
|
139
|
-
message: merged.message ?? DEFAULT_HTTP_POLICY.message,
|
|
140
|
-
code: merged.code ?? DEFAULT_HTTP_POLICY.code,
|
|
141
|
-
requestId: merged.requestId ?? DEFAULT_HTTP_POLICY.requestId,
|
|
142
|
-
retryable: merged.retryable ?? DEFAULT_HTTP_POLICY.retryable
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
// src/types.ts
|
|
148
|
-
var APP_ERROR_KINDS = {
|
|
149
|
-
http: true,
|
|
150
|
-
network: true,
|
|
151
|
-
timeout: true,
|
|
152
|
-
parse: true,
|
|
153
|
-
validation: true,
|
|
154
|
-
unknown: true
|
|
155
|
-
};
|
|
156
|
-
var isRecord2 = (value) => typeof value === "object" && value !== null;
|
|
157
|
-
var isAppError = (value) => {
|
|
158
|
-
if (!isRecord2(value)) return false;
|
|
159
|
-
const kind = value.kind;
|
|
160
|
-
if (typeof kind !== "string" || !(kind in APP_ERROR_KINDS)) return false;
|
|
161
|
-
return typeof value.message === "string";
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
// src/fromFetch.ts
|
|
165
|
-
var DEFAULT_MESSAGE = "Something went wrong";
|
|
166
|
-
var normalizeMessage = (value) => {
|
|
167
|
-
if (typeof value !== "string") return void 0;
|
|
168
|
-
const trimmed = value.trim();
|
|
169
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
170
|
-
};
|
|
171
|
-
var defaultRetryable = (status) => {
|
|
172
|
-
if (typeof status !== "number") return false;
|
|
173
|
-
return status >= 500 && status <= 599;
|
|
174
|
-
};
|
|
175
|
-
var safeInvoke = (fn) => {
|
|
176
|
-
try {
|
|
177
|
-
return fn();
|
|
178
|
-
} catch {
|
|
179
|
-
return void 0;
|
|
180
|
-
}
|
|
181
|
-
};
|
|
182
|
-
var fromFetch = (response, body, policy) => {
|
|
183
|
-
const resolvedPolicy = defineErrorPolicy(policy);
|
|
184
|
-
const status = typeof response.status === "number" ? response.status : void 0;
|
|
185
|
-
const message = normalizeMessage(
|
|
186
|
-
safeInvoke(() => resolvedPolicy.http.message(body, response))
|
|
187
|
-
) ?? DEFAULT_MESSAGE;
|
|
188
|
-
const code = normalizeMessage(
|
|
189
|
-
safeInvoke(() => resolvedPolicy.http.code(body, response))
|
|
190
|
-
);
|
|
191
|
-
const requestId = normalizeMessage(
|
|
192
|
-
safeInvoke(() => resolvedPolicy.http.requestId(response.headers))
|
|
193
|
-
);
|
|
194
|
-
const retryable = safeInvoke(() => resolvedPolicy.http.retryable(status)) ?? defaultRetryable(status);
|
|
195
|
-
return {
|
|
196
|
-
kind: "http",
|
|
197
|
-
message,
|
|
198
|
-
retryable,
|
|
199
|
-
...status !== void 0 ? { status } : {},
|
|
200
|
-
...code ? { code } : {},
|
|
201
|
-
...requestId ? { requestId } : {},
|
|
202
|
-
...body !== void 0 ? { details: body } : {},
|
|
203
|
-
...response !== void 0 ? { cause: response } : {}
|
|
204
|
-
};
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
// src/adapters/axiosLike.ts
|
|
208
|
-
var TIMEOUT_CODES = /* @__PURE__ */ new Set(["ECONNABORTED", "ETIMEDOUT", "ESOCKETTIMEDOUT"]);
|
|
209
|
-
var NETWORK_CODES = /* @__PURE__ */ new Set([
|
|
210
|
-
"ERR_NETWORK",
|
|
211
|
-
"ENOTFOUND",
|
|
212
|
-
"ECONNREFUSED",
|
|
213
|
-
"ECONNRESET",
|
|
214
|
-
"EAI_AGAIN",
|
|
215
|
-
"ETIMEDOUT",
|
|
216
|
-
"EHOSTUNREACH",
|
|
217
|
-
"ENETUNREACH"
|
|
218
|
-
]);
|
|
219
|
-
var isRecord3 = (value) => typeof value === "object" && value !== null;
|
|
220
|
-
var getString = (value) => typeof value === "string" ? value : void 0;
|
|
221
|
-
var getStatus = (value) => typeof value === "number" ? value : void 0;
|
|
222
|
-
var getResponseLike = (value) => {
|
|
223
|
-
if (!isRecord3(value)) return void 0;
|
|
224
|
-
return {
|
|
225
|
-
status: getStatus(value.status),
|
|
226
|
-
statusText: getString(value.statusText),
|
|
227
|
-
data: value.data,
|
|
228
|
-
headers: value.headers
|
|
229
|
-
};
|
|
230
|
-
};
|
|
231
|
-
var getAxiosLikeErrorInfo = (error) => {
|
|
232
|
-
if (!isRecord3(error)) return null;
|
|
233
|
-
const isAxiosMarker = error.isAxiosError === true;
|
|
234
|
-
const response = getResponseLike(error.response);
|
|
235
|
-
const request = error.request;
|
|
236
|
-
const looksAxios = isAxiosMarker || response !== void 0 || request !== void 0;
|
|
237
|
-
if (!looksAxios) return null;
|
|
238
|
-
const code = getString(error.code);
|
|
239
|
-
const message = getString(error.message);
|
|
240
|
-
const status = response?.status;
|
|
241
|
-
const data = response?.data;
|
|
242
|
-
const headers = response?.headers;
|
|
243
|
-
const messageLower = message?.toLowerCase();
|
|
244
|
-
const isTimeout = (code ? TIMEOUT_CODES.has(code) : false) || (messageLower ? messageLower.includes("timeout") : false);
|
|
245
|
-
const isNetworkError2 = !response && (request !== void 0 || (code ? NETWORK_CODES.has(code) : false) || (messageLower ? messageLower.includes("network error") : false));
|
|
246
|
-
return {
|
|
247
|
-
response,
|
|
248
|
-
status,
|
|
249
|
-
data,
|
|
250
|
-
headers,
|
|
251
|
-
code,
|
|
252
|
-
message,
|
|
253
|
-
isTimeout,
|
|
254
|
-
isNetworkError: isNetworkError2
|
|
255
|
-
};
|
|
256
|
-
};
|
|
257
|
-
var toHttpResponseLike = (info) => {
|
|
258
|
-
if (!info.response) return void 0;
|
|
259
|
-
return {
|
|
260
|
-
status: info.status,
|
|
261
|
-
statusText: info.response.statusText,
|
|
262
|
-
headers: info.headers
|
|
263
|
-
};
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
// src/toAppError.ts
|
|
267
|
-
var DEFAULT_MESSAGE2 = "Something went wrong";
|
|
268
|
-
var NETWORK_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
269
|
-
"ENOTFOUND",
|
|
270
|
-
"ECONNREFUSED",
|
|
271
|
-
"ECONNRESET",
|
|
272
|
-
"EAI_AGAIN",
|
|
273
|
-
"EHOSTUNREACH",
|
|
274
|
-
"ENETUNREACH",
|
|
275
|
-
"ERR_NETWORK"
|
|
276
|
-
]);
|
|
277
|
-
var TIMEOUT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
278
|
-
"ETIMEDOUT",
|
|
279
|
-
"ESOCKETTIMEDOUT",
|
|
280
|
-
"ECONNABORTED"
|
|
281
|
-
]);
|
|
282
|
-
var isRecord4 = (value) => typeof value === "object" && value !== null;
|
|
283
|
-
var getString2 = (value) => typeof value === "string" ? value : void 0;
|
|
284
|
-
var normalizeMessage2 = (value) => {
|
|
285
|
-
if (typeof value !== "string") return void 0;
|
|
286
|
-
const trimmed = value.trim();
|
|
287
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
288
|
-
};
|
|
289
|
-
var defaultRetryable2 = (kind, status) => {
|
|
290
|
-
if (kind === "network") return true;
|
|
291
|
-
if (kind === "http" && typeof status === "number") {
|
|
292
|
-
return status >= 500 && status <= 599;
|
|
293
|
-
}
|
|
294
|
-
return false;
|
|
295
|
-
};
|
|
296
|
-
var safeInvoke2 = (fn) => {
|
|
297
|
-
try {
|
|
298
|
-
return fn();
|
|
299
|
-
} catch {
|
|
300
|
-
return void 0;
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
var getErrorInfo = (error) => {
|
|
304
|
-
if (!isRecord4(error)) return { name: void 0, message: void 0, code: void 0 };
|
|
305
|
-
return {
|
|
306
|
-
name: getString2(error.name),
|
|
307
|
-
message: getString2(error.message),
|
|
308
|
-
code: getString2(error.code)
|
|
309
|
-
};
|
|
310
|
-
};
|
|
311
|
-
var isTimeoutError = (name, message, code) => {
|
|
312
|
-
if (name === "AbortError") return true;
|
|
313
|
-
if (code && TIMEOUT_ERROR_CODES.has(code)) return true;
|
|
314
|
-
const lowered = message?.toLowerCase();
|
|
315
|
-
return lowered ? lowered.includes("timeout") : false;
|
|
316
|
-
};
|
|
317
|
-
var isNetworkError = (name, message, code) => {
|
|
318
|
-
if (name === "TypeError") {
|
|
319
|
-
const lowered2 = message?.toLowerCase() ?? "";
|
|
320
|
-
if (lowered2.includes("failed to fetch") || lowered2.includes("network request failed") || lowered2.includes("networkerror") || lowered2.includes("load failed")) {
|
|
321
|
-
return true;
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
if (code && NETWORK_ERROR_CODES.has(code)) return true;
|
|
325
|
-
const lowered = message?.toLowerCase() ?? "";
|
|
326
|
-
return lowered.includes("network error");
|
|
327
|
-
};
|
|
328
|
-
var isParseError = (name) => name === "SyntaxError";
|
|
329
|
-
var isValidationError = (error, name) => {
|
|
330
|
-
if (name && name.toLowerCase().includes("validation")) return true;
|
|
331
|
-
if (!isRecord4(error)) return false;
|
|
332
|
-
return Array.isArray(error.issues) || Array.isArray(error.errors);
|
|
333
|
-
};
|
|
334
|
-
var normalizeExisting = (error) => {
|
|
335
|
-
const message = normalizeMessage2(error.message) ?? DEFAULT_MESSAGE2;
|
|
336
|
-
const retryable = typeof error.retryable === "boolean" ? error.retryable : defaultRetryable2(error.kind, error.status);
|
|
337
|
-
return {
|
|
338
|
-
...error,
|
|
339
|
-
message,
|
|
340
|
-
retryable
|
|
341
|
-
};
|
|
342
|
-
};
|
|
343
|
-
var buildAppError = (options) => ({
|
|
344
|
-
kind: options.kind,
|
|
345
|
-
message: options.message,
|
|
346
|
-
retryable: options.retryable,
|
|
347
|
-
...options.status !== void 0 ? { status: options.status } : {},
|
|
348
|
-
...options.code ? { code: options.code } : {},
|
|
349
|
-
...options.requestId ? { requestId: options.requestId } : {},
|
|
350
|
-
...options.details !== void 0 ? { details: options.details } : {},
|
|
351
|
-
...options.cause !== void 0 ? { cause: options.cause } : {}
|
|
352
|
-
});
|
|
353
|
-
var fromStatusObject = (error, policy) => {
|
|
354
|
-
if (!isRecord4(error)) return null;
|
|
355
|
-
if (typeof error.status !== "number" || error.status < 400) return null;
|
|
356
|
-
const response = {
|
|
357
|
-
status: error.status,
|
|
358
|
-
statusText: getString2(error.statusText),
|
|
359
|
-
headers: error.headers
|
|
360
|
-
};
|
|
361
|
-
const details = error.data !== void 0 ? error.data : error.body !== void 0 ? error.body : error.details;
|
|
362
|
-
const message = normalizeMessage2(safeInvoke2(() => policy.http.message(details, response))) ?? DEFAULT_MESSAGE2;
|
|
363
|
-
const code = safeInvoke2(() => policy.http.code(details, response));
|
|
364
|
-
const requestId = safeInvoke2(() => policy.http.requestId(response.headers));
|
|
365
|
-
const retryable = safeInvoke2(() => policy.http.retryable(error.status)) ?? defaultRetryable2("http", error.status);
|
|
366
|
-
return buildAppError({
|
|
367
|
-
kind: "http",
|
|
368
|
-
message,
|
|
369
|
-
status: error.status,
|
|
370
|
-
code: normalizeMessage2(code),
|
|
371
|
-
retryable,
|
|
372
|
-
requestId: normalizeMessage2(requestId),
|
|
373
|
-
details,
|
|
374
|
-
cause: error
|
|
375
|
-
});
|
|
376
|
-
};
|
|
377
|
-
var toAppError = (error, policy) => {
|
|
378
|
-
const resolvedPolicy = defineErrorPolicy(policy);
|
|
379
|
-
try {
|
|
380
|
-
if (isAppError(error)) return normalizeExisting(error);
|
|
381
|
-
const axiosInfo = getAxiosLikeErrorInfo(error);
|
|
382
|
-
if (axiosInfo) {
|
|
383
|
-
if (axiosInfo.response) {
|
|
384
|
-
const response = toHttpResponseLike(axiosInfo);
|
|
385
|
-
const message2 = normalizeMessage2(
|
|
386
|
-
safeInvoke2(() => resolvedPolicy.http.message(axiosInfo.data, response))
|
|
387
|
-
) ?? DEFAULT_MESSAGE2;
|
|
388
|
-
const code2 = safeInvoke2(
|
|
389
|
-
() => resolvedPolicy.http.code(axiosInfo.data, response)
|
|
390
|
-
);
|
|
391
|
-
const requestId = safeInvoke2(
|
|
392
|
-
() => resolvedPolicy.http.requestId(axiosInfo.headers)
|
|
393
|
-
);
|
|
394
|
-
const retryable = safeInvoke2(() => resolvedPolicy.http.retryable(axiosInfo.status)) ?? defaultRetryable2("http", axiosInfo.status);
|
|
395
|
-
return buildAppError({
|
|
396
|
-
kind: "http",
|
|
397
|
-
message: message2,
|
|
398
|
-
status: axiosInfo.status,
|
|
399
|
-
code: normalizeMessage2(code2),
|
|
400
|
-
retryable,
|
|
401
|
-
requestId: normalizeMessage2(requestId),
|
|
402
|
-
details: axiosInfo.data,
|
|
403
|
-
cause: error
|
|
404
|
-
});
|
|
405
|
-
}
|
|
406
|
-
if (axiosInfo.isTimeout) {
|
|
407
|
-
return buildAppError({
|
|
408
|
-
kind: "timeout",
|
|
409
|
-
message: DEFAULT_MESSAGE2,
|
|
410
|
-
retryable: false,
|
|
411
|
-
cause: error
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
if (axiosInfo.isNetworkError) {
|
|
415
|
-
return buildAppError({
|
|
416
|
-
kind: "network",
|
|
417
|
-
message: DEFAULT_MESSAGE2,
|
|
418
|
-
retryable: true,
|
|
419
|
-
cause: error
|
|
420
|
-
});
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
const { name, message, code } = getErrorInfo(error);
|
|
424
|
-
if (isTimeoutError(name, message, code)) {
|
|
425
|
-
return buildAppError({
|
|
426
|
-
kind: "timeout",
|
|
427
|
-
message: DEFAULT_MESSAGE2,
|
|
428
|
-
retryable: false,
|
|
429
|
-
cause: error
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
if (isNetworkError(name, message, code)) {
|
|
433
|
-
return buildAppError({
|
|
434
|
-
kind: "network",
|
|
435
|
-
message: DEFAULT_MESSAGE2,
|
|
436
|
-
retryable: true,
|
|
437
|
-
cause: error
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
if (isParseError(name)) {
|
|
441
|
-
return buildAppError({
|
|
442
|
-
kind: "parse",
|
|
443
|
-
message: DEFAULT_MESSAGE2,
|
|
444
|
-
retryable: false,
|
|
445
|
-
cause: error
|
|
446
|
-
});
|
|
447
|
-
}
|
|
448
|
-
if (isValidationError(error, name)) {
|
|
449
|
-
return buildAppError({
|
|
450
|
-
kind: "validation",
|
|
451
|
-
message: DEFAULT_MESSAGE2,
|
|
452
|
-
retryable: false,
|
|
453
|
-
cause: error,
|
|
454
|
-
details: error
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
const httpFromStatus = fromStatusObject(error, resolvedPolicy);
|
|
458
|
-
if (httpFromStatus) return httpFromStatus;
|
|
459
|
-
} catch {
|
|
460
|
-
}
|
|
461
|
-
return buildAppError({
|
|
462
|
-
kind: "unknown",
|
|
463
|
-
message: DEFAULT_MESSAGE2,
|
|
464
|
-
retryable: false,
|
|
465
|
-
cause: error
|
|
466
|
-
});
|
|
467
|
-
};
|
|
468
|
-
|
|
469
|
-
// src/helpers.ts
|
|
470
|
-
var normalizeMessage3 = (value) => {
|
|
471
|
-
if (typeof value !== "string") return void 0;
|
|
472
|
-
const trimmed = value.trim();
|
|
473
|
-
return trimmed.length > 0 ? trimmed : void 0;
|
|
474
|
-
};
|
|
475
|
-
var errorKey = (error) => {
|
|
476
|
-
const normalized = isAppError(error) ? error : toAppError(error);
|
|
477
|
-
const parts = [
|
|
478
|
-
normalized.kind,
|
|
479
|
-
normalized.status !== void 0 ? String(normalized.status) : void 0,
|
|
480
|
-
normalizeMessage3(normalized.code),
|
|
481
|
-
normalizeMessage3(normalized.message)
|
|
482
|
-
].filter((value) => Boolean(value));
|
|
483
|
-
return parts.join("|");
|
|
484
|
-
};
|
|
485
|
-
var isRetryable = (error) => {
|
|
486
|
-
const normalized = isAppError(error) ? error : toAppError(error);
|
|
487
|
-
if (typeof normalized.retryable === "boolean") return normalized.retryable;
|
|
488
|
-
if (normalized.kind === "network") return true;
|
|
489
|
-
if (normalized.kind === "http") {
|
|
490
|
-
return typeof normalized.status === "number" && normalized.status >= 500;
|
|
491
|
-
}
|
|
492
|
-
return false;
|
|
493
|
-
};
|
|
494
|
-
var attempt = async (fn, policy) => {
|
|
495
|
-
try {
|
|
496
|
-
const data = await fn();
|
|
497
|
-
return { ok: true, data };
|
|
498
|
-
} catch (error) {
|
|
499
|
-
return { ok: false, error: toAppError(error, policy) };
|
|
500
|
-
}
|
|
501
|
-
};
|
|
502
|
-
export {
|
|
503
|
-
attempt,
|
|
504
|
-
defineErrorPolicy,
|
|
505
|
-
errorKey,
|
|
506
|
-
fromFetch,
|
|
507
|
-
isAppError,
|
|
508
|
-
isRetryable,
|
|
509
|
-
toAppError
|
|
510
|
-
};
|