@naturalcycles/js-lib 14.142.0 → 14.144.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/app.error.d.ts +4 -4
- package/dist/error/app.error.js +3 -3
- package/dist/error/assert.d.ts +5 -3
- package/dist/error/assert.js +18 -6
- package/dist/error/error.model.d.ts +44 -21
- package/dist/error/error.util.d.ts +4 -4
- package/dist/error/error.util.js +8 -11
- package/dist/error/httpRequestError.d.ts +22 -0
- package/dist/error/httpRequestError.js +27 -0
- package/dist/error/jsonParseError.d.ts +11 -0
- package/dist/error/jsonParseError.js +11 -0
- package/dist/error/try.d.ts +4 -0
- package/dist/error/try.js +10 -2
- package/dist/http/fetcher.js +34 -31
- package/dist/http/http.model.d.ts +4 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/promise/pTimeout.d.ts +2 -2
- package/dist/promise/pTimeout.js +2 -2
- package/dist/string/json.util.d.ts +9 -1
- package/dist/string/json.util.js +19 -1
- package/dist/string/stringifyAny.js +6 -6
- package/dist-esm/error/app.error.js +3 -3
- package/dist-esm/error/assert.js +17 -6
- package/dist-esm/error/error.util.js +5 -8
- package/dist-esm/error/httpRequestError.js +23 -0
- package/dist-esm/error/jsonParseError.js +7 -0
- package/dist-esm/error/try.js +8 -1
- package/dist-esm/http/fetcher.js +36 -28
- package/dist-esm/index.js +2 -1
- package/dist-esm/promise/pTimeout.js +2 -2
- package/dist-esm/string/json.util.js +17 -0
- package/dist-esm/string/stringifyAny.js +7 -7
- package/package.json +1 -1
- package/src/error/app.error.ts +6 -6
- package/src/error/assert.ts +22 -9
- package/src/error/error.model.ts +48 -23
- package/src/error/error.util.ts +15 -12
- package/src/error/httpRequestError.ts +25 -0
- package/src/error/jsonParseError.ts +21 -0
- package/src/error/try.ts +12 -1
- package/src/http/fetcher.ts +42 -32
- package/src/http/http.model.ts +5 -0
- package/src/index.ts +2 -1
- package/src/promise/pTimeout.ts +3 -3
- package/src/string/json.util.ts +20 -4
- package/src/string/stringifyAny.ts +7 -7
- package/dist/error/http.error.d.ts +0 -8
- package/dist/error/http.error.js +0 -13
- package/dist-esm/error/http.error.js +0 -9
- package/src/error/http.error.ts +0 -13
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* Based on: https://medium.com/@xpl/javascript-deriving-from-error-properly-8d2f8f315801
|
|
9
9
|
*/
|
|
10
10
|
export class AppError extends Error {
|
|
11
|
-
constructor(message, data = {},
|
|
11
|
+
constructor(message, data = {}, cause, name) {
|
|
12
12
|
super(message);
|
|
13
13
|
Object.defineProperty(this, 'name', {
|
|
14
14
|
value: name || this.constructor.name,
|
|
@@ -27,10 +27,10 @@ export class AppError extends Error {
|
|
|
27
27
|
configurable: true,
|
|
28
28
|
enumerable: false,
|
|
29
29
|
});
|
|
30
|
-
if (
|
|
30
|
+
if (cause) {
|
|
31
31
|
Object.defineProperty(this, 'cause', {
|
|
32
32
|
// I'd love to do _anyToError(opt.cause) here, but it causes circular dep ;(
|
|
33
|
-
value:
|
|
33
|
+
value: cause,
|
|
34
34
|
writable: true,
|
|
35
35
|
configurable: true,
|
|
36
36
|
enumerable: false,
|
package/dist-esm/error/assert.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _deepEquals, _stringifyAny } from '..';
|
|
1
|
+
import { _deepEquals, _isErrorObject, _stringifyAny } from '..';
|
|
2
2
|
import { AppError } from './app.error';
|
|
3
3
|
/**
|
|
4
4
|
* Evaluates the `condition` (casts it to Boolean).
|
|
@@ -57,9 +57,20 @@ export function _assertDeepEquals(actual, expected, message, errorData) {
|
|
|
57
57
|
throw new AssertionError(msg, Object.assign({ userFriendly: true }, errorData));
|
|
58
58
|
}
|
|
59
59
|
}
|
|
60
|
-
export function _assertIsError(err,
|
|
61
|
-
if (!(err instanceof
|
|
62
|
-
const msg = [
|
|
60
|
+
export function _assertIsError(err, errorClass = Error) {
|
|
61
|
+
if (!(err instanceof errorClass)) {
|
|
62
|
+
const msg = [
|
|
63
|
+
`expected to be instanceof ${errorClass.name}`,
|
|
64
|
+
`actual typeof: ${typeof err}`,
|
|
65
|
+
].join('\n');
|
|
66
|
+
throw new AssertionError(msg, {
|
|
67
|
+
userFriendly: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function _assertIsErrorObject(obj) {
|
|
72
|
+
if (!_isErrorObject(obj)) {
|
|
73
|
+
const msg = [`expected to be ErrorObject`, `actual typeof: ${typeof obj}`].join('\n');
|
|
63
74
|
throw new AssertionError(msg, {
|
|
64
75
|
userFriendly: true,
|
|
65
76
|
});
|
|
@@ -84,7 +95,7 @@ export function _assertTypeOf(v, expectedType, message) {
|
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
97
|
export class AssertionError extends AppError {
|
|
87
|
-
constructor(message, data = {},
|
|
88
|
-
super(message, data,
|
|
98
|
+
constructor(message, data = {}, cause) {
|
|
99
|
+
super(message, data, cause, 'AssertionError');
|
|
89
100
|
}
|
|
90
101
|
}
|
|
@@ -36,7 +36,7 @@ export function _anyToErrorObject(o, errorData) {
|
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
38
38
|
o = _jsonParseIfPossible(o);
|
|
39
|
-
if (
|
|
39
|
+
if (_isBackendErrorResponseObject(o)) {
|
|
40
40
|
eo = o.error;
|
|
41
41
|
}
|
|
42
42
|
else if (_isErrorObject(o)) {
|
|
@@ -107,15 +107,12 @@ export function _errorObjectToError(o, errorClass = Error) {
|
|
|
107
107
|
}
|
|
108
108
|
return err;
|
|
109
109
|
}
|
|
110
|
-
export function
|
|
111
|
-
return
|
|
110
|
+
export function _isBackendErrorResponseObject(o) {
|
|
111
|
+
return _isErrorObject(o === null || o === void 0 ? void 0 : o.error);
|
|
112
112
|
}
|
|
113
|
-
export function
|
|
113
|
+
export function _isHttpRequestErrorObject(o) {
|
|
114
114
|
var _a;
|
|
115
|
-
return
|
|
116
|
-
typeof o.name === 'string' &&
|
|
117
|
-
typeof o.message === 'string' &&
|
|
118
|
-
typeof ((_a = o.data) === null || _a === void 0 ? void 0 : _a.httpStatusCode) === 'number');
|
|
115
|
+
return !!o && o.name === 'HttpRequestError' && typeof ((_a = o.data) === null || _a === void 0 ? void 0 : _a.requestUrl) === 'string';
|
|
119
116
|
}
|
|
120
117
|
/**
|
|
121
118
|
* Note: any instance of AppError is also automatically an ErrorObject
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AppError } from './app.error';
|
|
2
|
+
/**
|
|
3
|
+
* Error that is thrown when Http Request was made and returned an error.
|
|
4
|
+
* Thrown by, for example, Fetcher.
|
|
5
|
+
*
|
|
6
|
+
* On the Frontend this Error class represents the error when calling the API,
|
|
7
|
+
* contains all the necessary request and response information.
|
|
8
|
+
*
|
|
9
|
+
* On the Backend, similarly, it represents the error when calling some 3rd-party API
|
|
10
|
+
* (backend-to-backend call).
|
|
11
|
+
* On the Backend it often propagates all the way to the Backend error handler,
|
|
12
|
+
* where it would be wrapped in BackendErrorResponseObject.
|
|
13
|
+
*
|
|
14
|
+
* Please note that `ErrorData.backendResponseStatusCode` is NOT exactly the same as
|
|
15
|
+
* `HttpRequestErrorData.responseStatusCode`.
|
|
16
|
+
* E.g 3rd-party call may return 401, but our Backend will still wrap it into an 500 error
|
|
17
|
+
* (by default).
|
|
18
|
+
*/
|
|
19
|
+
export class HttpRequestError extends AppError {
|
|
20
|
+
constructor(message, data, cause) {
|
|
21
|
+
super(message, data, cause, 'HttpRequestError');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { _truncateMiddle } from '../string/string.util';
|
|
2
|
+
import { AppError } from './app.error';
|
|
3
|
+
export class JsonParseError extends AppError {
|
|
4
|
+
constructor(data, cause) {
|
|
5
|
+
super(['Failed to parse', (data === null || data === void 0 ? void 0 : data.text) && _truncateMiddle(data.text, 200)].filter(Boolean).join(': '), data, cause, 'JsonParseError');
|
|
6
|
+
}
|
|
7
|
+
}
|
package/dist-esm/error/try.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { _stringifyAny } from '../string/stringifyAny';
|
|
1
2
|
import { AppError } from './app.error';
|
|
2
3
|
/**
|
|
3
4
|
* Calls a function, returns a Tuple of [error, value].
|
|
@@ -50,7 +51,7 @@ export async function pTry(promise) {
|
|
|
50
51
|
*/
|
|
51
52
|
export class UnexpectedPassError extends AppError {
|
|
52
53
|
constructor() {
|
|
53
|
-
super('expected error was not thrown');
|
|
54
|
+
super('expected error was not thrown', {}, undefined, 'UnexpectedPassError');
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
57
|
/**
|
|
@@ -101,3 +102,9 @@ export async function pExpectedError(promise, errorClass) {
|
|
|
101
102
|
return err; // this is expected!
|
|
102
103
|
}
|
|
103
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Shortcut function to simplify error snapshot-matching in tests.
|
|
107
|
+
*/
|
|
108
|
+
export async function pExpectedErrorString(promise, errorClass) {
|
|
109
|
+
return _stringifyAny(await pExpectedError(promise, errorClass));
|
|
110
|
+
}
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import { __asyncValues } from "tslib";
|
|
3
3
|
import { isServerSide } from '../env';
|
|
4
4
|
import { _anyToError, _anyToErrorObject, _errorToErrorObject } from '../error/error.util';
|
|
5
|
-
import {
|
|
5
|
+
import { HttpRequestError } from '../error/httpRequestError';
|
|
6
6
|
import { _clamp } from '../number/number.util';
|
|
7
7
|
import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, } from '../object/object.util';
|
|
8
8
|
import { pDelay } from '../promise/pDelay';
|
|
9
|
-
import { _jsonParseIfPossible } from '../string/json.util';
|
|
9
|
+
import { _jsonParse, _jsonParseIfPossible } from '../string/json.util';
|
|
10
10
|
import { _since } from '../time/time.util';
|
|
11
11
|
import { HTTP_METHODS } from './http.model';
|
|
12
12
|
const defRetryOptions = {
|
|
@@ -156,7 +156,7 @@ export class Fetcher {
|
|
|
156
156
|
}
|
|
157
157
|
else {
|
|
158
158
|
// !res.ok
|
|
159
|
-
await this.onNotOkResponse(res, timeout);
|
|
159
|
+
await this.onNotOkResponse(res, started, timeout);
|
|
160
160
|
}
|
|
161
161
|
}
|
|
162
162
|
try {
|
|
@@ -190,15 +190,21 @@ export class Fetcher {
|
|
|
190
190
|
if (text) {
|
|
191
191
|
try {
|
|
192
192
|
res.body = text;
|
|
193
|
-
res.body =
|
|
193
|
+
res.body = _jsonParse(text, req.jsonReviver);
|
|
194
194
|
}
|
|
195
195
|
catch (err) {
|
|
196
|
-
|
|
197
|
-
res.err =
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
196
|
+
// Error while parsing json
|
|
197
|
+
// res.err = _anyToError(err, HttpRequestError, {
|
|
198
|
+
// requestUrl: res.req.url,
|
|
199
|
+
// requestBaseUrl: this.cfg.baseUrl,
|
|
200
|
+
// requestMethod: res.req.init.method,
|
|
201
|
+
// requestSignature: res.signature,
|
|
202
|
+
// requestDuration: Date.now() - started,
|
|
203
|
+
// responseStatusCode: res.fetchResponse.status,
|
|
204
|
+
// } satisfies HttpRequestErrorData)
|
|
205
|
+
res.err = _anyToError(err);
|
|
201
206
|
res.ok = false;
|
|
207
|
+
return await this.onNotOkResponse(res, started, timeout);
|
|
202
208
|
}
|
|
203
209
|
}
|
|
204
210
|
else {
|
|
@@ -247,33 +253,35 @@ export class Fetcher {
|
|
|
247
253
|
async callNativeFetch(url, init) {
|
|
248
254
|
return await globalThis.fetch(url, init);
|
|
249
255
|
}
|
|
250
|
-
async onNotOkResponse(res, timeout) {
|
|
256
|
+
async onNotOkResponse(res, started, timeout) {
|
|
251
257
|
var _a, _b;
|
|
252
258
|
clearTimeout(timeout);
|
|
253
|
-
let
|
|
254
|
-
if (res.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
else if (res.err) {
|
|
259
|
-
errObj = _errorToErrorObject(res.err);
|
|
259
|
+
let cause;
|
|
260
|
+
if (res.err) {
|
|
261
|
+
// This is only possible on JSON.parse error (or CORS error!)
|
|
262
|
+
// This check should go first, to avoid calling .text() twice (which will fail)
|
|
263
|
+
cause = _errorToErrorObject(res.err);
|
|
260
264
|
}
|
|
261
|
-
else {
|
|
262
|
-
|
|
265
|
+
else if (res.fetchResponse) {
|
|
266
|
+
const body = _jsonParseIfPossible(await res.fetchResponse.text());
|
|
267
|
+
if (body) {
|
|
268
|
+
cause = _anyToErrorObject(body);
|
|
269
|
+
}
|
|
263
270
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
originalMessage,
|
|
268
|
-
]
|
|
269
|
-
.filter(Boolean)
|
|
270
|
-
.join('\n');
|
|
271
|
-
res.err = new HttpError(errObj.message, _filterNullishValues(Object.assign(Object.assign({}, errObj.data), { originalMessage, httpStatusCode: ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0,
|
|
271
|
+
const message = [(_a = res.fetchResponse) === null || _a === void 0 ? void 0 : _a.status, res.signature].filter(Boolean).join(' ');
|
|
272
|
+
res.err = new HttpRequestError(message, _filterNullishValues({
|
|
273
|
+
responseStatusCode: ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0,
|
|
272
274
|
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
273
275
|
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
274
276
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
275
277
|
// method: req.method,
|
|
276
|
-
|
|
278
|
+
// tryCount: req.tryCount,
|
|
279
|
+
requestUrl: res.req.url,
|
|
280
|
+
requestBaseUrl: this.cfg.baseUrl || null,
|
|
281
|
+
requestMethod: res.req.init.method,
|
|
282
|
+
requestSignature: res.signature,
|
|
283
|
+
requestDuration: Date.now() - started,
|
|
284
|
+
}), cause);
|
|
277
285
|
await this.processRetry(res);
|
|
278
286
|
}
|
|
279
287
|
async processRetry(res) {
|
package/dist-esm/index.js
CHANGED
|
@@ -20,9 +20,10 @@ export * from './enum.util';
|
|
|
20
20
|
export * from './error/error.model';
|
|
21
21
|
export * from './error/error.util';
|
|
22
22
|
export * from './error/errorMode';
|
|
23
|
-
export * from './error/
|
|
23
|
+
export * from './error/httpRequestError';
|
|
24
24
|
export * from './error/try';
|
|
25
25
|
export * from './error/tryCatch';
|
|
26
|
+
export * from './error/jsonParseError';
|
|
26
27
|
export * from './json-schema/from-data/generateJsonSchemaFromData';
|
|
27
28
|
export * from './json-schema/jsonSchema.cnst';
|
|
28
29
|
export * from './json-schema/jsonSchema.model';
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { AppError } from '../error/app.error';
|
|
2
2
|
import { _errorDataAppend } from '../error/error.util';
|
|
3
3
|
export class TimeoutError extends AppError {
|
|
4
|
-
constructor(message, data = {},
|
|
5
|
-
super(message, data,
|
|
4
|
+
constructor(message, data = {}, cause) {
|
|
5
|
+
super(message, data, cause, 'TimeoutError');
|
|
6
6
|
}
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { JsonParseError } from '../error/jsonParseError';
|
|
1
2
|
// const possibleJsonStartTokens = ['{', '[', '"']
|
|
2
3
|
const DETECT_JSON = /^\s*[{["\-\d]/;
|
|
3
4
|
/**
|
|
@@ -14,3 +15,19 @@ export function _jsonParseIfPossible(obj, reviver) {
|
|
|
14
15
|
}
|
|
15
16
|
return obj;
|
|
16
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Same as JSON.parse, but throws JsonParseError:
|
|
20
|
+
*
|
|
21
|
+
* 1. It's message includes a piece of source text (truncated)
|
|
22
|
+
* 2. It's data.text contains full source text
|
|
23
|
+
*/
|
|
24
|
+
export function _jsonParse(s, reviver) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(s, reviver);
|
|
27
|
+
}
|
|
28
|
+
catch (_a) {
|
|
29
|
+
throw new JsonParseError({
|
|
30
|
+
text: s,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _isErrorObject,
|
|
1
|
+
import { _isErrorObject, _isBackendErrorResponseObject } from '../error/error.util';
|
|
2
2
|
import { _jsonParseIfPossible } from './json.util';
|
|
3
3
|
import { _safeJsonStringify } from './safeJsonStringify';
|
|
4
4
|
const supportsAggregateError = typeof globalThis.AggregateError === 'function';
|
|
@@ -52,7 +52,7 @@ export function _stringifyAny(obj, opt = {}) {
|
|
|
52
52
|
//
|
|
53
53
|
// HttpErrorResponse
|
|
54
54
|
//
|
|
55
|
-
if (
|
|
55
|
+
if (_isBackendErrorResponseObject(obj)) {
|
|
56
56
|
return _stringifyAny(obj.error, opt);
|
|
57
57
|
}
|
|
58
58
|
if (obj instanceof Error || _isErrorObject(obj)) {
|
|
@@ -65,11 +65,11 @@ export function _stringifyAny(obj, opt = {}) {
|
|
|
65
65
|
// if (obj?.name === 'Error') {
|
|
66
66
|
// s = obj.message
|
|
67
67
|
// }
|
|
68
|
-
if (_isErrorObject(obj) && _isHttpErrorObject(obj)) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
s
|
|
68
|
+
// if (_isErrorObject(obj) && _isHttpErrorObject(obj)) {
|
|
69
|
+
// // Printing (0) to avoid ambiguity
|
|
70
|
+
// s = `${obj.name}(${obj.data.httpStatusCode}): ${obj.message}`
|
|
71
|
+
// }
|
|
72
|
+
s = [obj.name, obj.message].filter(Boolean).join(': ');
|
|
73
73
|
if (typeof obj.code === 'string') {
|
|
74
74
|
// Error that has no `data`, but has `code` property
|
|
75
75
|
s += `\ncode: ${obj.code}`;
|
package/package.json
CHANGED
package/src/error/app.error.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ErrorData } from './error.model'
|
|
1
|
+
import type { ErrorData, ErrorObject } from './error.model'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Base class for all our (not system) errors.
|
|
@@ -13,11 +13,11 @@ export class AppError<DATA_TYPE extends ErrorData = ErrorData> extends Error {
|
|
|
13
13
|
data!: DATA_TYPE
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* cause here is normalized to be
|
|
16
|
+
* cause here is normalized to be an ErrorObject
|
|
17
17
|
*/
|
|
18
|
-
override cause?:
|
|
18
|
+
override cause?: ErrorObject
|
|
19
19
|
|
|
20
|
-
constructor(message: string, data = {} as DATA_TYPE,
|
|
20
|
+
constructor(message: string, data = {} as DATA_TYPE, cause?: ErrorObject, name?: string) {
|
|
21
21
|
super(message)
|
|
22
22
|
|
|
23
23
|
Object.defineProperty(this, 'name', {
|
|
@@ -40,10 +40,10 @@ export class AppError<DATA_TYPE extends ErrorData = ErrorData> extends Error {
|
|
|
40
40
|
enumerable: false,
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
-
if (
|
|
43
|
+
if (cause) {
|
|
44
44
|
Object.defineProperty(this, 'cause', {
|
|
45
45
|
// I'd love to do _anyToError(opt.cause) here, but it causes circular dep ;(
|
|
46
|
-
value:
|
|
46
|
+
value: cause,
|
|
47
47
|
writable: true,
|
|
48
48
|
configurable: true,
|
|
49
49
|
enumerable: false,
|
package/src/error/assert.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ErrorData } from '..'
|
|
2
|
-
import { _deepEquals, _stringifyAny } from '..'
|
|
1
|
+
import type { ErrorData, ErrorObject } from '..'
|
|
2
|
+
import { _deepEquals, _isErrorObject, _stringifyAny, Class } from '..'
|
|
3
3
|
import { AppError } from './app.error'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -88,12 +88,25 @@ export function _assertDeepEquals<T>(
|
|
|
88
88
|
|
|
89
89
|
export function _assertIsError<ERR extends Error = Error>(
|
|
90
90
|
err: any,
|
|
91
|
-
|
|
91
|
+
errorClass: Class<ERR> = Error as any,
|
|
92
92
|
): asserts err is ERR {
|
|
93
|
-
if (!(err instanceof
|
|
94
|
-
const msg = [
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
if (!(err instanceof errorClass)) {
|
|
94
|
+
const msg = [
|
|
95
|
+
`expected to be instanceof ${errorClass.name}`,
|
|
96
|
+
`actual typeof: ${typeof err}`,
|
|
97
|
+
].join('\n')
|
|
98
|
+
|
|
99
|
+
throw new AssertionError(msg, {
|
|
100
|
+
userFriendly: true,
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function _assertIsErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
|
|
106
|
+
obj: any,
|
|
107
|
+
): asserts obj is ErrorObject<DATA_TYPE> {
|
|
108
|
+
if (!_isErrorObject(obj)) {
|
|
109
|
+
const msg = [`expected to be ErrorObject`, `actual typeof: ${typeof obj}`].join('\n')
|
|
97
110
|
|
|
98
111
|
throw new AssertionError(msg, {
|
|
99
112
|
userFriendly: true,
|
|
@@ -124,7 +137,7 @@ export function _assertTypeOf<T>(v: any, expectedType: string, message?: string)
|
|
|
124
137
|
}
|
|
125
138
|
|
|
126
139
|
export class AssertionError extends AppError {
|
|
127
|
-
constructor(message: string, data = {},
|
|
128
|
-
super(message, data,
|
|
140
|
+
constructor(message: string, data = {}, cause?: ErrorObject) {
|
|
141
|
+
super(message, data, cause, 'AssertionError')
|
|
129
142
|
}
|
|
130
143
|
}
|
package/src/error/error.model.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { HttpMethod, HttpStatusCode } from '../http/http.model'
|
|
2
|
+
import { NumberOfMilliseconds } from '../types'
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
5
|
* Extendable payload object to transfer custom additional data with AppError.
|
|
3
6
|
*/
|
|
@@ -21,9 +24,17 @@ export interface ErrorData {
|
|
|
21
24
|
/**
|
|
22
25
|
* Set to true to force reporting this error (e.g to Sentry).
|
|
23
26
|
* Useful to be able to force-report e.g a 4xx error, which by default wouldn't be reported.
|
|
27
|
+
* Set to false to force not-reporting it.
|
|
24
28
|
*/
|
|
25
29
|
report?: boolean
|
|
26
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Set to true or false to control error reporting on the Client-side.
|
|
33
|
+
* It works as in indication/hint, not a guarantee,
|
|
34
|
+
* because Client-side still need to manually check and respect this property.
|
|
35
|
+
*/
|
|
36
|
+
reportClientSide?: boolean
|
|
37
|
+
|
|
27
38
|
/**
|
|
28
39
|
* If defined - used by SentrySharedService in backend-lib.
|
|
29
40
|
* Allows to report only X% of errors of this type.
|
|
@@ -36,7 +47,8 @@ export interface ErrorData {
|
|
|
36
47
|
* (e.g frontend-lib adds a method, url, etc for all the errors)
|
|
37
48
|
* `originalMessage` is used to preserve the original `error.message` as it came from the backend.
|
|
38
49
|
*/
|
|
39
|
-
originalMessage?: string
|
|
50
|
+
// originalMessage?: string
|
|
51
|
+
// use .cause.message instead
|
|
40
52
|
|
|
41
53
|
/**
|
|
42
54
|
* Can be used by error-reporting tools (e.g Sentry).
|
|
@@ -45,42 +57,51 @@ export interface ErrorData {
|
|
|
45
57
|
*/
|
|
46
58
|
fingerprint?: string[]
|
|
47
59
|
|
|
48
|
-
httpStatusCode?: number
|
|
49
|
-
|
|
50
60
|
/**
|
|
51
|
-
*
|
|
61
|
+
* Set when throwing an error from your backend code, to indicate desired http status code.
|
|
62
|
+
* e.g throw new AppError('oj', { backendResponseStatusCode: 401 })
|
|
52
63
|
*/
|
|
53
|
-
|
|
54
|
-
}
|
|
64
|
+
backendResponseStatusCode?: HttpStatusCode
|
|
55
65
|
|
|
56
|
-
export interface HttpErrorData extends ErrorData {
|
|
57
66
|
/**
|
|
58
|
-
*
|
|
67
|
+
* Set to true when the error was thrown after response headers were sent.
|
|
59
68
|
*/
|
|
60
|
-
|
|
69
|
+
headersSent?: boolean
|
|
61
70
|
|
|
62
71
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
* GET /api/some-endpoint
|
|
72
|
+
* Used in e.g http 401 error.
|
|
66
73
|
*/
|
|
67
|
-
|
|
74
|
+
adminAuthRequired?: boolean
|
|
68
75
|
|
|
69
76
|
/**
|
|
70
|
-
*
|
|
77
|
+
* Used in e.g http 403 error.
|
|
71
78
|
*/
|
|
72
|
-
|
|
73
|
-
}
|
|
79
|
+
adminPermissionsRequired?: string[]
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Open-ended.
|
|
83
|
+
*/
|
|
84
|
+
[k: string]: any
|
|
77
85
|
}
|
|
78
86
|
|
|
79
|
-
export interface
|
|
87
|
+
export interface HttpRequestErrorData extends ErrorData {
|
|
88
|
+
requestUrl: string
|
|
89
|
+
requestBaseUrl?: string
|
|
90
|
+
requestMethod: HttpMethod
|
|
80
91
|
/**
|
|
81
|
-
*
|
|
92
|
+
* Conveniently combines Method and Url, respects `logWithSearchParams` (and similar) configuration.
|
|
93
|
+
* E.g:
|
|
94
|
+
* GET /some/url
|
|
82
95
|
*/
|
|
83
|
-
|
|
96
|
+
requestSignature: string
|
|
97
|
+
/**
|
|
98
|
+
* Can be set to 0 if request "failed to start" or "failed to reach the server".
|
|
99
|
+
*/
|
|
100
|
+
requestDuration: NumberOfMilliseconds
|
|
101
|
+
/**
|
|
102
|
+
* 0 is used for edge cases when e.g it failed to reach the server.
|
|
103
|
+
*/
|
|
104
|
+
responseStatusCode: HttpStatusCode
|
|
84
105
|
}
|
|
85
106
|
|
|
86
107
|
// export interface ErrorLike {
|
|
@@ -101,6 +122,8 @@ export interface Admin403ErrorData extends HttpErrorData {
|
|
|
101
122
|
* Cannot contain any properties that stock Error doesn't have, otherwise
|
|
102
123
|
* it will be hard to transform it to ErrorObject.
|
|
103
124
|
* Everything "extra" should go under `data`.
|
|
125
|
+
*
|
|
126
|
+
* Instance of AppError class should satisfy ErrorObject interface.
|
|
104
127
|
*/
|
|
105
128
|
export interface ErrorObject<DATA_TYPE extends ErrorData = ErrorData> {
|
|
106
129
|
/**
|
|
@@ -134,8 +157,10 @@ export interface ErrorObject<DATA_TYPE extends ErrorData = ErrorData> {
|
|
|
134
157
|
}
|
|
135
158
|
|
|
136
159
|
/**
|
|
137
|
-
* JSON HTTP response from the Backend that represents "Error".
|
|
160
|
+
* JSON HTTP response object from the Backend that represents "Error".
|
|
161
|
+
* Assumption is that if JSON response "looks like this" - it's safe to print it in a custom way.
|
|
162
|
+
* Otherwise - it'll be printed in a generic way.
|
|
138
163
|
*/
|
|
139
|
-
export interface
|
|
164
|
+
export interface BackendErrorResponseObject<DATA_TYPE extends ErrorData = ErrorData> {
|
|
140
165
|
error: ErrorObject<DATA_TYPE>
|
|
141
166
|
}
|
package/src/error/error.util.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ErrorData,
|
|
3
|
+
ErrorObject,
|
|
4
|
+
BackendErrorResponseObject,
|
|
5
|
+
Class,
|
|
6
|
+
HttpRequestErrorData,
|
|
7
|
+
} from '..'
|
|
2
8
|
import { AppError, _jsonParseIfPossible, _stringifyAny } from '..'
|
|
3
9
|
|
|
4
10
|
/**
|
|
@@ -52,7 +58,7 @@ export function _anyToErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
|
|
|
52
58
|
} else {
|
|
53
59
|
o = _jsonParseIfPossible(o)
|
|
54
60
|
|
|
55
|
-
if (
|
|
61
|
+
if (_isBackendErrorResponseObject(o)) {
|
|
56
62
|
eo = o.error as ErrorObject<DATA_TYPE>
|
|
57
63
|
} else if (_isErrorObject(o)) {
|
|
58
64
|
eo = o as ErrorObject<DATA_TYPE>
|
|
@@ -139,23 +145,20 @@ export function _errorObjectToError<DATA_TYPE extends ErrorData, ERROR_TYPE exte
|
|
|
139
145
|
return err
|
|
140
146
|
}
|
|
141
147
|
|
|
142
|
-
export function
|
|
143
|
-
return
|
|
148
|
+
export function _isBackendErrorResponseObject(o: any): o is BackendErrorResponseObject {
|
|
149
|
+
return _isErrorObject(o?.error)
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
export function
|
|
147
|
-
return
|
|
148
|
-
!!o &&
|
|
149
|
-
typeof o.name === 'string' &&
|
|
150
|
-
typeof o.message === 'string' &&
|
|
151
|
-
typeof o.data?.httpStatusCode === 'number'
|
|
152
|
-
)
|
|
152
|
+
export function _isHttpRequestErrorObject(o: any): o is ErrorObject<HttpRequestErrorData> {
|
|
153
|
+
return !!o && o.name === 'HttpRequestError' && typeof o.data?.requestUrl === 'string'
|
|
153
154
|
}
|
|
154
155
|
|
|
155
156
|
/**
|
|
156
157
|
* Note: any instance of AppError is also automatically an ErrorObject
|
|
157
158
|
*/
|
|
158
|
-
export function _isErrorObject
|
|
159
|
+
export function _isErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
|
|
160
|
+
o: any,
|
|
161
|
+
): o is ErrorObject<DATA_TYPE> {
|
|
159
162
|
return (
|
|
160
163
|
!!o &&
|
|
161
164
|
typeof o === 'object' &&
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { AppError } from './app.error'
|
|
2
|
+
import type { ErrorObject, HttpRequestErrorData } from './error.model'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Error that is thrown when Http Request was made and returned an error.
|
|
6
|
+
* Thrown by, for example, Fetcher.
|
|
7
|
+
*
|
|
8
|
+
* On the Frontend this Error class represents the error when calling the API,
|
|
9
|
+
* contains all the necessary request and response information.
|
|
10
|
+
*
|
|
11
|
+
* On the Backend, similarly, it represents the error when calling some 3rd-party API
|
|
12
|
+
* (backend-to-backend call).
|
|
13
|
+
* On the Backend it often propagates all the way to the Backend error handler,
|
|
14
|
+
* where it would be wrapped in BackendErrorResponseObject.
|
|
15
|
+
*
|
|
16
|
+
* Please note that `ErrorData.backendResponseStatusCode` is NOT exactly the same as
|
|
17
|
+
* `HttpRequestErrorData.responseStatusCode`.
|
|
18
|
+
* E.g 3rd-party call may return 401, but our Backend will still wrap it into an 500 error
|
|
19
|
+
* (by default).
|
|
20
|
+
*/
|
|
21
|
+
export class HttpRequestError extends AppError<HttpRequestErrorData> {
|
|
22
|
+
constructor(message: string, data: HttpRequestErrorData, cause?: ErrorObject) {
|
|
23
|
+
super(message, data, cause, 'HttpRequestError')
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { _truncateMiddle } from '../string/string.util'
|
|
2
|
+
import { AppError } from './app.error'
|
|
3
|
+
import { ErrorData, ErrorObject } from './error.model'
|
|
4
|
+
|
|
5
|
+
export interface JsonParseErrorData extends ErrorData {
|
|
6
|
+
/**
|
|
7
|
+
* Original text that failed to get parsed.
|
|
8
|
+
*/
|
|
9
|
+
text?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class JsonParseError extends AppError<JsonParseErrorData> {
|
|
13
|
+
constructor(data: JsonParseErrorData, cause?: ErrorObject) {
|
|
14
|
+
super(
|
|
15
|
+
['Failed to parse', data?.text && _truncateMiddle(data.text, 200)].filter(Boolean).join(': '),
|
|
16
|
+
data,
|
|
17
|
+
cause,
|
|
18
|
+
'JsonParseError',
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
}
|