@marianmeres/http-utils 1.10.0 → 1.12.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/README.md CHANGED
@@ -32,7 +32,7 @@ try {
32
32
  assert(e.statusText === HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.TEXT);
33
33
  // `body` is a custom prop containing the raw http response body text (JSON.parse-d if available)
34
34
  assert(e.body.message === 'hey');
35
- // `cause` is a standart Error prop, containg here some default debug info
35
+ // `cause` is a standart Error prop, containing here some default debug info
36
36
  assert(err.cause.response.headers)
37
37
  }
38
38
 
package/dist/api.d.ts CHANGED
@@ -12,11 +12,15 @@ interface FetchParams {
12
12
  assert?: null | boolean;
13
13
  }
14
14
  type BaseFetchParams = BaseParams & FetchParams;
15
- export declare const createHttpApi: (base?: string | null, defaults?: Partial<BaseFetchParams> | (() => Promise<Partial<BaseFetchParams>>)) => {
16
- get(path: string, params?: FetchParams, respHeaders?: any, _dumpParams?: boolean): Promise<any>;
17
- post(path: string, data?: any, params?: FetchParams, respHeaders?: any, _dumpParams?: boolean): Promise<any>;
18
- put(path: string, data?: any, params?: FetchParams, respHeaders?: any, _dumpParams?: boolean): Promise<any>;
19
- patch(path: string, data?: any, params?: FetchParams, respHeaders?: any, _dumpParams?: boolean): Promise<any>;
20
- del(path: string, data?: any, params?: FetchParams, respHeaders?: any, _dumpParams?: boolean): Promise<any>;
15
+ type ErrorMessageExtractor = (body: any, response: Response) => any;
16
+ export declare function createHttpApi(base?: string | null, defaults?: Partial<BaseFetchParams> | (() => Promise<Partial<BaseFetchParams>>), factoryErrorMessageExtractor?: ErrorMessageExtractor | null | undefined): {
17
+ get(path: string, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
18
+ post(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
19
+ put(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
20
+ patch(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
21
+ del(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
21
22
  };
23
+ export declare namespace createHttpApi {
24
+ var defaultErrorMessageExtractor: ErrorMessageExtractor | null | undefined;
25
+ }
22
26
  export {};
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,11 @@ 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;
87
93
  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,6 +234,7 @@ 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,
@@ -297,7 +305,7 @@ const _fetchRaw = async ({ method, path, data = null, token = null, headers = nu
297
305
  }
298
306
  return await fetch(path, opts);
299
307
  };
300
- const _fetch = async (params, respHeaders = null, _dumpParams = false) => {
308
+ const _fetch = async (params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) => {
301
309
  if (_dumpParams)
302
310
  return params;
303
311
  const r = await _fetchRaw(params);
@@ -319,7 +327,24 @@ const _fetch = async (params, respHeaders = null, _dumpParams = false) => {
319
327
  catch (e) { }
320
328
  params.assert ??= true; // default is true
321
329
  if (!r.ok && params.assert) {
322
- throw createHttpError(r.status, null, body, {
330
+ // now we need to extract error message from an unknown response... this is obviously
331
+ // impossible unless we know what to expect, but we'll do some educated tries...
332
+ const extractor = errorMessageExtractor ?? // provided arg
333
+ createHttpApi.defaultErrorMessageExtractor ?? // static default
334
+ // educated guess fallback
335
+ function (_body, _response) {
336
+ let msg =
337
+ // try opinionated convention first
338
+ _body?.error?.message ||
339
+ _body?.message ||
340
+ _response?.statusText ||
341
+ 'Unknown error';
342
+ if (msg.length > 255)
343
+ msg = `[Shortened]: ${msg.slice(0, 255)}`;
344
+ return msg;
345
+ };
346
+ // adding `cause` describing more details
347
+ throw createHttpError(r.status, extractor(body, r), body, {
323
348
  method: params.method,
324
349
  path: params.path,
325
350
  response: {
@@ -331,7 +356,7 @@ const _fetch = async (params, respHeaders = null, _dumpParams = false) => {
331
356
  }
332
357
  return body;
333
358
  };
334
- const createHttpApi = (base, defaults) => {
359
+ function createHttpApi(base, defaults, factoryErrorMessageExtractor) {
335
360
  const _merge = (a, b) => {
336
361
  const wrap = { result: a };
337
362
  merge.dset(wrap, 'result', b);
@@ -347,33 +372,34 @@ const createHttpApi = (base, defaults) => {
347
372
  });
348
373
  return {
349
374
  // GET
350
- async get(path, params, respHeaders = null, _dumpParams = false) {
375
+ async get(path, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
351
376
  path = `${base || ''}${path || ''}`;
352
- return _fetch(_merge(await _getDefs(), { ...params, method: 'GET', path }), respHeaders, _dumpParams);
377
+ return _fetch(_merge(await _getDefs(), { ...params, method: 'GET', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
353
378
  },
354
379
  // POST
355
- async post(path, data = null, params, respHeaders = null, _dumpParams = false) {
380
+ async post(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
356
381
  path = `${base || ''}${path || ''}`;
357
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'POST', path }), respHeaders, _dumpParams);
382
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'POST', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
358
383
  },
359
384
  // PUT
360
- async put(path, data = null, params, respHeaders = null, _dumpParams = false) {
385
+ async put(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
361
386
  path = `${base || ''}${path || ''}`;
362
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PUT', path }), respHeaders, _dumpParams);
387
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PUT', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
363
388
  },
364
389
  // PATCH
365
- async patch(path, data = null, params, respHeaders = null, _dumpParams = false) {
390
+ async patch(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
366
391
  path = `${base || ''}${path || ''}`;
367
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PATCH', path }), respHeaders, _dumpParams);
392
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PATCH', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
368
393
  },
369
394
  // DELETE
370
395
  // https://stackoverflow.com/questions/299628/is-an-entity-body-allowed-for-an-http-delete-request
371
- async del(path, data = null, params, respHeaders = null, _dumpParams = false) {
396
+ async del(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
372
397
  path = `${base || ''}${path || ''}`;
373
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'DELETE', path }), respHeaders, _dumpParams);
398
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'DELETE', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
374
399
  },
375
400
  };
376
- };
401
+ }
402
+ createHttpApi.defaultErrorMessageExtractor = null;
377
403
 
378
404
  exports.HTTP_ERROR = HTTP_ERROR;
379
405
  exports.HTTP_STATUS = HTTP_STATUS;
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,6 +232,7 @@ 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,
@@ -295,7 +303,7 @@ const _fetchRaw = async ({ method, path, data = null, token = null, headers = nu
295
303
  }
296
304
  return await fetch(path, opts);
297
305
  };
298
- const _fetch = async (params, respHeaders = null, _dumpParams = false) => {
306
+ const _fetch = async (params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) => {
299
307
  if (_dumpParams)
300
308
  return params;
301
309
  const r = await _fetchRaw(params);
@@ -317,7 +325,24 @@ const _fetch = async (params, respHeaders = null, _dumpParams = false) => {
317
325
  catch (e) { }
318
326
  params.assert ??= true; // default is true
319
327
  if (!r.ok && params.assert) {
320
- throw createHttpError(r.status, null, body, {
328
+ // now we need to extract error message from an unknown response... this is obviously
329
+ // impossible unless we know what to expect, but we'll do some educated tries...
330
+ const extractor = errorMessageExtractor ?? // provided arg
331
+ createHttpApi.defaultErrorMessageExtractor ?? // static default
332
+ // educated guess fallback
333
+ function (_body, _response) {
334
+ let msg =
335
+ // try opinionated convention first
336
+ _body?.error?.message ||
337
+ _body?.message ||
338
+ _response?.statusText ||
339
+ 'Unknown error';
340
+ if (msg.length > 255)
341
+ msg = `[Shortened]: ${msg.slice(0, 255)}`;
342
+ return msg;
343
+ };
344
+ // adding `cause` describing more details
345
+ throw createHttpError(r.status, extractor(body, r), body, {
321
346
  method: params.method,
322
347
  path: params.path,
323
348
  response: {
@@ -329,7 +354,7 @@ const _fetch = async (params, respHeaders = null, _dumpParams = false) => {
329
354
  }
330
355
  return body;
331
356
  };
332
- const createHttpApi = (base, defaults) => {
357
+ function createHttpApi(base, defaults, factoryErrorMessageExtractor) {
333
358
  const _merge = (a, b) => {
334
359
  const wrap = { result: a };
335
360
  dset(wrap, 'result', b);
@@ -345,32 +370,33 @@ const createHttpApi = (base, defaults) => {
345
370
  });
346
371
  return {
347
372
  // GET
348
- async get(path, params, respHeaders = null, _dumpParams = false) {
373
+ async get(path, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
349
374
  path = `${base || ''}${path || ''}`;
350
- return _fetch(_merge(await _getDefs(), { ...params, method: 'GET', path }), respHeaders, _dumpParams);
375
+ return _fetch(_merge(await _getDefs(), { ...params, method: 'GET', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
351
376
  },
352
377
  // POST
353
- async post(path, data = null, params, respHeaders = null, _dumpParams = false) {
378
+ async post(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
354
379
  path = `${base || ''}${path || ''}`;
355
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'POST', path }), respHeaders, _dumpParams);
380
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'POST', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
356
381
  },
357
382
  // PUT
358
- async put(path, data = null, params, respHeaders = null, _dumpParams = false) {
383
+ async put(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
359
384
  path = `${base || ''}${path || ''}`;
360
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PUT', path }), respHeaders, _dumpParams);
385
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PUT', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
361
386
  },
362
387
  // PATCH
363
- async patch(path, data = null, params, respHeaders = null, _dumpParams = false) {
388
+ async patch(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
364
389
  path = `${base || ''}${path || ''}`;
365
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PATCH', path }), respHeaders, _dumpParams);
390
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'PATCH', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
366
391
  },
367
392
  // DELETE
368
393
  // https://stackoverflow.com/questions/299628/is-an-entity-body-allowed-for-an-http-delete-request
369
- async del(path, data = null, params, respHeaders = null, _dumpParams = false) {
394
+ async del(path, data = null, params, respHeaders = null, errorMessageExtractor = null, _dumpParams = false) {
370
395
  path = `${base || ''}${path || ''}`;
371
- return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'DELETE', path }), respHeaders, _dumpParams);
396
+ return _fetch(_merge(await _getDefs(), { ...(params || {}), data, method: 'DELETE', path }), respHeaders, errorMessageExtractor ?? factoryErrorMessageExtractor, _dumpParams);
372
397
  },
373
398
  };
374
- };
399
+ }
400
+ createHttpApi.defaultErrorMessageExtractor = null;
375
401
 
376
402
  export { HTTP_ERROR, HTTP_STATUS, createHttpApi, createHttpError };
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.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Misc DRY http fetch related helpers",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "homepage": "https://github.com/marianmeres/http-utils#readme",
34
34
  "devDependencies": {
35
+ "@marianmeres/clog": "^1.0.1",
35
36
  "@marianmeres/release": "^1.1.2",
36
37
  "@marianmeres/test-runner": "^2.0.16",
37
38
  "@rollup/plugin-typescript": "^11.1.6",