@marianmeres/http-utils 2.7.0 → 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/AGENTS.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
```yaml
|
|
6
6
|
name: "@marianmeres/http-utils"
|
|
7
|
-
version: "2.
|
|
7
|
+
version: "2.8.0"
|
|
8
8
|
license: MIT
|
|
9
9
|
runtime: deno, node
|
|
10
10
|
type: library
|
|
@@ -13,7 +13,10 @@ category: http-client
|
|
|
13
13
|
|
|
14
14
|
## Purpose
|
|
15
15
|
|
|
16
|
-
Lightweight, opinionated HTTP client wrapper for the native `fetch` API. Provides
|
|
16
|
+
Lightweight, opinionated HTTP client wrapper for the native `fetch` API. Provides
|
|
17
|
+
type-safe HTTP errors mapped to specific error classes (e.g., 404 → NotFound), convenient
|
|
18
|
+
defaults (auto JSON parsing, Bearer token support, base URLs), and flexible three-tier
|
|
19
|
+
error message extraction.
|
|
17
20
|
|
|
18
21
|
## Architecture
|
|
19
22
|
|
|
@@ -31,121 +34,137 @@ src/
|
|
|
31
34
|
|
|
32
35
|
```typescript
|
|
33
36
|
function createHttpApi(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
): HttpApi
|
|
37
|
+
base?: string | null,
|
|
38
|
+
defaults?: Partial<FetchParams> | (() => Promise<Partial<FetchParams>>),
|
|
39
|
+
factoryErrorMessageExtractor?: ErrorMessageExtractor | null,
|
|
40
|
+
): HttpApi;
|
|
38
41
|
```
|
|
39
42
|
|
|
40
43
|
### HttpApi Methods
|
|
41
44
|
|
|
42
|
-
| Method
|
|
43
|
-
|
|
44
|
-
| `get`
|
|
45
|
-
| `post`
|
|
46
|
-
| `put`
|
|
47
|
-
| `patch`
|
|
48
|
-
| `del`
|
|
49
|
-
| `url`
|
|
50
|
-
| `base`
|
|
51
|
-
| `onRequest`
|
|
52
|
-
| `onResponse` | `onResponse(i: ResponseInterceptor \| null): this`
|
|
45
|
+
| Method | Signature | Description |
|
|
46
|
+
| ------------ | ------------------------------------------------------ | -------------------------------------------------------------------- |
|
|
47
|
+
| `get` | `get(path, options?: GetOptions): Promise<unknown>` | GET request |
|
|
48
|
+
| `post` | `post(path, options?: DataOptions): Promise<unknown>` | POST request |
|
|
49
|
+
| `put` | `put(path, options?: DataOptions): Promise<unknown>` | PUT request |
|
|
50
|
+
| `patch` | `patch(path, options?: DataOptions): Promise<unknown>` | PATCH request |
|
|
51
|
+
| `del` | `del(path, options?: DataOptions): Promise<unknown>` | DELETE request |
|
|
52
|
+
| `url` | `url(path: string): string` | Build full URL (base trailing slash + path leading slash normalized) |
|
|
53
|
+
| `base` | `get/set base: string \| null` | Base URL property |
|
|
54
|
+
| `onRequest` | `onRequest(i: RequestInterceptor \| null): this` | Register request interceptor |
|
|
55
|
+
| `onResponse` | `onResponse(i: ResponseInterceptor \| null): this` | Register response interceptor |
|
|
53
56
|
|
|
54
57
|
### Exported Types
|
|
55
58
|
|
|
56
59
|
```typescript
|
|
57
60
|
type RequestData =
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
| Record<string, unknown>
|
|
62
|
+
| unknown[]
|
|
63
|
+
| FormData
|
|
64
|
+
| Blob
|
|
65
|
+
| ArrayBuffer
|
|
66
|
+
| ArrayBufferView
|
|
67
|
+
| URLSearchParams
|
|
68
|
+
| ReadableStream
|
|
69
|
+
| string
|
|
70
|
+
| number
|
|
71
|
+
| boolean
|
|
72
|
+
| null;
|
|
70
73
|
|
|
71
74
|
type QueryValue =
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
| string
|
|
76
|
+
| number
|
|
77
|
+
| boolean
|
|
78
|
+
| (string | number | boolean)[]
|
|
79
|
+
| null
|
|
80
|
+
| undefined;
|
|
75
81
|
|
|
76
82
|
interface FetchParams {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
data?: RequestData;
|
|
84
|
+
token?: string | null;
|
|
85
|
+
headers?: HeadersInit | null;
|
|
86
|
+
signal?: AbortSignal;
|
|
87
|
+
timeout?: number | null; // ms
|
|
88
|
+
query?: Record<string, QueryValue> | null;
|
|
89
|
+
credentials?: "omit" | "same-origin" | "include" | null;
|
|
90
|
+
raw?: boolean | null;
|
|
91
|
+
assert?: boolean | null;
|
|
86
92
|
}
|
|
87
93
|
|
|
88
94
|
interface GetOptions {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
params?: FetchParams;
|
|
96
|
+
respHeaders?: ResponseHeaders | null;
|
|
97
|
+
errorExtractor?: ErrorMessageExtractor | null;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
interface DataOptions {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
101
|
+
data?: RequestData;
|
|
102
|
+
params?: FetchParams;
|
|
103
|
+
respHeaders?: ResponseHeaders | null;
|
|
104
|
+
errorExtractor?: ErrorMessageExtractor | null;
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
type ErrorMessageExtractor = (body: unknown, response: Response) => string;
|
|
102
108
|
type ResponseHeaders = Record<string, string | number>;
|
|
103
109
|
|
|
104
110
|
type RequestInterceptor = (
|
|
105
|
-
|
|
106
|
-
|
|
111
|
+
init: RequestInit,
|
|
112
|
+
ctx: { method: string; url: string },
|
|
107
113
|
) => RequestInit | void | Promise<RequestInit | void>;
|
|
108
114
|
|
|
109
115
|
type ResponseInterceptor = (
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
response: Response,
|
|
117
|
+
ctx: { method: string; url: string },
|
|
112
118
|
) => Response | void | Promise<Response | void>;
|
|
113
119
|
```
|
|
114
120
|
|
|
115
121
|
### Error Classes (HTTP_ERROR namespace)
|
|
116
122
|
|
|
117
123
|
```typescript
|
|
118
|
-
HTTP_ERROR.HttpError
|
|
119
|
-
HTTP_ERROR.BadRequest
|
|
120
|
-
HTTP_ERROR.Unauthorized
|
|
121
|
-
HTTP_ERROR.Forbidden
|
|
122
|
-
HTTP_ERROR.NotFound
|
|
123
|
-
HTTP_ERROR.MethodNotAllowed // 405
|
|
124
|
-
HTTP_ERROR.RequestTimeout
|
|
125
|
-
HTTP_ERROR.Conflict
|
|
126
|
-
HTTP_ERROR.Gone
|
|
127
|
-
HTTP_ERROR.LengthRequired
|
|
128
|
-
HTTP_ERROR.ImATeapot
|
|
129
|
-
HTTP_ERROR.UnprocessableContent // 422
|
|
130
|
-
HTTP_ERROR.TooManyRequests
|
|
131
|
-
HTTP_ERROR.InternalServerError // 500
|
|
132
|
-
HTTP_ERROR.NotImplemented
|
|
133
|
-
HTTP_ERROR.BadGateway
|
|
134
|
-
HTTP_ERROR.ServiceUnavailable // 503
|
|
124
|
+
HTTP_ERROR.HttpError; // Base class (default 500)
|
|
125
|
+
HTTP_ERROR.BadRequest; // 400
|
|
126
|
+
HTTP_ERROR.Unauthorized; // 401
|
|
127
|
+
HTTP_ERROR.Forbidden; // 403
|
|
128
|
+
HTTP_ERROR.NotFound; // 404
|
|
129
|
+
HTTP_ERROR.MethodNotAllowed; // 405
|
|
130
|
+
HTTP_ERROR.RequestTimeout; // 408
|
|
131
|
+
HTTP_ERROR.Conflict; // 409
|
|
132
|
+
HTTP_ERROR.Gone; // 410
|
|
133
|
+
HTTP_ERROR.LengthRequired; // 411
|
|
134
|
+
HTTP_ERROR.ImATeapot; // 418
|
|
135
|
+
HTTP_ERROR.UnprocessableContent; // 422
|
|
136
|
+
HTTP_ERROR.TooManyRequests; // 429
|
|
137
|
+
HTTP_ERROR.InternalServerError; // 500
|
|
138
|
+
HTTP_ERROR.NotImplemented; // 501
|
|
139
|
+
HTTP_ERROR.BadGateway; // 502
|
|
140
|
+
HTTP_ERROR.ServiceUnavailable; // 503
|
|
141
|
+
HTTP_ERROR.NetworkError; // 0 (transport-level failure: DNS/refused/timeout/unreachable; extends HttpError, cause = underlying error)
|
|
135
142
|
```
|
|
136
143
|
|
|
137
144
|
### Helper Functions
|
|
138
145
|
|
|
139
146
|
```typescript
|
|
140
147
|
// Marks options object for options-based API (vs legacy positional API)
|
|
141
|
-
function opts<T extends GetOptions | DataOptions>(options: T): T
|
|
148
|
+
function opts<T extends GetOptions | DataOptions>(options: T): T;
|
|
142
149
|
```
|
|
143
150
|
|
|
144
151
|
### Utility Functions
|
|
145
152
|
|
|
146
153
|
```typescript
|
|
147
|
-
function createHttpError(
|
|
148
|
-
|
|
154
|
+
function createHttpError(
|
|
155
|
+
code: number | string,
|
|
156
|
+
message?: string | null,
|
|
157
|
+
body?: unknown,
|
|
158
|
+
cause?: unknown,
|
|
159
|
+
): HttpError;
|
|
160
|
+
function getErrorMessage(e: unknown, stripErrorPrefix?: boolean): string;
|
|
161
|
+
// Wraps fetch; on transport failure throws NetworkError("<what> unreachable (<url>): <reason>", { cause }).
|
|
162
|
+
// AbortError/TimeoutError pass through untouched. Used internally by HttpApi.
|
|
163
|
+
function fetchOrThrow(
|
|
164
|
+
input: string | URL | Request,
|
|
165
|
+
init?: RequestInit,
|
|
166
|
+
what?: string,
|
|
167
|
+
): Promise<Response>;
|
|
149
168
|
```
|
|
150
169
|
|
|
151
170
|
### HTTP Status Codes
|
|
@@ -169,23 +188,35 @@ class HTTP_STATUS {
|
|
|
169
188
|
|
|
170
189
|
## Key Behaviors
|
|
171
190
|
|
|
172
|
-
1. **Auto JSON parsing**: Response bodies are parsed as JSON if possible; empty bodies
|
|
191
|
+
1. **Auto JSON parsing**: Response bodies are parsed as JSON if possible; empty bodies
|
|
192
|
+
(204/205) return `null`
|
|
173
193
|
2. **Bearer token**: `token` param auto-adds `Authorization: Bearer {token}` header
|
|
174
|
-
3. **Error throwing**: By default, non-OK responses throw HttpError (disable with
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
3. **Error throwing**: By default, non-OK responses throw HttpError (disable with
|
|
195
|
+
`assert: false`)
|
|
196
|
+
4. **Response headers**: Pass `respHeaders: {}` to capture response headers (mutated in
|
|
197
|
+
place)
|
|
198
|
+
5. **Raw response**: Use `raw: true` to get raw Response object; the caller must consume
|
|
199
|
+
the body
|
|
200
|
+
6. **Error priority**: per-request extractor → per-instance → global → built-in fallback;
|
|
201
|
+
a throwing extractor falls through to the next priority rather than crashing
|
|
202
|
+
7. **URL normalization**: `#url(path)` strips trailing `/` from base and ensures leading
|
|
203
|
+
`/` on path, so `base + path` never produces `//` or missing `/`
|
|
204
|
+
8. **Timeout**: `params.timeout` (ms) aborts via `AbortSignal.timeout`; composed with
|
|
205
|
+
`params.signal` via `AbortSignal.any`
|
|
206
|
+
9. **Query**: `params.query` object appended as URL search params; `null`/`undefined`
|
|
207
|
+
skipped, arrays → repeated keys
|
|
208
|
+
10. **Transport errors**: connectivity failures (DNS, refused connection, unreachable
|
|
209
|
+
host) throw `HTTP_ERROR.NetworkError` (status 0) via `fetchOrThrow`, with the real
|
|
210
|
+
reason in the message and the underlying error as `cause` — never an opaque "fetch
|
|
211
|
+
failed". Deliberate `AbortError`/`TimeoutError` are not wrapped.
|
|
181
212
|
|
|
182
213
|
## Request Body Serialization
|
|
183
214
|
|
|
184
|
-
| Runtime type
|
|
185
|
-
|
|
186
|
-
| `null` / `undefined`
|
|
187
|
-
| `string`
|
|
188
|
-
| `number` / `boolean` / plain object / array
|
|
215
|
+
| Runtime type | Sent as | Content-Type set |
|
|
216
|
+
| ------------------------------------------------------------------------------------ | ------------------------- | -------------------------------------------------------- |
|
|
217
|
+
| `null` / `undefined` | No body | — |
|
|
218
|
+
| `string` | raw | caller-controlled |
|
|
219
|
+
| `number` / `boolean` / plain object / array | `JSON.stringify` | `application/json` only if not already set |
|
|
189
220
|
| `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer`, typed arrays, `ReadableStream` | passed to fetch unchanged | fetch handles (e.g. multipart boundary, form-urlencoded) |
|
|
190
221
|
|
|
191
222
|
User-provided `Content-Type` header is always preserved.
|
|
@@ -206,14 +237,14 @@ deno task publish # Publish to JSR and NPM
|
|
|
206
237
|
|
|
207
238
|
## File Locations
|
|
208
239
|
|
|
209
|
-
| Purpose
|
|
210
|
-
|
|
211
|
-
| Entry point
|
|
212
|
-
| HttpApi implementation | `src/api.ts`
|
|
213
|
-
| Error classes
|
|
214
|
-
| Status codes
|
|
215
|
-
| Tests
|
|
216
|
-
| NPM output
|
|
240
|
+
| Purpose | Path |
|
|
241
|
+
| ---------------------- | ----------------- |
|
|
242
|
+
| Entry point | `src/mod.ts` |
|
|
243
|
+
| HttpApi implementation | `src/api.ts` |
|
|
244
|
+
| Error classes | `src/error.ts` |
|
|
245
|
+
| Status codes | `src/status.ts` |
|
|
246
|
+
| Tests | `tests/*.test.ts` |
|
|
247
|
+
| NPM output | `.npm-dist/` |
|
|
217
248
|
|
|
218
249
|
## Common Patterns
|
|
219
250
|
|
|
@@ -223,7 +254,7 @@ deno task publish # Publish to JSR and NPM
|
|
|
223
254
|
import { createHttpApi, opts } from "@marianmeres/http-utils";
|
|
224
255
|
|
|
225
256
|
const api = createHttpApi("https://api.example.com", {
|
|
226
|
-
|
|
257
|
+
headers: { "Authorization": "Bearer token" },
|
|
227
258
|
});
|
|
228
259
|
|
|
229
260
|
// Legacy API (default - object is request body)
|
|
@@ -238,11 +269,11 @@ await api.post("/users", opts({ data: { name: "John" }, params: { token: "abc" }
|
|
|
238
269
|
|
|
239
270
|
```typescript
|
|
240
271
|
try {
|
|
241
|
-
|
|
272
|
+
await api.get("/resource");
|
|
242
273
|
} catch (error) {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
274
|
+
if (error instanceof HTTP_ERROR.NotFound) {
|
|
275
|
+
// Handle 404
|
|
276
|
+
}
|
|
246
277
|
}
|
|
247
278
|
```
|
|
248
279
|
|
|
@@ -250,6 +281,6 @@ try {
|
|
|
250
281
|
|
|
251
282
|
```typescript
|
|
252
283
|
const api = createHttpApi("https://api.example.com", async () => ({
|
|
253
|
-
|
|
284
|
+
headers: { "Authorization": `Bearer ${await getToken()}` },
|
|
254
285
|
}));
|
|
255
286
|
```
|