@marianmeres/http-utils 2.7.1 → 2.9.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 CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  ```yaml
6
6
  name: "@marianmeres/http-utils"
7
- version: "2.5.1"
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 type-safe HTTP errors mapped to specific error classes (e.g., 404 → NotFound), convenient defaults (auto JSON parsing, Bearer token support, base URLs), and flexible three-tier error message extraction.
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,142 @@ src/
31
34
 
32
35
  ```typescript
33
36
  function createHttpApi(
34
- base?: string | null,
35
- defaults?: Partial<FetchParams> | (() => Promise<Partial<FetchParams>>),
36
- factoryErrorMessageExtractor?: ErrorMessageExtractor | null
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 | Signature | Description |
43
- |--------|-----------|-------------|
44
- | `get` | `get(path, options?: GetOptions): Promise<unknown>` | GET request |
45
- | `post` | `post(path, options?: DataOptions): Promise<unknown>` | POST request |
46
- | `put` | `put(path, options?: DataOptions): Promise<unknown>` | PUT request |
47
- | `patch` | `patch(path, options?: DataOptions): Promise<unknown>` | PATCH request |
48
- | `del` | `del(path, options?: DataOptions): Promise<unknown>` | DELETE request |
49
- | `url` | `url(path: string): string` | Build full URL (base trailing slash + path leading slash normalized) |
50
- | `base` | `get/set base: string \| null` | Base URL property |
51
- | `onRequest` | `onRequest(i: RequestInterceptor \| null): this` | Register request interceptor |
52
- | `onResponse` | `onResponse(i: ResponseInterceptor \| null): this` | Register response interceptor |
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
- | Record<string, unknown>
59
- | unknown[]
60
- | FormData
61
- | Blob
62
- | ArrayBuffer
63
- | ArrayBufferView
64
- | URLSearchParams
65
- | ReadableStream
66
- | string
67
- | number
68
- | boolean
69
- | null;
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
- | string | number | boolean
73
- | (string | number | boolean)[]
74
- | null | undefined;
75
+ | string
76
+ | number
77
+ | boolean
78
+ | (string | number | boolean)[]
79
+ | null
80
+ | undefined;
75
81
 
76
82
  interface FetchParams {
77
- data?: RequestData;
78
- token?: string | null;
79
- headers?: HeadersInit | null;
80
- signal?: AbortSignal;
81
- timeout?: number | null; // ms
82
- query?: Record<string, QueryValue> | null;
83
- credentials?: 'omit' | 'same-origin' | 'include' | null;
84
- raw?: boolean | null;
85
- assert?: boolean | null;
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
- params?: FetchParams;
90
- respHeaders?: ResponseHeaders | null;
91
- errorExtractor?: ErrorMessageExtractor | null;
95
+ params?: FetchParams;
96
+ respHeaders?: ResponseHeaders | null;
97
+ errorExtractor?: ErrorMessageExtractor | null;
92
98
  }
93
99
 
94
100
  interface DataOptions {
95
- data?: RequestData;
96
- params?: FetchParams;
97
- respHeaders?: ResponseHeaders | null;
98
- errorExtractor?: ErrorMessageExtractor | null;
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
- init: RequestInit,
106
- ctx: { method: string; url: string }
111
+ init: RequestInit,
112
+ ctx: { method: string; url: string },
107
113
  ) => RequestInit | void | Promise<RequestInit | void>;
108
114
 
109
115
  type ResponseInterceptor = (
110
- response: Response,
111
- ctx: { method: string; url: string }
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 // Base class (default 500)
119
- HTTP_ERROR.BadRequest // 400
120
- HTTP_ERROR.Unauthorized // 401
121
- HTTP_ERROR.Forbidden // 403
122
- HTTP_ERROR.NotFound // 404
123
- HTTP_ERROR.MethodNotAllowed // 405
124
- HTTP_ERROR.RequestTimeout // 408
125
- HTTP_ERROR.Conflict // 409
126
- HTTP_ERROR.Gone // 410
127
- HTTP_ERROR.LengthRequired // 411
128
- HTTP_ERROR.ImATeapot // 418
129
- HTTP_ERROR.UnprocessableContent // 422
130
- HTTP_ERROR.TooManyRequests // 429
131
- HTTP_ERROR.InternalServerError // 500
132
- HTTP_ERROR.NotImplemented // 501
133
- HTTP_ERROR.BadGateway // 502
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(code: number | string, message?: string | null, body?: unknown, cause?: unknown): HttpError
148
- function getErrorMessage(e: unknown, stripErrorPrefix?: boolean): string
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
+ // 3rd arg is a label string OR FetchOrThrowOptions { what?, onRequest?, onError? } (pure observers; a string normalizes to { what }).
164
+ // onRequest fires before dispatch (throw => request not sent). onError fires on every failure
165
+ // with kind: "abort" | "timeout" | "network" (throw => swallowed, real error preserved).
166
+ // Global defaults (overridable per call; resolution: per-call ?? global; instruments HttpApi too):
167
+ // fetchOrThrow.global.onRequest / fetchOrThrow.global.onError
168
+ function fetchOrThrow(
169
+ input: string | URL | Request,
170
+ init?: RequestInit,
171
+ whatOrOptions?: string | FetchOrThrowOptions,
172
+ ): Promise<Response>;
149
173
  ```
150
174
 
151
175
  ### HTTP Status Codes
@@ -169,23 +193,38 @@ class HTTP_STATUS {
169
193
 
170
194
  ## Key Behaviors
171
195
 
172
- 1. **Auto JSON parsing**: Response bodies are parsed as JSON if possible; empty bodies (204/205) return `null`
196
+ 1. **Auto JSON parsing**: Response bodies are parsed as JSON if possible; empty bodies
197
+ (204/205) return `null`
173
198
  2. **Bearer token**: `token` param auto-adds `Authorization: Bearer {token}` header
174
- 3. **Error throwing**: By default, non-OK responses throw HttpError (disable with `assert: false`)
175
- 4. **Response headers**: Pass `respHeaders: {}` to capture response headers (mutated in place)
176
- 5. **Raw response**: Use `raw: true` to get raw Response object; the caller must consume the body
177
- 6. **Error priority**: per-request extractor → per-instance → global → built-in fallback; a throwing extractor falls through to the next priority rather than crashing
178
- 7. **URL normalization**: `#url(path)` strips trailing `/` from base and ensures leading `/` on path, so `base + path` never produces `//` or missing `/`
179
- 8. **Timeout**: `params.timeout` (ms) aborts via `AbortSignal.timeout`; composed with `params.signal` via `AbortSignal.any`
180
- 9. **Query**: `params.query` object appended as URL search params; `null`/`undefined` skipped, arrays repeated keys
199
+ 3. **Error throwing**: By default, non-OK responses throw HttpError (disable with
200
+ `assert: false`)
201
+ 4. **Response headers**: Pass `respHeaders: {}` to capture response headers (mutated in
202
+ place)
203
+ 5. **Raw response**: Use `raw: true` to get raw Response object; the caller must consume
204
+ the body
205
+ 6. **Error priority**: per-request extractor per-instance globalbuilt-in fallback;
206
+ a throwing extractor falls through to the next priority rather than crashing
207
+ 7. **URL normalization**: `#url(path)` strips trailing `/` from base and ensures leading
208
+ `/` on path, so `base + path` never produces `//` or missing `/`
209
+ 8. **Timeout**: `params.timeout` (ms) aborts via `AbortSignal.timeout`; composed with
210
+ `params.signal` via `AbortSignal.any`
211
+ 9. **Query**: `params.query` object appended as URL search params; `null`/`undefined`
212
+ skipped, arrays → repeated keys
213
+ 10. **Transport errors**: connectivity failures (DNS, refused connection, unreachable
214
+ host) throw `HTTP_ERROR.NetworkError` (status 0) via `fetchOrThrow`, with the real
215
+ reason in the message and the underlying error as `cause` — never an opaque "fetch
216
+ failed". Deliberate `AbortError`/`TimeoutError` are not wrapped.
217
+ 11. **Request tracing**: `fetchOrThrow.global.onRequest` / `onError` are observer-only
218
+ hooks (no recovery/transform), overridable per call (`per-call ?? global`). They also
219
+ fire for `HttpApi` requests, since the client routes through `fetchOrThrow`.
181
220
 
182
221
  ## Request Body Serialization
183
222
 
184
- | Runtime type | Sent as | Content-Type set |
185
- |---|---|---|
186
- | `null` / `undefined` | No body | — |
187
- | `string` | raw | caller-controlled |
188
- | `number` / `boolean` / plain object / array | `JSON.stringify` | `application/json` only if not already set |
223
+ | Runtime type | Sent as | Content-Type set |
224
+ | ------------------------------------------------------------------------------------ | ------------------------- | -------------------------------------------------------- |
225
+ | `null` / `undefined` | No body | — |
226
+ | `string` | raw | caller-controlled |
227
+ | `number` / `boolean` / plain object / array | `JSON.stringify` | `application/json` only if not already set |
189
228
  | `FormData`, `URLSearchParams`, `Blob`, `ArrayBuffer`, typed arrays, `ReadableStream` | passed to fetch unchanged | fetch handles (e.g. multipart boundary, form-urlencoded) |
190
229
 
191
230
  User-provided `Content-Type` header is always preserved.
@@ -206,14 +245,14 @@ deno task publish # Publish to JSR and NPM
206
245
 
207
246
  ## File Locations
208
247
 
209
- | Purpose | Path |
210
- |---------|------|
211
- | Entry point | `src/mod.ts` |
212
- | HttpApi implementation | `src/api.ts` |
213
- | Error classes | `src/error.ts` |
214
- | Status codes | `src/status.ts` |
215
- | Tests | `tests/*.test.ts` |
216
- | NPM output | `.npm-dist/` |
248
+ | Purpose | Path |
249
+ | ---------------------- | ----------------- |
250
+ | Entry point | `src/mod.ts` |
251
+ | HttpApi implementation | `src/api.ts` |
252
+ | Error classes | `src/error.ts` |
253
+ | Status codes | `src/status.ts` |
254
+ | Tests | `tests/*.test.ts` |
255
+ | NPM output | `.npm-dist/` |
217
256
 
218
257
  ## Common Patterns
219
258
 
@@ -223,7 +262,7 @@ deno task publish # Publish to JSR and NPM
223
262
  import { createHttpApi, opts } from "@marianmeres/http-utils";
224
263
 
225
264
  const api = createHttpApi("https://api.example.com", {
226
- headers: { "Authorization": "Bearer token" }
265
+ headers: { "Authorization": "Bearer token" },
227
266
  });
228
267
 
229
268
  // Legacy API (default - object is request body)
@@ -238,11 +277,11 @@ await api.post("/users", opts({ data: { name: "John" }, params: { token: "abc" }
238
277
 
239
278
  ```typescript
240
279
  try {
241
- await api.get("/resource");
280
+ await api.get("/resource");
242
281
  } catch (error) {
243
- if (error instanceof HTTP_ERROR.NotFound) {
244
- // Handle 404
245
- }
282
+ if (error instanceof HTTP_ERROR.NotFound) {
283
+ // Handle 404
284
+ }
246
285
  }
247
286
  ```
248
287
 
@@ -250,6 +289,6 @@ try {
250
289
 
251
290
  ```typescript
252
291
  const api = createHttpApi("https://api.example.com", async () => ({
253
- headers: { "Authorization": `Bearer ${await getToken()}` }
292
+ headers: { "Authorization": `Bearer ${await getToken()}` },
254
293
  }));
255
294
  ```