@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/README.md +261 -44
- package/dist/api.d.ts +140 -16
- package/dist/api.js +298 -0
- package/dist/error.d.ts +45 -2
- package/dist/error.js +243 -0
- package/dist/mod.d.ts +3 -0
- package/dist/mod.js +3 -0
- package/dist/status.d.ts +18 -2
- package/dist/status.js +137 -0
- package/package.json +6 -40
- package/dist/index.cjs +0 -472
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -466
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
|
-
|
|
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
package/dist/mod.js
ADDED
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
|
-
|
|
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
|
|
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;
|