@marianmeres/http-utils 1.23.0 → 2.0.2

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/dist/api.js ADDED
@@ -0,0 +1,298 @@
1
+ import { createHttpError } from './error.js';
2
+ // This is an opinionated HTTP client wrapper and may not be suitable for every use case.
3
+ // It provides convenient defaults over plain fetch calls without adding unnecessary abstractions.
4
+ /**
5
+ * Deep merges two objects. Later properties overwrite earlier properties.
6
+ */
7
+ function deepMerge(target, source) {
8
+ const output = { ...target };
9
+ if (isObject(target) && isObject(source)) {
10
+ Object.keys(source).forEach(key => {
11
+ if (isObject(source[key])) {
12
+ if (!(key in target)) {
13
+ Object.assign(output, { [key]: source[key] });
14
+ }
15
+ else {
16
+ output[key] = deepMerge(target[key], source[key]);
17
+ }
18
+ }
19
+ else {
20
+ Object.assign(output, { [key]: source[key] });
21
+ }
22
+ });
23
+ }
24
+ return output;
25
+ }
26
+ function isObject(item) {
27
+ return item && typeof item === 'object' && !Array.isArray(item);
28
+ }
29
+ const _fetchRaw = async ({ method, path, data = null, token = null, headers = null, signal, credentials, }) => {
30
+ const normalizedHeaders = Object.entries(headers || {}).reduce((m, [k, v]) => ({ ...m, [k.toLowerCase()]: v }), {});
31
+ const opts = {
32
+ method,
33
+ credentials: credentials ?? undefined,
34
+ headers: normalizedHeaders,
35
+ signal
36
+ };
37
+ if (data) {
38
+ const isObj = typeof data === 'object';
39
+ // FormData: multipart/form-data -- no explicit Content-Type
40
+ if (data instanceof FormData) {
41
+ opts.body = data;
42
+ }
43
+ // Cover 99% of use cases (may not fit all scenarios)
44
+ else {
45
+ // If not explicitly stated, assume JSON
46
+ if (isObj || !normalizedHeaders['content-type']) {
47
+ normalizedHeaders['content-type'] = 'application/json';
48
+ }
49
+ opts.body = JSON.stringify(data);
50
+ }
51
+ }
52
+ // Opinionated convention: auto-add Bearer token
53
+ if (token) {
54
+ normalizedHeaders['authorization'] = `Bearer ${token}`;
55
+ }
56
+ opts.headers = normalizedHeaders;
57
+ return await fetch(path, opts);
58
+ };
59
+ const _fetch = async (params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) => {
60
+ if (_dumpParams)
61
+ return params;
62
+ const r = await _fetchRaw(params);
63
+ if (params.raw)
64
+ return r;
65
+ // Convert Headers to plain object
66
+ const headers = [...r.headers.entries()].reduce((m, [k, v]) => ({ ...m, [k]: v }), {});
67
+ // Mutate respHeaders to provide access to response headers and status
68
+ if (respHeaders) {
69
+ Object.assign(respHeaders, headers,
70
+ // Add status/text under special keys
71
+ { __http_status_code__: r.status, __http_status_text__: r.statusText });
72
+ }
73
+ let body = await r.text();
74
+ // prettier-ignore
75
+ try {
76
+ body = JSON.parse(body);
77
+ }
78
+ catch (_e) { /* ignore parse errors */ }
79
+ params.assert ??= true; // default is true
80
+ if (!r.ok && params.assert) {
81
+ // now we need to extract error message from an unknown response... this is obviously
82
+ // impossible unless we know what to expect, but we'll do some educated tries...
83
+ const extractor = errorMessageExtractor ?? // provided arg
84
+ createHttpApi.defaultErrorMessageExtractor ?? // static default
85
+ // educated guess fallback
86
+ function (_body, _response) {
87
+ let msg =
88
+ // try opinionated convention first
89
+ _body?.error?.message ||
90
+ _body?.message ||
91
+ _body?.error ||
92
+ _response?.statusText ||
93
+ 'Unknown error';
94
+ if (msg.length > 255)
95
+ msg = `[Shortened]: ${msg.slice(0, 255)}`;
96
+ return msg;
97
+ };
98
+ // adding `cause` describing more details
99
+ throw createHttpError(r.status, extractor(body, r), body, {
100
+ method: params.method,
101
+ path: params.path,
102
+ response: {
103
+ status: r.status,
104
+ statusText: r.statusText,
105
+ headers,
106
+ },
107
+ });
108
+ }
109
+ return body;
110
+ };
111
+ /**
112
+ * HTTP API client with convenient defaults and error handling.
113
+ */
114
+ export class HttpApi {
115
+ #base;
116
+ #defaults;
117
+ #factoryErrorMessageExtractor;
118
+ constructor(base, defaults, factoryErrorMessageExtractor) {
119
+ this.#base = base;
120
+ this.#defaults = defaults;
121
+ this.#factoryErrorMessageExtractor = factoryErrorMessageExtractor;
122
+ }
123
+ #merge(a, b) {
124
+ return deepMerge(a, b);
125
+ }
126
+ async #getDefs() {
127
+ if (typeof this.#defaults === 'function') {
128
+ return { ...(await this.#defaults()) };
129
+ }
130
+ return { ...(this.#defaults || {}) };
131
+ }
132
+ #buildPath(path, base) {
133
+ base = `${base || ''}`;
134
+ path = `${path || ''}`;
135
+ return /^https?:/.test(path) ? path : base + path;
136
+ }
137
+ async get(path, paramsOrOptions, respHeaders, errorMessageExtractor, _dumpParams = false) {
138
+ // Detect which API is being used
139
+ let params;
140
+ let headers = null;
141
+ let extractor = null;
142
+ if (paramsOrOptions && ('respHeaders' in paramsOrOptions || 'errorExtractor' in paramsOrOptions)) {
143
+ // New options API
144
+ const opts = paramsOrOptions;
145
+ params = opts.params;
146
+ headers = opts.respHeaders ?? null;
147
+ extractor = opts.errorExtractor ?? null;
148
+ }
149
+ else {
150
+ // Legacy positional API
151
+ params = paramsOrOptions;
152
+ headers = respHeaders ?? null;
153
+ extractor = errorMessageExtractor ?? null;
154
+ }
155
+ path = this.#buildPath(path, this.#base);
156
+ return _fetch(this.#merge(await this.#getDefs(), { ...params, method: 'GET', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
157
+ }
158
+ async post(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
159
+ // Detect which API is being used
160
+ let data = null;
161
+ let fetchParams;
162
+ let headers = null;
163
+ let extractor = null;
164
+ if (dataOrOptions && ('data' in dataOrOptions ||
165
+ 'params' in dataOrOptions ||
166
+ 'respHeaders' in dataOrOptions ||
167
+ 'errorExtractor' in dataOrOptions)) {
168
+ // New options API
169
+ const opts = dataOrOptions;
170
+ data = opts.data ?? null;
171
+ fetchParams = opts.params;
172
+ headers = opts.respHeaders ?? null;
173
+ extractor = opts.errorExtractor ?? null;
174
+ }
175
+ else {
176
+ // Legacy positional API
177
+ data = dataOrOptions ?? null;
178
+ fetchParams = params;
179
+ headers = respHeaders ?? null;
180
+ extractor = errorMessageExtractor ?? null;
181
+ }
182
+ path = this.#buildPath(path, this.#base);
183
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'POST', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
184
+ }
185
+ async put(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
186
+ let data = null;
187
+ let fetchParams;
188
+ let headers = null;
189
+ let extractor = null;
190
+ if (dataOrOptions && ('data' in dataOrOptions ||
191
+ 'params' in dataOrOptions ||
192
+ 'respHeaders' in dataOrOptions ||
193
+ 'errorExtractor' in dataOrOptions)) {
194
+ const opts = dataOrOptions;
195
+ data = opts.data ?? null;
196
+ fetchParams = opts.params;
197
+ headers = opts.respHeaders ?? null;
198
+ extractor = opts.errorExtractor ?? null;
199
+ }
200
+ else {
201
+ data = dataOrOptions ?? null;
202
+ fetchParams = params;
203
+ headers = respHeaders ?? null;
204
+ extractor = errorMessageExtractor ?? null;
205
+ }
206
+ path = this.#buildPath(path, this.#base);
207
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'PUT', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
208
+ }
209
+ async patch(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
210
+ let data = null;
211
+ let fetchParams;
212
+ let headers = null;
213
+ let extractor = null;
214
+ if (dataOrOptions && ('data' in dataOrOptions ||
215
+ 'params' in dataOrOptions ||
216
+ 'respHeaders' in dataOrOptions ||
217
+ 'errorExtractor' in dataOrOptions)) {
218
+ const opts = dataOrOptions;
219
+ data = opts.data ?? null;
220
+ fetchParams = opts.params;
221
+ headers = opts.respHeaders ?? null;
222
+ extractor = opts.errorExtractor ?? null;
223
+ }
224
+ else {
225
+ data = dataOrOptions ?? null;
226
+ fetchParams = params;
227
+ headers = respHeaders ?? null;
228
+ extractor = errorMessageExtractor ?? null;
229
+ }
230
+ path = this.#buildPath(path, this.#base);
231
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'PATCH', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
232
+ }
233
+ async del(path, dataOrOptions, params, respHeaders, errorMessageExtractor, _dumpParams = false) {
234
+ let data = null;
235
+ let fetchParams;
236
+ let headers = null;
237
+ let extractor = null;
238
+ if (dataOrOptions && ('data' in dataOrOptions ||
239
+ 'params' in dataOrOptions ||
240
+ 'respHeaders' in dataOrOptions ||
241
+ 'errorExtractor' in dataOrOptions)) {
242
+ const opts = dataOrOptions;
243
+ data = opts.data ?? null;
244
+ fetchParams = opts.params;
245
+ headers = opts.respHeaders ?? null;
246
+ extractor = opts.errorExtractor ?? null;
247
+ }
248
+ else {
249
+ data = dataOrOptions ?? null;
250
+ fetchParams = params;
251
+ headers = respHeaders ?? null;
252
+ extractor = errorMessageExtractor ?? null;
253
+ }
254
+ path = this.#buildPath(path, this.#base);
255
+ return _fetch(this.#merge(await this.#getDefs(), { ...(fetchParams || {}), data, method: 'DELETE', path }), headers, extractor ?? this.#factoryErrorMessageExtractor, _dumpParams);
256
+ }
257
+ /**
258
+ * Helper method to build the full URL from a path.
259
+ *
260
+ * @param path - The path to resolve (absolute URLs are returned as-is).
261
+ * @returns The resolved URL (base + path, or just path if it's already absolute).
262
+ */
263
+ url(path) {
264
+ return this.#buildPath(path, this.#base);
265
+ }
266
+ /**
267
+ * Get or set the base URL for all requests.
268
+ */
269
+ get base() {
270
+ return this.#base;
271
+ }
272
+ set base(v) {
273
+ this.#base = v;
274
+ }
275
+ }
276
+ /**
277
+ * Creates an HTTP API client with convenient defaults and error handling.
278
+ *
279
+ * @param base - Optional base URL to prepend to all requests. Can be changed later via the `base` property.
280
+ * @param defaults - Optional default parameters to merge with each request. Can be an object or async function returning an object.
281
+ * @param factoryErrorMessageExtractor - Optional function to extract error messages from failed responses.
282
+ *
283
+ * @returns An HttpApi instance with methods: get, post, put, patch, del, url, base.
284
+ *
285
+ * @example
286
+ * ```ts
287
+ * const api = createHttpApi('https://api.example.com', {
288
+ * headers: { 'Authorization': 'Bearer token' }
289
+ * });
290
+ *
291
+ * const data = await api.get('/users');
292
+ * await api.post('/users', { name: 'John' });
293
+ * ```
294
+ */
295
+ export function createHttpApi(base, defaults, factoryErrorMessageExtractor) {
296
+ return new HttpApi(base, defaults, factoryErrorMessageExtractor);
297
+ }
298
+ createHttpApi.defaultErrorMessageExtractor = null;
package/dist/error.d.ts CHANGED
@@ -1,7 +1,13 @@
1
+ /**
2
+ * Base HTTP error class. Extends Error with HTTP-specific properties.
3
+ */
1
4
  declare class HttpError extends Error {
2
5
  name: string;
6
+ /** HTTP status code (e.g., 404, 500) */
3
7
  status: number;
8
+ /** HTTP status text (e.g., "Not Found", "Internal Server Error") */
4
9
  statusText: string;
10
+ /** Response body (auto-parsed as JSON if possible) */
5
11
  body: any;
6
12
  }
7
13
  declare class BadRequest extends HttpError {
@@ -82,6 +88,7 @@ declare class ServiceUnavailable extends HttpError {
82
88
  status: number;
83
89
  statusText: string;
84
90
  }
91
+ export { HttpError, BadRequest, Unauthorized, Forbidden, NotFound, MethodNotAllowed, RequestTimeout, Conflict, Gone, LengthRequired, ImATeapot, UnprocessableContent, TooManyRequests, InternalServerError, NotImplemented, BadGateway, ServiceUnavailable, };
85
92
  export declare const HTTP_ERROR: {
86
93
  HttpError: typeof HttpError;
87
94
  BadRequest: typeof BadRequest;
@@ -101,6 +108,42 @@ export declare const HTTP_ERROR: {
101
108
  BadGateway: typeof BadGateway;
102
109
  ServiceUnavailable: typeof ServiceUnavailable;
103
110
  };
104
- export declare const createHttpError: (code: number | string, message?: string | null, body?: string | null, cause?: any) => HttpError;
111
+ /**
112
+ * Creates an HTTP error from a status code and optional details.
113
+ * Returns a specific error class for well-known status codes (e.g., 404 → NotFound),
114
+ * or generic HttpError for unknown codes. Invalid codes default to 500.
115
+ *
116
+ * @param code - HTTP status code (400-599).
117
+ * @param message - Optional error message (defaults to status text).
118
+ * @param body - Optional response body (will be auto-parsed as JSON if it's a string).
119
+ * @param cause - Optional error cause/details (will be auto-parsed as JSON if it's a string).
120
+ *
121
+ * @returns An HttpError instance (or subclass for well-known codes).
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * const err = createHttpError(404, 'User not found', { id: 123 });
126
+ * console.log(err instanceof NotFound); // true
127
+ * console.log(err.status); // 404
128
+ * console.log(err.body); // { id: 123 }
129
+ * ```
130
+ */
131
+ export declare const createHttpError: (code: number | string, message?: string | null, body?: any, cause?: any) => HttpError;
132
+ /**
133
+ * Extracts a human-readable error message from various error formats.
134
+ * Tries multiple strategies to find the best message, with fallbacks.
135
+ *
136
+ * Priority order:
137
+ * 1. e.cause.message / e.cause.code / e.cause (if string)
138
+ * 2. e.body.error.message / e.body.message / e.body.error / e.body (if string)
139
+ * 3. e.message
140
+ * 4. e.name
141
+ * 5. e.toString()
142
+ * 6. "Unknown Error"
143
+ *
144
+ * @param e - The error to extract a message from (can be any type).
145
+ * @param stripErrorPrefix - Whether to remove "Error: " prefix from the message (default: true).
146
+ *
147
+ * @returns A human-readable error message string.
148
+ */
105
149
  export declare const getErrorMessage: (e: any, stripErrorPrefix?: boolean) => string;
106
- export {};
package/dist/error.js ADDED
@@ -0,0 +1,243 @@
1
+ import { HTTP_STATUS } from './status.js';
2
+ /**
3
+ * Base HTTP error class. Extends Error with HTTP-specific properties.
4
+ */
5
+ class HttpError extends Error {
6
+ name = 'HttpError';
7
+ /** HTTP status code (e.g., 404, 500) */
8
+ status = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR.CODE;
9
+ /** HTTP status text (e.g., "Not Found", "Internal Server Error") */
10
+ statusText = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR.TEXT;
11
+ /** Response body (auto-parsed as JSON if possible) */
12
+ body = null;
13
+ }
14
+ // some more specific instances of the well known ones...
15
+ // client
16
+ class BadRequest extends HttpError {
17
+ name = 'HttpBadRequestError';
18
+ status = HTTP_STATUS.ERROR_CLIENT.BAD_REQUEST.CODE;
19
+ statusText = HTTP_STATUS.ERROR_CLIENT.BAD_REQUEST.TEXT;
20
+ }
21
+ class Unauthorized extends HttpError {
22
+ name = 'HttpUnauthorizedError';
23
+ status = HTTP_STATUS.ERROR_CLIENT.UNAUTHORIZED.CODE;
24
+ statusText = HTTP_STATUS.ERROR_CLIENT.UNAUTHORIZED.TEXT;
25
+ }
26
+ class Forbidden extends HttpError {
27
+ name = 'HttpForbiddenError';
28
+ status = HTTP_STATUS.ERROR_CLIENT.FORBIDDEN.CODE;
29
+ statusText = HTTP_STATUS.ERROR_CLIENT.FORBIDDEN.TEXT;
30
+ }
31
+ class NotFound extends HttpError {
32
+ name = 'HttpNotFoundError';
33
+ status = HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.CODE;
34
+ statusText = HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.TEXT;
35
+ }
36
+ class MethodNotAllowed extends HttpError {
37
+ name = 'HttpMethodNotAllowedError';
38
+ status = HTTP_STATUS.ERROR_CLIENT.METHOD_NOT_ALLOWED.CODE;
39
+ statusText = HTTP_STATUS.ERROR_CLIENT.METHOD_NOT_ALLOWED.TEXT;
40
+ }
41
+ class RequestTimeout extends HttpError {
42
+ name = 'HttpRequestTimeoutError';
43
+ status = HTTP_STATUS.ERROR_CLIENT.REQUEST_TIMEOUT.CODE;
44
+ statusText = HTTP_STATUS.ERROR_CLIENT.REQUEST_TIMEOUT.TEXT;
45
+ }
46
+ class Conflict extends HttpError {
47
+ name = 'HttpConflictError';
48
+ status = HTTP_STATUS.ERROR_CLIENT.CONFLICT.CODE;
49
+ statusText = HTTP_STATUS.ERROR_CLIENT.CONFLICT.TEXT;
50
+ }
51
+ class Gone extends HttpError {
52
+ name = 'HttpGoneError';
53
+ status = HTTP_STATUS.ERROR_CLIENT.GONE.CODE;
54
+ statusText = HTTP_STATUS.ERROR_CLIENT.GONE.TEXT;
55
+ }
56
+ class LengthRequired extends HttpError {
57
+ name = 'HttpLengthRequiredError';
58
+ status = HTTP_STATUS.ERROR_CLIENT.LENGTH_REQUIRED.CODE;
59
+ statusText = HTTP_STATUS.ERROR_CLIENT.LENGTH_REQUIRED.TEXT;
60
+ }
61
+ class UnprocessableContent extends HttpError {
62
+ name = 'HttpUnprocessableContentError';
63
+ status = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.CODE;
64
+ statusText = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.TEXT;
65
+ }
66
+ class TooManyRequests extends HttpError {
67
+ name = 'HttpTooManyRequestsError';
68
+ status = HTTP_STATUS.ERROR_CLIENT.TOO_MANY_REQUESTS.CODE;
69
+ statusText = HTTP_STATUS.ERROR_CLIENT.TOO_MANY_REQUESTS.TEXT;
70
+ }
71
+ class ImATeapot extends HttpError {
72
+ name = 'HttpImATeapotError';
73
+ status = HTTP_STATUS.ERROR_CLIENT.IM_A_TEAPOT.CODE;
74
+ statusText = HTTP_STATUS.ERROR_CLIENT.IM_A_TEAPOT.TEXT;
75
+ }
76
+ // server
77
+ class InternalServerError extends HttpError {
78
+ name = 'HttpInternalServerError';
79
+ }
80
+ class NotImplemented extends HttpError {
81
+ name = 'HttpServiceUnavailableError';
82
+ status = HTTP_STATUS.ERROR_SERVER.NOT_IMPLEMENTED.CODE;
83
+ statusText = HTTP_STATUS.ERROR_SERVER.NOT_IMPLEMENTED.TEXT;
84
+ }
85
+ class BadGateway extends HttpError {
86
+ name = 'HttpBadGatewayError';
87
+ status = HTTP_STATUS.ERROR_SERVER.BAD_GATEWAY.CODE;
88
+ statusText = HTTP_STATUS.ERROR_SERVER.BAD_GATEWAY.TEXT;
89
+ }
90
+ class ServiceUnavailable extends HttpError {
91
+ name = 'HttpServiceUnavailableError';
92
+ status = HTTP_STATUS.ERROR_SERVER.SERVICE_UNAVAILABLE.CODE;
93
+ statusText = HTTP_STATUS.ERROR_SERVER.SERVICE_UNAVAILABLE.TEXT;
94
+ }
95
+ // Export individual error classes
96
+ export { HttpError,
97
+ // Client errors
98
+ BadRequest, Unauthorized, Forbidden, NotFound, MethodNotAllowed, RequestTimeout, Conflict, Gone, LengthRequired, ImATeapot, UnprocessableContent, TooManyRequests,
99
+ // Server errors
100
+ InternalServerError, NotImplemented, BadGateway, ServiceUnavailable, };
101
+ // Namespace export for convenience
102
+ export const HTTP_ERROR = {
103
+ // base
104
+ HttpError,
105
+ // client
106
+ BadRequest,
107
+ Unauthorized,
108
+ Forbidden,
109
+ NotFound,
110
+ MethodNotAllowed,
111
+ RequestTimeout,
112
+ Conflict,
113
+ Gone,
114
+ LengthRequired,
115
+ ImATeapot,
116
+ UnprocessableContent,
117
+ TooManyRequests,
118
+ // server
119
+ InternalServerError,
120
+ NotImplemented,
121
+ BadGateway,
122
+ ServiceUnavailable,
123
+ };
124
+ const _wellKnownCtorMap = {
125
+ '400': BadRequest,
126
+ '401': Unauthorized,
127
+ '403': Forbidden,
128
+ '404': NotFound,
129
+ '405': MethodNotAllowed,
130
+ '408': RequestTimeout,
131
+ '409': Conflict,
132
+ '410': Gone,
133
+ '411': LengthRequired,
134
+ '418': ImATeapot,
135
+ '422': UnprocessableContent,
136
+ '429': TooManyRequests,
137
+ //
138
+ '500': InternalServerError,
139
+ '501': NotImplemented,
140
+ '502': BadGateway,
141
+ '503': ServiceUnavailable,
142
+ };
143
+ const _maybeJsonParse = (v) => {
144
+ if (typeof v === 'string') {
145
+ try {
146
+ v = JSON.parse(v);
147
+ }
148
+ catch (e) { }
149
+ }
150
+ return v;
151
+ };
152
+ /**
153
+ * Creates an HTTP error from a status code and optional details.
154
+ * Returns a specific error class for well-known status codes (e.g., 404 → NotFound),
155
+ * or generic HttpError for unknown codes. Invalid codes default to 500.
156
+ *
157
+ * @param code - HTTP status code (400-599).
158
+ * @param message - Optional error message (defaults to status text).
159
+ * @param body - Optional response body (will be auto-parsed as JSON if it's a string).
160
+ * @param cause - Optional error cause/details (will be auto-parsed as JSON if it's a string).
161
+ *
162
+ * @returns An HttpError instance (or subclass for well-known codes).
163
+ *
164
+ * @example
165
+ * ```ts
166
+ * const err = createHttpError(404, 'User not found', { id: 123 });
167
+ * console.log(err instanceof NotFound); // true
168
+ * console.log(err.status); // 404
169
+ * console.log(err.body); // { id: 123 }
170
+ * ```
171
+ */
172
+ export const createHttpError = (code, message, body, cause) => {
173
+ const fallback = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR;
174
+ code = Number(code);
175
+ if (isNaN(code) || !(code >= 400 && code < 600))
176
+ code = fallback.CODE;
177
+ // opinionated conventions
178
+ body = _maybeJsonParse(body);
179
+ cause = _maybeJsonParse(cause);
180
+ // try to find the well known one, otherwise fallback to generic
181
+ const ctor = _wellKnownCtorMap[`${code}`] ?? HttpError;
182
+ //
183
+ const found = HTTP_STATUS.findByCode(code);
184
+ const statusText = found?.TEXT ?? fallback.TEXT;
185
+ //
186
+ let e = new ctor(message || statusText, { cause });
187
+ e.status = found?.CODE ?? fallback.CODE;
188
+ e.statusText = statusText;
189
+ e.body = body;
190
+ return e;
191
+ };
192
+ /**
193
+ * Extracts a human-readable error message from various error formats.
194
+ * Tries multiple strategies to find the best message, with fallbacks.
195
+ *
196
+ * Priority order:
197
+ * 1. e.cause.message / e.cause.code / e.cause (if string)
198
+ * 2. e.body.error.message / e.body.message / e.body.error / e.body (if string)
199
+ * 3. e.message
200
+ * 4. e.name
201
+ * 5. e.toString()
202
+ * 6. "Unknown Error"
203
+ *
204
+ * @param e - The error to extract a message from (can be any type).
205
+ * @param stripErrorPrefix - Whether to remove "Error: " prefix from the message (default: true).
206
+ *
207
+ * @returns A human-readable error message string.
208
+ */
209
+ export const getErrorMessage = (e, stripErrorPrefix = true) => {
210
+ if (!e)
211
+ return '';
212
+ // Errors may bubble from various sources which are not always under control.
213
+ // We try our best to extract a meaningful message using common conventions.
214
+ const cause = _maybeJsonParse(e?.cause);
215
+ const body = _maybeJsonParse(e?.body);
216
+ let msg =
217
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
218
+ // e.cause is the standard prop for error details, so should be considered as
219
+ // the most authoritative (if available)
220
+ // "code" and "message" are my own conventions
221
+ cause?.message ||
222
+ cause?.code ||
223
+ (typeof cause === 'string' ? cause : null) ||
224
+ // non-standard "body" is this package's HttpError prop
225
+ body?.error?.message ||
226
+ body?.message ||
227
+ body?.error ||
228
+ (typeof body === 'string' ? body : null) ||
229
+ // the common message from Error ctor (e.g. "Foo" if new TypeError("Foo"))
230
+ e?.message ||
231
+ // the Error class name (e.g. TypeError)
232
+ e?.name ||
233
+ // this should handle (almost) everything else (mainly if e is not an Error instance)
234
+ e?.toString() ||
235
+ // very last fallback if `toString()` was not available (or returned empty)
236
+ 'Unknown Error';
237
+ // ensure we're sending string
238
+ msg = `${msg}`;
239
+ if (stripErrorPrefix) {
240
+ msg = msg.replace(/^[^:]*Error: /i, '');
241
+ }
242
+ return msg;
243
+ };
package/dist/mod.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { HttpApi, createHttpApi, type DataOptions, type GetOptions, } from "./api.js";
2
+ export { HTTP_ERROR, createHttpError, getErrorMessage } from "./error.js";
3
+ export { HTTP_STATUS } from "./status.js";
package/dist/mod.js ADDED
@@ -0,0 +1,3 @@
1
+ export { HttpApi, createHttpApi, } from "./api.js";
2
+ export { HTTP_ERROR, createHttpError, getErrorMessage } from "./error.js";
3
+ export { HTTP_STATUS } from "./status.js";
package/dist/status.d.ts CHANGED
@@ -1,3 +1,7 @@
1
+ /**
2
+ * HTTP status codes organized by category with convenience shortcuts.
3
+ * Provides comprehensive coverage of standard HTTP status codes.
4
+ */
1
5
  export declare class HTTP_STATUS {
2
6
  static readonly INFO: {
3
7
  CONTINUE: {
@@ -60,7 +64,7 @@ export declare class HTTP_STATUS {
60
64
  };
61
65
  };
62
66
  static readonly REDIRECT: {
63
- MUTLIPLE_CHOICES: {
67
+ MULTIPLE_CHOICES: {
64
68
  CODE: number;
65
69
  TEXT: string;
66
70
  };
@@ -257,7 +261,7 @@ export declare class HTTP_STATUS {
257
261
  static readonly CREATED: number;
258
262
  static readonly ACCEPTED: number;
259
263
  static readonly NO_CONTENT: number;
260
- static readonly MUTLIPLE_CHOICES: number;
264
+ static readonly MULTIPLE_CHOICES: number;
261
265
  static readonly FOUND: number;
262
266
  static readonly NOT_MODIFIED: number;
263
267
  static readonly MOVED_PERMANENTLY: number;
@@ -275,6 +279,18 @@ export declare class HTTP_STATUS {
275
279
  static readonly INTERNAL_SERVER_ERROR: number;
276
280
  static readonly NOT_IMPLEMENTED: number;
277
281
  static readonly SERVICE_UNAVAILABLE: number;
282
+ /**
283
+ * Finds a status code definition by its numeric code.
284
+ *
285
+ * @param code - The HTTP status code to look up (e.g., 200, 404, 500).
286
+ * @returns An object with CODE, TEXT, _TYPE (category), and _KEY (name), or null if not found.
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * const status = HTTP_STATUS.findByCode(404);
291
+ * // { CODE: 404, TEXT: "Not Found", _TYPE: "ERROR_CLIENT", _KEY: "NOT_FOUND" }
292
+ * ```
293
+ */
278
294
  static findByCode(code: number | string): {
279
295
  CODE: number;
280
296
  TEXT: string;