@marianmeres/http-utils 1.11.0 → 1.13.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/dist/error.d.ts CHANGED
@@ -44,6 +44,11 @@ declare class Gone extends HttpError {
44
44
  status: number;
45
45
  statusText: string;
46
46
  }
47
+ declare class UnprocessableContent extends HttpError {
48
+ name: string;
49
+ status: number;
50
+ statusText: string;
51
+ }
47
52
  declare class ImATeapot extends HttpError {
48
53
  name: string;
49
54
  status: number;
@@ -78,10 +83,12 @@ export declare const HTTP_ERROR: {
78
83
  Conflict: typeof Conflict;
79
84
  Gone: typeof Gone;
80
85
  ImATeapot: typeof ImATeapot;
86
+ UnprocessableContent: typeof UnprocessableContent;
81
87
  InternalServerError: typeof InternalServerError;
82
88
  NotImplemented: typeof NotImplemented;
83
89
  BadGateway: typeof BadGateway;
84
90
  ServiceUnavailable: typeof ServiceUnavailable;
85
91
  };
86
- export declare const createHttpError: (code: number | string, message?: string | null, body?: string | null, cause?: any) => BadRequest | Unauthorized | Forbidden | NotFound | MethodNotAllowed | RequestTimeout | Conflict | Gone | ImATeapot | InternalServerError | NotImplemented | BadGateway | ServiceUnavailable;
92
+ export declare const createHttpError: (code: number | string, message?: string | null, body?: string | null, cause?: any) => BadRequest | Unauthorized | Forbidden | NotFound | MethodNotAllowed | RequestTimeout | Conflict | Gone | UnprocessableContent | ImATeapot | InternalServerError | NotImplemented | BadGateway | ServiceUnavailable;
93
+ export declare const getErrorMessage: (e: any, stripErrorPrefix?: boolean) => string;
87
94
  export {};
package/dist/index.cjs CHANGED
@@ -103,6 +103,7 @@ class HTTP_STATUS {
103
103
  static METHOD_NOT_ALLOWED = HTTP_STATUS.ERROR_CLIENT.METHOD_NOT_ALLOWED.CODE;
104
104
  static CONFLICT = HTTP_STATUS.ERROR_CLIENT.CONFLICT.CODE;
105
105
  static GONE = HTTP_STATUS.ERROR_CLIENT.GONE.CODE;
106
+ static UNPROCESSABLE_CONTENT = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.CODE;
106
107
  // 5xx
107
108
  static INTERNAL_SERVER_ERROR = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR.CODE;
108
109
  static NOT_IMPLEMENTED = HTTP_STATUS.ERROR_SERVER.NOT_IMPLEMENTED.CODE;
@@ -173,6 +174,11 @@ class Gone extends HttpError {
173
174
  status = HTTP_STATUS.ERROR_CLIENT.GONE.CODE;
174
175
  statusText = HTTP_STATUS.ERROR_CLIENT.GONE.TEXT;
175
176
  }
177
+ class UnprocessableContent extends HttpError {
178
+ name = 'HttpUnprocessableContentError';
179
+ status = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.CODE;
180
+ statusText = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.TEXT;
181
+ }
176
182
  class ImATeapot extends HttpError {
177
183
  name = 'HttpImATeapotError';
178
184
  status = HTTP_STATUS.ERROR_CLIENT.IM_A_TEAPOT.CODE;
@@ -211,6 +217,7 @@ const HTTP_ERROR = {
211
217
  Conflict,
212
218
  Gone,
213
219
  ImATeapot,
220
+ UnprocessableContent,
214
221
  // server
215
222
  InternalServerError,
216
223
  NotImplemented,
@@ -227,12 +234,22 @@ const _wellKnownCtorMap = {
227
234
  '409': Conflict,
228
235
  '410': Gone,
229
236
  '418': ImATeapot,
237
+ '422': UnprocessableContent,
230
238
  //
231
239
  '500': InternalServerError,
232
240
  '501': NotImplemented,
233
241
  '502': BadGateway,
234
242
  '503': ServiceUnavailable,
235
243
  };
244
+ const _maybeJsonParse = (v) => {
245
+ if (typeof v === 'string') {
246
+ try {
247
+ v = JSON.parse(v);
248
+ }
249
+ catch (e) { }
250
+ }
251
+ return v;
252
+ };
236
253
  const createHttpError = (code, message,
237
254
  // arbitrary content, typically http response body which threw this error
238
255
  // (will be JSON.parse-d if the content is a valid json string)
@@ -245,21 +262,9 @@ cause) => {
245
262
  if (isNaN(code) || !(code >= 400 && code < 600))
246
263
  code = fallback.CODE;
247
264
  // opinionated convention
248
- if (typeof body === 'string') {
249
- // prettier-ignore
250
- try {
251
- body = JSON.parse(body);
252
- }
253
- catch (e) { }
254
- }
265
+ body = _maybeJsonParse(body);
255
266
  // opinionated convention
256
- if (typeof cause === 'string') {
257
- // prettier-ignore
258
- try {
259
- cause = JSON.parse(cause);
260
- }
261
- catch (e) { }
262
- }
267
+ cause = _maybeJsonParse(cause);
263
268
  // try to find the well known one, otherwise fallback to generic
264
269
  const ctor = _wellKnownCtorMap[`${code}`] ?? HttpError;
265
270
  //
@@ -272,6 +277,40 @@ cause) => {
272
277
  e.body = body;
273
278
  return e;
274
279
  };
280
+ const getErrorMessage = (e, stripErrorPrefix = true) => {
281
+ if (!e)
282
+ return '';
283
+ // PROBLEM is that error may bubble from various sources which are not always under control
284
+ // and even if they were it still may not be trivial to keep similar structure on each error boundary...
285
+ // So, we'll just do what we can, it will not be perfect, but should handle most cases most of the time.
286
+ // Also, I'm relying on some of my own opinionated conventions as well...
287
+ const cause = _maybeJsonParse(e?.cause);
288
+ const body = _maybeJsonParse(e?.body);
289
+ let msg =
290
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
291
+ // e.cause is the standard prop for error details, so should be considered as
292
+ // the most authoritative (if available)
293
+ // "code" and "message" are my own conventions
294
+ cause?.code ||
295
+ cause?.message ||
296
+ (typeof cause === 'string' ? cause : null) ||
297
+ // non-standard "body" is this package's HttpError prop
298
+ body?.error?.message ||
299
+ body?.message ||
300
+ (typeof body === 'string' ? body : null) ||
301
+ // the common message from Error ctor (e.g. "Foo" if new TypeError("Foo"))
302
+ e?.message ||
303
+ // the Error class name (e.g. TypeError)
304
+ e?.name ||
305
+ // this should handle (almost) everything else (mainly if e is not the Error instance)
306
+ e?.toString() ||
307
+ // very last fallback if `toString()` was not available (or returned empty)
308
+ 'Unknown Error';
309
+ if (stripErrorPrefix) {
310
+ msg = msg.replace(/^[^:]*Error: /, '');
311
+ }
312
+ return msg;
313
+ };
275
314
 
276
315
  const _fetchRaw = async ({ method, path, data = null, token = null, headers = null, signal = null, credentials, }) => {
277
316
  headers = Object.entries(headers || {}).reduce((m, [k, v]) => ({ ...m, [k.toLowerCase()]: v }), {});
@@ -397,3 +436,4 @@ exports.HTTP_ERROR = HTTP_ERROR;
397
436
  exports.HTTP_STATUS = HTTP_STATUS;
398
437
  exports.createHttpApi = createHttpApi;
399
438
  exports.createHttpError = createHttpError;
439
+ exports.getErrorMessage = getErrorMessage;
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { HTTP_STATUS } from './status.js';
2
- export { HTTP_ERROR, createHttpError } from './error.js';
2
+ export { HTTP_ERROR, createHttpError, getErrorMessage } from './error.js';
3
3
  export { createHttpApi } from './api.js';
package/dist/index.js CHANGED
@@ -101,6 +101,7 @@ class HTTP_STATUS {
101
101
  static METHOD_NOT_ALLOWED = HTTP_STATUS.ERROR_CLIENT.METHOD_NOT_ALLOWED.CODE;
102
102
  static CONFLICT = HTTP_STATUS.ERROR_CLIENT.CONFLICT.CODE;
103
103
  static GONE = HTTP_STATUS.ERROR_CLIENT.GONE.CODE;
104
+ static UNPROCESSABLE_CONTENT = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.CODE;
104
105
  // 5xx
105
106
  static INTERNAL_SERVER_ERROR = HTTP_STATUS.ERROR_SERVER.INTERNAL_SERVER_ERROR.CODE;
106
107
  static NOT_IMPLEMENTED = HTTP_STATUS.ERROR_SERVER.NOT_IMPLEMENTED.CODE;
@@ -171,6 +172,11 @@ class Gone extends HttpError {
171
172
  status = HTTP_STATUS.ERROR_CLIENT.GONE.CODE;
172
173
  statusText = HTTP_STATUS.ERROR_CLIENT.GONE.TEXT;
173
174
  }
175
+ class UnprocessableContent extends HttpError {
176
+ name = 'HttpUnprocessableContentError';
177
+ status = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.CODE;
178
+ statusText = HTTP_STATUS.ERROR_CLIENT.UNPROCESSABLE_CONTENT.TEXT;
179
+ }
174
180
  class ImATeapot extends HttpError {
175
181
  name = 'HttpImATeapotError';
176
182
  status = HTTP_STATUS.ERROR_CLIENT.IM_A_TEAPOT.CODE;
@@ -209,6 +215,7 @@ const HTTP_ERROR = {
209
215
  Conflict,
210
216
  Gone,
211
217
  ImATeapot,
218
+ UnprocessableContent,
212
219
  // server
213
220
  InternalServerError,
214
221
  NotImplemented,
@@ -225,12 +232,22 @@ const _wellKnownCtorMap = {
225
232
  '409': Conflict,
226
233
  '410': Gone,
227
234
  '418': ImATeapot,
235
+ '422': UnprocessableContent,
228
236
  //
229
237
  '500': InternalServerError,
230
238
  '501': NotImplemented,
231
239
  '502': BadGateway,
232
240
  '503': ServiceUnavailable,
233
241
  };
242
+ const _maybeJsonParse = (v) => {
243
+ if (typeof v === 'string') {
244
+ try {
245
+ v = JSON.parse(v);
246
+ }
247
+ catch (e) { }
248
+ }
249
+ return v;
250
+ };
234
251
  const createHttpError = (code, message,
235
252
  // arbitrary content, typically http response body which threw this error
236
253
  // (will be JSON.parse-d if the content is a valid json string)
@@ -243,21 +260,9 @@ cause) => {
243
260
  if (isNaN(code) || !(code >= 400 && code < 600))
244
261
  code = fallback.CODE;
245
262
  // opinionated convention
246
- if (typeof body === 'string') {
247
- // prettier-ignore
248
- try {
249
- body = JSON.parse(body);
250
- }
251
- catch (e) { }
252
- }
263
+ body = _maybeJsonParse(body);
253
264
  // opinionated convention
254
- if (typeof cause === 'string') {
255
- // prettier-ignore
256
- try {
257
- cause = JSON.parse(cause);
258
- }
259
- catch (e) { }
260
- }
265
+ cause = _maybeJsonParse(cause);
261
266
  // try to find the well known one, otherwise fallback to generic
262
267
  const ctor = _wellKnownCtorMap[`${code}`] ?? HttpError;
263
268
  //
@@ -270,6 +275,40 @@ cause) => {
270
275
  e.body = body;
271
276
  return e;
272
277
  };
278
+ const getErrorMessage = (e, stripErrorPrefix = true) => {
279
+ if (!e)
280
+ return '';
281
+ // PROBLEM is that error may bubble from various sources which are not always under control
282
+ // and even if they were it still may not be trivial to keep similar structure on each error boundary...
283
+ // So, we'll just do what we can, it will not be perfect, but should handle most cases most of the time.
284
+ // Also, I'm relying on some of my own opinionated conventions as well...
285
+ const cause = _maybeJsonParse(e?.cause);
286
+ const body = _maybeJsonParse(e?.body);
287
+ let msg =
288
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
289
+ // e.cause is the standard prop for error details, so should be considered as
290
+ // the most authoritative (if available)
291
+ // "code" and "message" are my own conventions
292
+ cause?.code ||
293
+ cause?.message ||
294
+ (typeof cause === 'string' ? cause : null) ||
295
+ // non-standard "body" is this package's HttpError prop
296
+ body?.error?.message ||
297
+ body?.message ||
298
+ (typeof body === 'string' ? body : null) ||
299
+ // the common message from Error ctor (e.g. "Foo" if new TypeError("Foo"))
300
+ e?.message ||
301
+ // the Error class name (e.g. TypeError)
302
+ e?.name ||
303
+ // this should handle (almost) everything else (mainly if e is not the Error instance)
304
+ e?.toString() ||
305
+ // very last fallback if `toString()` was not available (or returned empty)
306
+ 'Unknown Error';
307
+ if (stripErrorPrefix) {
308
+ msg = msg.replace(/^[^:]*Error: /, '');
309
+ }
310
+ return msg;
311
+ };
273
312
 
274
313
  const _fetchRaw = async ({ method, path, data = null, token = null, headers = null, signal = null, credentials, }) => {
275
314
  headers = Object.entries(headers || {}).reduce((m, [k, v]) => ({ ...m, [k.toLowerCase()]: v }), {});
@@ -391,4 +430,4 @@ function createHttpApi(base, defaults, factoryErrorMessageExtractor) {
391
430
  }
392
431
  createHttpApi.defaultErrorMessageExtractor = null;
393
432
 
394
- export { HTTP_ERROR, HTTP_STATUS, createHttpApi, createHttpError };
433
+ export { HTTP_ERROR, HTTP_STATUS, createHttpApi, createHttpError, getErrorMessage };
package/dist/status.d.ts CHANGED
@@ -270,6 +270,7 @@ export declare class HTTP_STATUS {
270
270
  static readonly METHOD_NOT_ALLOWED: number;
271
271
  static readonly CONFLICT: number;
272
272
  static readonly GONE: number;
273
+ static readonly UNPROCESSABLE_CONTENT: number;
273
274
  static readonly INTERNAL_SERVER_ERROR: number;
274
275
  static readonly NOT_IMPLEMENTED: number;
275
276
  static readonly SERVICE_UNAVAILABLE: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marianmeres/http-utils",
3
- "version": "1.11.0",
3
+ "version": "1.13.0",
4
4
  "description": "Misc DRY http fetch related helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",