@marianmeres/http-utils 2.7.1 â 2.8.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/AGENTS.md +130 -99
- package/API.md +292 -197
- package/README.md +105 -49
- package/dist/api.d.ts +42 -0
- package/dist/api.js +77 -4
- package/dist/error.d.ts +28 -1
- package/dist/error.js +106 -64
- package/dist/mod.d.ts +2 -2
- package/dist/mod.js +2 -2
- package/dist/status.js +77 -61
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,14 +4,18 @@
|
|
|
4
4
|
[](https://jsr.io/@marianmeres/http-utils)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
|
|
7
|
-
Opinionated, lightweight HTTP client wrapper for `fetch` with type-safe errors and
|
|
7
|
+
Opinionated, lightweight HTTP client wrapper for `fetch` with type-safe errors and
|
|
8
|
+
convenient defaults.
|
|
8
9
|
|
|
9
10
|
## Features
|
|
10
11
|
|
|
11
12
|
- ðŊ **Type-safe HTTP errors** - Well-known status codes map to specific error classes
|
|
12
13
|
- ð§ **Convenient defaults** - Auto JSON parsing, Bearer tokens, base URLs
|
|
13
14
|
- ðŠķ **Lightweight** - Zero dependencies, thin wrapper over native `fetch`
|
|
14
|
-
- ðĻ **Flexible error handling** - Three-tier error message extraction (local â factory â
|
|
15
|
+
- ðĻ **Flexible error handling** - Three-tier error message extraction (local â factory â
|
|
16
|
+
global)
|
|
17
|
+
- ð°ïļ **No swallowed transport errors** - DNS/connection failures surface the host and real
|
|
18
|
+
reason instead of an opaque "fetch failed"
|
|
15
19
|
- ðĶ **Deno & Node.js** - Works in both runtimes
|
|
16
20
|
- ðĶū **Generic return types** - Optional type parameters for typed responses
|
|
17
21
|
|
|
@@ -26,51 +30,60 @@ npm install @marianmeres/http-utils
|
|
|
26
30
|
```
|
|
27
31
|
|
|
28
32
|
```ts
|
|
29
|
-
import { createHttpApi,
|
|
33
|
+
import { createHttpApi, HTTP_ERROR, opts } from "@marianmeres/http-utils";
|
|
30
34
|
```
|
|
31
35
|
|
|
32
36
|
## Quick Start
|
|
33
37
|
|
|
34
38
|
```ts
|
|
35
|
-
import { createHttpApi,
|
|
39
|
+
import { createHttpApi, HTTP_ERROR, NotFound, opts } from "@marianmeres/http-utils";
|
|
36
40
|
|
|
37
41
|
// Create an API client with base URL
|
|
38
42
|
const api = createHttpApi("https://api.example.com", {
|
|
39
|
-
|
|
43
|
+
headers: { "Authorization": "Bearer your-token" },
|
|
40
44
|
});
|
|
41
45
|
|
|
42
46
|
// GET request (options API with opts() wrapper)
|
|
43
|
-
const users = await api.get(
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
const users = await api.get(
|
|
48
|
+
"/users",
|
|
49
|
+
opts({
|
|
50
|
+
params: { headers: { "X-Custom": "value" } },
|
|
51
|
+
}),
|
|
52
|
+
);
|
|
46
53
|
|
|
47
54
|
// POST request (options API with opts() wrapper)
|
|
48
|
-
const newUser = await api.post(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
55
|
+
const newUser = await api.post(
|
|
56
|
+
"/users",
|
|
57
|
+
opts({
|
|
58
|
+
data: { name: "John Doe" },
|
|
59
|
+
params: { headers: { "X-Custom": "value" } },
|
|
60
|
+
}),
|
|
61
|
+
);
|
|
52
62
|
|
|
53
63
|
// Legacy API (default behavior without opts())
|
|
54
64
|
const legacyUsers = await api.get("/users", { headers: { "X-Custom": "value" } });
|
|
55
65
|
const legacyUser = await api.post("/users", { name: "John Doe" });
|
|
56
66
|
|
|
57
67
|
// With type parameters for typed responses
|
|
58
|
-
interface User {
|
|
68
|
+
interface User {
|
|
69
|
+
id: number;
|
|
70
|
+
name: string;
|
|
71
|
+
}
|
|
59
72
|
const user = await api.get<User>("/users/1");
|
|
60
73
|
const created = await api.post<User>("/users", opts({ data: { name: "Jane" } }));
|
|
61
74
|
|
|
62
75
|
// Error handling
|
|
63
76
|
try {
|
|
64
|
-
|
|
77
|
+
await api.get("/not-found");
|
|
65
78
|
} catch (error) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
79
|
+
if (error instanceof NotFound) {
|
|
80
|
+
console.log("Resource not found");
|
|
81
|
+
}
|
|
82
|
+
// or use the namespace
|
|
83
|
+
if (error instanceof HTTP_ERROR.NotFound) {
|
|
84
|
+
console.log(error.status); // 404
|
|
85
|
+
console.log(error.body); // Response body
|
|
86
|
+
}
|
|
74
87
|
}
|
|
75
88
|
```
|
|
76
89
|
|
|
@@ -82,7 +95,7 @@ Creates an HTTP API client.
|
|
|
82
95
|
|
|
83
96
|
```ts
|
|
84
97
|
const api = createHttpApi("https://api.example.com", {
|
|
85
|
-
|
|
98
|
+
headers: { "Authorization": "Bearer token" },
|
|
86
99
|
});
|
|
87
100
|
```
|
|
88
101
|
|
|
@@ -90,16 +103,22 @@ const api = createHttpApi("https://api.example.com", {
|
|
|
90
103
|
|
|
91
104
|
```ts
|
|
92
105
|
// GET (options API with opts() wrapper)
|
|
93
|
-
const data = await api.get(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
106
|
+
const data = await api.get(
|
|
107
|
+
"/users",
|
|
108
|
+
opts({
|
|
109
|
+
params: { headers: { "X-Custom": "value" } },
|
|
110
|
+
respHeaders: {},
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
97
113
|
|
|
98
114
|
// POST/PUT/PATCH/DELETE (options API with opts() wrapper)
|
|
99
|
-
await api.post(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
115
|
+
await api.post(
|
|
116
|
+
"/users",
|
|
117
|
+
opts({
|
|
118
|
+
data: { name: "John" },
|
|
119
|
+
params: { token: "bearer-token" },
|
|
120
|
+
}),
|
|
121
|
+
);
|
|
103
122
|
|
|
104
123
|
// Legacy API (default behavior without opts())
|
|
105
124
|
const data = await api.get("/users", { headers: { "X-Custom": "value" } });
|
|
@@ -108,14 +127,15 @@ await api.post("/users", { name: "John" });
|
|
|
108
127
|
|
|
109
128
|
### The `opts()` Helper
|
|
110
129
|
|
|
111
|
-
The `opts()` function explicitly marks an options object for the options-based API.
|
|
130
|
+
The `opts()` function explicitly marks an options object for the options-based API.
|
|
131
|
+
Without it, arguments are treated as legacy positional parameters.
|
|
112
132
|
|
|
113
133
|
```ts
|
|
114
134
|
// Without opts() - legacy behavior: object is sent as request body
|
|
115
|
-
await api.post("/users", { data: { name: "John" } });
|
|
135
|
+
await api.post("/users", { data: { name: "John" } }); // Sends: { data: { name: "John" } }
|
|
116
136
|
|
|
117
137
|
// With opts() - options API: data is extracted and sent as body
|
|
118
|
-
await api.post("/users", opts({ data: { name: "John" } }));
|
|
138
|
+
await api.post("/users", opts({ data: { name: "John" } })); // Sends: { name: "John" }
|
|
119
139
|
```
|
|
120
140
|
|
|
121
141
|
This makes the API unambiguous and prevents accidental misinterpretation of request data.
|
|
@@ -126,27 +146,37 @@ This makes the API unambiguous and prevents accidental misinterpretation of requ
|
|
|
126
146
|
import { HTTP_ERROR, NotFound } from "@marianmeres/http-utils";
|
|
127
147
|
|
|
128
148
|
try {
|
|
129
|
-
|
|
149
|
+
await api.get("/resource");
|
|
130
150
|
} catch (error) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
151
|
+
if (error instanceof NotFound) {
|
|
152
|
+
console.log("Not found:", error.body);
|
|
153
|
+
}
|
|
154
|
+
// Transport-level failures (DNS, refused connection, unreachable host) throw
|
|
155
|
+
// a NetworkError (status 0) instead of an opaque "fetch failed":
|
|
156
|
+
if (error instanceof HTTP_ERROR.NetworkError) {
|
|
157
|
+
console.log(error.message); // e.g. "GET unreachable (https://...): ECONNREFUSED"
|
|
158
|
+
console.log(error.cause); // underlying transport error
|
|
159
|
+
}
|
|
160
|
+
// All errors have: status, statusText, body, cause
|
|
135
161
|
}
|
|
136
162
|
```
|
|
137
163
|
|
|
138
164
|
### Key Features
|
|
139
165
|
|
|
140
|
-
- **Auto JSON**: Response bodies are automatically parsed as JSON; empty bodies (204/205)
|
|
141
|
-
|
|
166
|
+
- **Auto JSON**: Response bodies are automatically parsed as JSON; empty bodies (204/205)
|
|
167
|
+
return `null`
|
|
168
|
+
- **Smart body handling**: Plain objects â JSON; `FormData` / `URLSearchParams` / `Blob` /
|
|
169
|
+
typed arrays / `ReadableStream` pass through; strings sent as-is
|
|
142
170
|
- **Query params**: Pass `query: { page: 1, tag: ["a", "b"] }` for URL search params
|
|
143
171
|
- **Timeouts**: Pass `timeout: 5000` for automatic request cancellation
|
|
144
172
|
- **Bearer tokens**: Use `token` param to auto-add `Authorization: Bearer` header
|
|
145
173
|
- **Response headers**: Pass `respHeaders: {}` to capture response headers
|
|
146
|
-
- **Raw response**: Use `raw: true` to get the raw Response object (caller must consume
|
|
174
|
+
- **Raw response**: Use `raw: true` to get the raw Response object (caller must consume
|
|
175
|
+
the body)
|
|
147
176
|
- **Non-throwing**: Use `assert: false` to prevent throwing on errors
|
|
148
177
|
- **AbortController**: Pass `signal` for request cancellation (composes with `timeout`)
|
|
149
|
-
- **Interceptors**: `api.onRequest(...)` / `api.onResponse(...)` for tracing, auth
|
|
178
|
+
- **Interceptors**: `api.onRequest(...)` / `api.onResponse(...)` for tracing, auth
|
|
179
|
+
refresh, etc.
|
|
150
180
|
- **Typed responses**: Use generics for type-safe responses: `api.get<User>("/users/1")`
|
|
151
181
|
|
|
152
182
|
### Query, Timeout, Interceptors
|
|
@@ -160,18 +190,44 @@ await api.get("/slow", { timeout: 5000 });
|
|
|
160
190
|
|
|
161
191
|
// Interceptors
|
|
162
192
|
api.onRequest((init, { method, url }) => {
|
|
163
|
-
|
|
193
|
+
console.log(method, url);
|
|
164
194
|
}).onResponse(async (resp) => {
|
|
165
|
-
|
|
195
|
+
if (resp.status === 401) await refreshToken();
|
|
166
196
|
});
|
|
167
197
|
```
|
|
168
198
|
|
|
169
199
|
## Full API Reference
|
|
170
200
|
|
|
171
|
-
For complete API documentation including all error classes, HTTP status codes, types, and
|
|
201
|
+
For complete API documentation including all error classes, HTTP status codes, types, and
|
|
202
|
+
utilities, see **[API.md](API.md)**.
|
|
172
203
|
|
|
173
204
|
## Utilities
|
|
174
205
|
|
|
206
|
+
### `fetchOrThrow(input, init?, what?)`
|
|
207
|
+
|
|
208
|
+
Wraps the native `fetch` so a transport-level failure surfaces the target host and the
|
|
209
|
+
real reason instead of an opaque `TypeError: fetch failed`. On failure it throws a
|
|
210
|
+
`NetworkError` (in the `HTTP_ERROR` namespace) whose message includes the URL and reason,
|
|
211
|
+
and whose `cause` is the underlying transport error. Deliberate
|
|
212
|
+
`AbortError`/`TimeoutError` are re-thrown untouched. The `HttpApi` client uses this
|
|
213
|
+
internally â reach for it directly when wrapping your own `fetch` calls.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { fetchOrThrow, HTTP_ERROR } from "@marianmeres/http-utils";
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
const res = await fetchOrThrow(
|
|
220
|
+
"https://issuer.example.com/jwks",
|
|
221
|
+
undefined,
|
|
222
|
+
"Token issuer",
|
|
223
|
+
);
|
|
224
|
+
} catch (e) {
|
|
225
|
+
if (e instanceof HTTP_ERROR.NetworkError) {
|
|
226
|
+
console.log(e.message); // "Token issuer unreachable (https://issuer.example.com/jwks): ENOTFOUND"
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
175
231
|
### `getErrorMessage(error)`
|
|
176
232
|
|
|
177
233
|
Extracts human-readable messages from any error format:
|
|
@@ -180,9 +236,9 @@ Extracts human-readable messages from any error format:
|
|
|
180
236
|
import { getErrorMessage } from "@marianmeres/http-utils";
|
|
181
237
|
|
|
182
238
|
try {
|
|
183
|
-
|
|
239
|
+
await api.get("/fail");
|
|
184
240
|
} catch (error) {
|
|
185
|
-
|
|
241
|
+
console.log(getErrorMessage(error)); // "Not Found"
|
|
186
242
|
}
|
|
187
243
|
```
|
|
188
244
|
|
|
@@ -195,4 +251,4 @@ import { createHttpError } from "@marianmeres/http-utils";
|
|
|
195
251
|
|
|
196
252
|
const error = createHttpError(404, "User not found", { userId: 123 });
|
|
197
253
|
throw error; // instanceof NotFound
|
|
198
|
-
```
|
|
254
|
+
```
|
package/dist/api.d.ts
CHANGED
|
@@ -117,6 +117,48 @@ export interface DataOptions {
|
|
|
117
117
|
* ```
|
|
118
118
|
*/
|
|
119
119
|
export declare function opts<T extends GetOptions | DataOptions>(options: T): T;
|
|
120
|
+
/**
|
|
121
|
+
* Wraps the native `fetch` so a transport-level failure surfaces the target host
|
|
122
|
+
* and the real reason instead of an opaque "fetch failed".
|
|
123
|
+
*
|
|
124
|
+
* Node/undici collapses DNS failures, refused connections and connect timeouts
|
|
125
|
+
* into a `TypeError: fetch failed` whose actual code (`ENOTFOUND`,
|
|
126
|
+
* `ECONNREFUSED`, `UND_ERR_CONNECT_TIMEOUT`, ...) lives on `err.cause` and is
|
|
127
|
+
* absent from the message and stack. On such a failure this throws a
|
|
128
|
+
* `NetworkError` (see {@link HTTP_ERROR}) whose message includes the URL and the
|
|
129
|
+
* resolved reason, and whose `cause` is the underlying transport error (so both
|
|
130
|
+
* `error.message` and `getErrorMessage(error)` report the real reason).
|
|
131
|
+
*
|
|
132
|
+
* Deliberate cancellations (`AbortError`) and timeouts (`TimeoutError`) are
|
|
133
|
+
* re-thrown untouched â they already carry clear semantics and must not be
|
|
134
|
+
* masked as "host unreachable".
|
|
135
|
+
*
|
|
136
|
+
* @param input - The `fetch` resource (URL string, `URL`, or `Request`).
|
|
137
|
+
* @param init - The `fetch` `RequestInit` options.
|
|
138
|
+
* @param what - Optional label describing the target (e.g. "Token issuer"),
|
|
139
|
+
* used to prefix the error message.
|
|
140
|
+
*
|
|
141
|
+
* @returns The `Response`. This does NOT throw on non-2xx HTTP statuses â only
|
|
142
|
+
* on transport-level failures, exactly like the native `fetch`.
|
|
143
|
+
*
|
|
144
|
+
* @throws A `NetworkError` on a transport-level failure (DNS, refused
|
|
145
|
+
* connection, unreachable host, ...).
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```ts
|
|
149
|
+
* import { fetchOrThrow, HTTP_ERROR } from "@marianmeres/http-utils";
|
|
150
|
+
*
|
|
151
|
+
* try {
|
|
152
|
+
* const res = await fetchOrThrow("https://issuer.example.com/jwks", undefined, "Token issuer");
|
|
153
|
+
* } catch (e) {
|
|
154
|
+
* if (e instanceof HTTP_ERROR.NetworkError) {
|
|
155
|
+
* // e.message â "Token issuer unreachable (https://issuer.example.com/jwks): ENOTFOUND"
|
|
156
|
+
* // e.cause â underlying transport error
|
|
157
|
+
* }
|
|
158
|
+
* }
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export declare function fetchOrThrow(input: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1], what?: string): Promise<Response>;
|
|
120
162
|
/**
|
|
121
163
|
* HTTP API client with convenient defaults and error handling.
|
|
122
164
|
*/
|
package/dist/api.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* HTTP API client factory and related types.
|
|
5
5
|
* Provides a convenient wrapper over the native `fetch` API with sensible defaults.
|
|
6
6
|
*/
|
|
7
|
-
import { createHttpError } from "./error.js";
|
|
7
|
+
import { createHttpError, getErrorMessage, NetworkError } from "./error.js";
|
|
8
8
|
/**
|
|
9
9
|
* Deep merges two objects. Later properties overwrite earlier properties.
|
|
10
10
|
* Arrays are overwritten, not concatenated (conventional behavior).
|
|
@@ -101,8 +101,9 @@ function composeSignal(userSignal, timeoutMs) {
|
|
|
101
101
|
userSignal.addEventListener("abort", () => abort(userSignal.reason));
|
|
102
102
|
if (timeoutSignal.aborted)
|
|
103
103
|
abort(timeoutSignal.reason);
|
|
104
|
-
else
|
|
104
|
+
else {
|
|
105
105
|
timeoutSignal.addEventListener("abort", () => abort(timeoutSignal.reason));
|
|
106
|
+
}
|
|
106
107
|
return ctrl.signal;
|
|
107
108
|
}
|
|
108
109
|
/** Symbol marker for explicit options API detection. */
|
|
@@ -209,6 +210,76 @@ function buildRequest(params) {
|
|
|
209
210
|
url = appendQuery(url, query);
|
|
210
211
|
return { url, init };
|
|
211
212
|
}
|
|
213
|
+
/** Best-effort human-readable description of a `fetch` target for error messages. */
|
|
214
|
+
function _describeFetchTarget(input) {
|
|
215
|
+
if (typeof input === "string")
|
|
216
|
+
return input;
|
|
217
|
+
if (input instanceof URL)
|
|
218
|
+
return input.href;
|
|
219
|
+
return input?.url ?? String(input);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Wraps the native `fetch` so a transport-level failure surfaces the target host
|
|
223
|
+
* and the real reason instead of an opaque "fetch failed".
|
|
224
|
+
*
|
|
225
|
+
* Node/undici collapses DNS failures, refused connections and connect timeouts
|
|
226
|
+
* into a `TypeError: fetch failed` whose actual code (`ENOTFOUND`,
|
|
227
|
+
* `ECONNREFUSED`, `UND_ERR_CONNECT_TIMEOUT`, ...) lives on `err.cause` and is
|
|
228
|
+
* absent from the message and stack. On such a failure this throws a
|
|
229
|
+
* `NetworkError` (see {@link HTTP_ERROR}) whose message includes the URL and the
|
|
230
|
+
* resolved reason, and whose `cause` is the underlying transport error (so both
|
|
231
|
+
* `error.message` and `getErrorMessage(error)` report the real reason).
|
|
232
|
+
*
|
|
233
|
+
* Deliberate cancellations (`AbortError`) and timeouts (`TimeoutError`) are
|
|
234
|
+
* re-thrown untouched â they already carry clear semantics and must not be
|
|
235
|
+
* masked as "host unreachable".
|
|
236
|
+
*
|
|
237
|
+
* @param input - The `fetch` resource (URL string, `URL`, or `Request`).
|
|
238
|
+
* @param init - The `fetch` `RequestInit` options.
|
|
239
|
+
* @param what - Optional label describing the target (e.g. "Token issuer"),
|
|
240
|
+
* used to prefix the error message.
|
|
241
|
+
*
|
|
242
|
+
* @returns The `Response`. This does NOT throw on non-2xx HTTP statuses â only
|
|
243
|
+
* on transport-level failures, exactly like the native `fetch`.
|
|
244
|
+
*
|
|
245
|
+
* @throws A `NetworkError` on a transport-level failure (DNS, refused
|
|
246
|
+
* connection, unreachable host, ...).
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* import { fetchOrThrow, HTTP_ERROR } from "@marianmeres/http-utils";
|
|
251
|
+
*
|
|
252
|
+
* try {
|
|
253
|
+
* const res = await fetchOrThrow("https://issuer.example.com/jwks", undefined, "Token issuer");
|
|
254
|
+
* } catch (e) {
|
|
255
|
+
* if (e instanceof HTTP_ERROR.NetworkError) {
|
|
256
|
+
* // e.message â "Token issuer unreachable (https://issuer.example.com/jwks): ENOTFOUND"
|
|
257
|
+
* // e.cause â underlying transport error
|
|
258
|
+
* }
|
|
259
|
+
* }
|
|
260
|
+
* ```
|
|
261
|
+
*/
|
|
262
|
+
export async function fetchOrThrow(input, init, what) {
|
|
263
|
+
try {
|
|
264
|
+
return await fetch(input, init);
|
|
265
|
+
}
|
|
266
|
+
catch (err) {
|
|
267
|
+
const name = err?.name;
|
|
268
|
+
// Preserve deliberate cancellations and timeouts as-is.
|
|
269
|
+
if (name === "AbortError" || name === "TimeoutError")
|
|
270
|
+
throw err;
|
|
271
|
+
// undici/Node bury the real reason on `err.cause`; prefer it so both
|
|
272
|
+
// `.message` and `getErrorMessage(networkError)` surface ENOTFOUND/etc.
|
|
273
|
+
// rather than re-surfacing the opaque outer "fetch failed".
|
|
274
|
+
const underlying = err?.cause ?? err;
|
|
275
|
+
const reason = getErrorMessage(underlying);
|
|
276
|
+
const url = _describeFetchTarget(input);
|
|
277
|
+
const message = what
|
|
278
|
+
? `${what} unreachable (${url}): ${reason}`
|
|
279
|
+
: `Network request to ${url} failed: ${reason}`;
|
|
280
|
+
throw new NetworkError(message, { cause: underlying });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
212
283
|
const _fetch = async (params, respHeaders = null, errorMessageExtractor = null, requestInterceptor = null, responseInterceptor = null, _dumpParams = false) => {
|
|
213
284
|
if (_dumpParams)
|
|
214
285
|
return params;
|
|
@@ -221,7 +292,7 @@ const _fetch = async (params, respHeaders = null, errorMessageExtractor = null,
|
|
|
221
292
|
if (patched)
|
|
222
293
|
init = patched;
|
|
223
294
|
}
|
|
224
|
-
let r = await
|
|
295
|
+
let r = await fetchOrThrow(url, init, params.method);
|
|
225
296
|
if (responseInterceptor) {
|
|
226
297
|
const patched = await responseInterceptor(r, {
|
|
227
298
|
method: params.method,
|
|
@@ -232,7 +303,9 @@ const _fetch = async (params, respHeaders = null, errorMessageExtractor = null,
|
|
|
232
303
|
try {
|
|
233
304
|
await r.body?.cancel();
|
|
234
305
|
}
|
|
235
|
-
catch (_e) {
|
|
306
|
+
catch (_e) {
|
|
307
|
+
/* ignore */
|
|
308
|
+
}
|
|
236
309
|
r = patched;
|
|
237
310
|
}
|
|
238
311
|
}
|
package/dist/error.d.ts
CHANGED
|
@@ -124,7 +124,33 @@ declare class ServiceUnavailable extends HttpError {
|
|
|
124
124
|
status: number;
|
|
125
125
|
statusText: string;
|
|
126
126
|
}
|
|
127
|
-
|
|
127
|
+
/**
|
|
128
|
+
* Transport-level failure: DNS resolution failure, refused connection, connect
|
|
129
|
+
* timeout, unreachable host, etc. No HTTP response was received, so `status` is
|
|
130
|
+
* `0` (the de-facto web convention for network-level failures, mirroring
|
|
131
|
+
* `XMLHttpRequest.status`). Thrown by `fetchOrThrow` (and, by extension, by the
|
|
132
|
+
* `HttpApi` client) with the underlying transport error attached as `cause`, so
|
|
133
|
+
* the real reason (`ENOTFOUND`, `ECONNREFUSED`, ...) is never swallowed behind
|
|
134
|
+
* an opaque "fetch failed".
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* try {
|
|
139
|
+
* await api.get("/resource");
|
|
140
|
+
* } catch (error) {
|
|
141
|
+
* if (error instanceof HTTP_ERROR.NetworkError) {
|
|
142
|
+
* console.log(error.message); // e.g. "GET unreachable (https://...): ECONNREFUSED"
|
|
143
|
+
* console.log(error.cause); // the underlying transport error
|
|
144
|
+
* }
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
declare class NetworkError extends HttpError {
|
|
149
|
+
name: string;
|
|
150
|
+
status: number;
|
|
151
|
+
statusText: string;
|
|
152
|
+
}
|
|
153
|
+
export { BadGateway, BadRequest, Conflict, Forbidden, Gone, HttpError, ImATeapot, InternalServerError, LengthRequired, MethodNotAllowed, NetworkError, NotFound, NotImplemented, RequestTimeout, ServiceUnavailable, TooManyRequests, Unauthorized, UnprocessableContent, };
|
|
128
154
|
/**
|
|
129
155
|
* Namespace containing all HTTP error classes for convenient access.
|
|
130
156
|
*
|
|
@@ -162,6 +188,7 @@ export declare const HTTP_ERROR: {
|
|
|
162
188
|
NotImplemented: typeof NotImplemented;
|
|
163
189
|
BadGateway: typeof BadGateway;
|
|
164
190
|
ServiceUnavailable: typeof ServiceUnavailable;
|
|
191
|
+
NetworkError: typeof NetworkError;
|
|
165
192
|
};
|
|
166
193
|
/**
|
|
167
194
|
* Creates an HTTP error from a status code and optional details.
|