@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 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,137 @@ 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
+ 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 (204/205) return `null`
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 `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
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 globalbuilt-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 | 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 |
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 | 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/` |
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
- headers: { "Authorization": "Bearer token" }
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
- await api.get("/resource");
272
+ await api.get("/resource");
242
273
  } catch (error) {
243
- if (error instanceof HTTP_ERROR.NotFound) {
244
- // Handle 404
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
- headers: { "Authorization": `Bearer ${await getToken()}` }
284
+ headers: { "Authorization": `Bearer ${await getToken()}` },
254
285
  }));
255
286
  ```