@naturalcycles/js-lib 14.174.1 → 14.176.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/assert.js +4 -13
- package/dist/error/error.util.d.ts +1 -1
- package/dist/error/error.util.js +2 -2
- package/dist/error/try.js +4 -8
- package/dist/http/fetcher.d.ts +7 -0
- package/dist/http/fetcher.js +32 -4
- package/dist/http/fetcher.model.d.ts +10 -0
- package/dist-esm/error/assert.js +4 -13
- package/dist-esm/error/error.util.js +2 -2
- package/dist-esm/error/try.js +4 -8
- package/dist-esm/http/fetcher.js +35 -7
- package/package.json +1 -1
- package/src/error/assert.ts +9 -18
- package/src/error/error.util.ts +2 -2
- package/src/error/try.ts +6 -6
- package/src/http/fetcher.model.ts +12 -0
- package/src/http/fetcher.ts +42 -5
package/dist/error/assert.js
CHANGED
|
@@ -20,7 +20,7 @@ const __1 = require("..");
|
|
|
20
20
|
function _assert(condition, // will be evaluated as Boolean
|
|
21
21
|
message, errorData) {
|
|
22
22
|
if (!condition) {
|
|
23
|
-
throw new __1.AssertionError(message || '
|
|
23
|
+
throw new __1.AssertionError(message || 'condition failed', {
|
|
24
24
|
userFriendly: true,
|
|
25
25
|
...errorData,
|
|
26
26
|
});
|
|
@@ -71,11 +71,7 @@ function _assertDeepEquals(actual, expected, message, errorData) {
|
|
|
71
71
|
exports._assertDeepEquals = _assertDeepEquals;
|
|
72
72
|
function _assertIsError(err, errorClass = Error) {
|
|
73
73
|
if (!(err instanceof errorClass)) {
|
|
74
|
-
|
|
75
|
-
`expected to be instanceof ${errorClass.name}`,
|
|
76
|
-
`actual typeof: ${typeof err}`,
|
|
77
|
-
].join('\n');
|
|
78
|
-
throw new __1.AssertionError(msg, {
|
|
74
|
+
throw new __1.AssertionError(`Expected to be instanceof ${errorClass.name}, actual typeof: ${typeof err}`, {
|
|
79
75
|
userFriendly: true,
|
|
80
76
|
});
|
|
81
77
|
}
|
|
@@ -95,8 +91,7 @@ function _assertErrorClassOrRethrow(err, errorClass) {
|
|
|
95
91
|
exports._assertErrorClassOrRethrow = _assertErrorClassOrRethrow;
|
|
96
92
|
function _assertIsErrorObject(obj) {
|
|
97
93
|
if (!(0, __1._isErrorObject)(obj)) {
|
|
98
|
-
|
|
99
|
-
throw new __1.AssertionError(msg, {
|
|
94
|
+
throw new __1.AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`, {
|
|
100
95
|
userFriendly: true,
|
|
101
96
|
});
|
|
102
97
|
}
|
|
@@ -112,11 +107,7 @@ function _assertIsNumber(v, message) {
|
|
|
112
107
|
exports._assertIsNumber = _assertIsNumber;
|
|
113
108
|
function _assertTypeOf(v, expectedType, message) {
|
|
114
109
|
if (typeof v !== expectedType) {
|
|
115
|
-
const msg =
|
|
116
|
-
message || `unexpected type`,
|
|
117
|
-
`expected: ${expectedType}`,
|
|
118
|
-
`got : ${typeof v}`,
|
|
119
|
-
].join('\n');
|
|
110
|
+
const msg = message || `Expected typeof ${expectedType}, actual typeof: ${typeof v}`;
|
|
120
111
|
throw new __1.AssertionError(msg, {
|
|
121
112
|
userFriendly: true,
|
|
122
113
|
});
|
package/dist/error/error.util.js
CHANGED
|
@@ -342,8 +342,8 @@ exports.TimeoutError = TimeoutError;
|
|
|
342
342
|
* "Pass" means "no error".
|
|
343
343
|
*/
|
|
344
344
|
class UnexpectedPassError extends AppError {
|
|
345
|
-
constructor() {
|
|
346
|
-
super('expected error was not thrown', {}, {
|
|
345
|
+
constructor(message) {
|
|
346
|
+
super(message || 'expected error was not thrown', {}, {
|
|
347
347
|
name: 'UnexpectedPassError',
|
|
348
348
|
});
|
|
349
349
|
}
|
package/dist/error/try.js
CHANGED
|
@@ -61,18 +61,16 @@ exports.pTry = pTry;
|
|
|
61
61
|
function _expectedError(fn, errorClass) {
|
|
62
62
|
try {
|
|
63
63
|
fn();
|
|
64
|
-
// Unexpected!
|
|
65
|
-
throw new error_util_1.UnexpectedPassError();
|
|
66
64
|
}
|
|
67
65
|
catch (err) {
|
|
68
|
-
if (err instanceof error_util_1.UnexpectedPassError)
|
|
69
|
-
throw err; // re-throw
|
|
70
66
|
if (errorClass && !(err instanceof errorClass)) {
|
|
71
67
|
console.warn(`_expectedError expected ${errorClass.constructor.name} but got different error class`);
|
|
72
68
|
throw err;
|
|
73
69
|
}
|
|
74
70
|
return err; // this is expected!
|
|
75
71
|
}
|
|
72
|
+
// Unexpected!
|
|
73
|
+
throw new error_util_1.UnexpectedPassError();
|
|
76
74
|
}
|
|
77
75
|
exports._expectedError = _expectedError;
|
|
78
76
|
/**
|
|
@@ -86,18 +84,16 @@ exports._expectedError = _expectedError;
|
|
|
86
84
|
async function pExpectedError(promise, errorClass) {
|
|
87
85
|
try {
|
|
88
86
|
await promise;
|
|
89
|
-
// Unexpected!
|
|
90
|
-
throw new error_util_1.UnexpectedPassError();
|
|
91
87
|
}
|
|
92
88
|
catch (err) {
|
|
93
|
-
if (err instanceof error_util_1.UnexpectedPassError)
|
|
94
|
-
throw err; // re-throw
|
|
95
89
|
if (errorClass && !(err instanceof errorClass)) {
|
|
96
90
|
console.warn(`pExpectedError expected ${errorClass.constructor.name} but got different error class`);
|
|
97
91
|
throw err;
|
|
98
92
|
}
|
|
99
93
|
return err; // this is expected!
|
|
100
94
|
}
|
|
95
|
+
// Unexpected!
|
|
96
|
+
throw new error_util_1.UnexpectedPassError();
|
|
101
97
|
}
|
|
102
98
|
exports.pExpectedError = pExpectedError;
|
|
103
99
|
/**
|
package/dist/http/fetcher.d.ts
CHANGED
|
@@ -50,6 +50,13 @@ export declare class Fetcher {
|
|
|
50
50
|
*/
|
|
51
51
|
getReadableStream(url: string, opt?: FetcherOptions): Promise<ReadableStream<Uint8Array>>;
|
|
52
52
|
fetch<T = unknown>(opt: FetcherOptions): Promise<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Execute fetch and expect/assert it to return an Error (which will be wrapped in
|
|
55
|
+
* HttpRequestError as it normally would).
|
|
56
|
+
* If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
|
|
57
|
+
* Useful in unit testing.
|
|
58
|
+
*/
|
|
59
|
+
expectError(opt: FetcherOptions): Promise<HttpRequestError>;
|
|
53
60
|
/**
|
|
54
61
|
* Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
|
|
55
62
|
* err, if defined, is strictly HttpRequestError.
|
package/dist/http/fetcher.js
CHANGED
|
@@ -121,6 +121,20 @@ class Fetcher {
|
|
|
121
121
|
}
|
|
122
122
|
return res.body;
|
|
123
123
|
}
|
|
124
|
+
/**
|
|
125
|
+
* Execute fetch and expect/assert it to return an Error (which will be wrapped in
|
|
126
|
+
* HttpRequestError as it normally would).
|
|
127
|
+
* If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
|
|
128
|
+
* Useful in unit testing.
|
|
129
|
+
*/
|
|
130
|
+
async expectError(opt) {
|
|
131
|
+
const res = await this.doFetch(opt);
|
|
132
|
+
if (!res.err) {
|
|
133
|
+
throw new error_util_1.UnexpectedPassError('Fetch was expected to error');
|
|
134
|
+
}
|
|
135
|
+
(0, assert_1._assertIsError)(res.err, error_util_1.HttpRequestError);
|
|
136
|
+
return res.err;
|
|
137
|
+
}
|
|
124
138
|
/**
|
|
125
139
|
* Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
|
|
126
140
|
* err, if defined, is strictly HttpRequestError.
|
|
@@ -168,6 +182,9 @@ class Fetcher {
|
|
|
168
182
|
// setup timeout
|
|
169
183
|
let timeoutId;
|
|
170
184
|
if (timeoutSeconds) {
|
|
185
|
+
// Used for Request timeout (when timeoutSeconds is set),
|
|
186
|
+
// but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
|
|
187
|
+
// UPD: no, not using for "downloadBody" currently
|
|
171
188
|
const abortController = new AbortController();
|
|
172
189
|
req.init.signal = abortController.signal;
|
|
173
190
|
timeoutId = setTimeout(() => {
|
|
@@ -187,7 +204,8 @@ class Fetcher {
|
|
|
187
204
|
}
|
|
188
205
|
}
|
|
189
206
|
try {
|
|
190
|
-
|
|
207
|
+
// Calls cfg.fetchFn if set, otherwise Fetcher.callNativeFetch
|
|
208
|
+
res.fetchResponse = await (this.cfg.fetchFn || Fetcher.callNativeFetch)(req.fullUrl, req.init);
|
|
191
209
|
res.ok = res.fetchResponse.ok;
|
|
192
210
|
// important to set it to undefined, otherwise it can keep the previous value (from previous try)
|
|
193
211
|
res.err = undefined;
|
|
@@ -211,10 +229,13 @@ class Fetcher {
|
|
|
211
229
|
// We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
|
|
212
230
|
await (0, pTimeout_1.pTimeout)(async () => await this.onOkResponse(res), {
|
|
213
231
|
timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
|
|
214
|
-
name: 'Fetcher.
|
|
232
|
+
name: 'Fetcher.downloadBody',
|
|
215
233
|
});
|
|
216
234
|
}
|
|
217
235
|
catch (err) {
|
|
236
|
+
// Important to cancel the original request to not keep it running (and occupying resources)
|
|
237
|
+
// UPD: no, we probably don't need to, because "request" has already completed, it's just the "body" is pending
|
|
238
|
+
// if (err instanceof TimeoutError) {}
|
|
218
239
|
// onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
|
|
219
240
|
res.err = (0, error_util_1._anyToError)(err);
|
|
220
241
|
res.ok = false;
|
|
@@ -315,10 +336,17 @@ class Fetcher {
|
|
|
315
336
|
message: 'Fetch failed',
|
|
316
337
|
data: {},
|
|
317
338
|
};
|
|
318
|
-
|
|
339
|
+
let responseStatusCode = res.fetchResponse?.status || 0;
|
|
340
|
+
if (res.statusFamily === 2) {
|
|
341
|
+
// important to reset httpStatusCode to 0 in this case, as status 2xx can be misleading
|
|
342
|
+
res.statusFamily = undefined;
|
|
343
|
+
res.statusCode = undefined;
|
|
344
|
+
responseStatusCode = 0;
|
|
345
|
+
}
|
|
346
|
+
const message = [res.statusCode, res.signature].filter(Boolean).join(' ');
|
|
319
347
|
res.err = new error_util_1.HttpRequestError(message, (0, object_util_1._filterNullishValues)({
|
|
320
348
|
response: res.fetchResponse,
|
|
321
|
-
responseStatusCode
|
|
349
|
+
responseStatusCode,
|
|
322
350
|
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
323
351
|
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
324
352
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
|
@@ -178,6 +178,10 @@ export interface FetcherOptions {
|
|
|
178
178
|
* If true - enables all possible logging.
|
|
179
179
|
*/
|
|
180
180
|
debug?: boolean;
|
|
181
|
+
/**
|
|
182
|
+
* If provided - will be used instead of static `Fetcher.callNativeFetch`.
|
|
183
|
+
*/
|
|
184
|
+
fetchFn?: FetchFunction;
|
|
181
185
|
}
|
|
182
186
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
183
187
|
method: HttpMethod;
|
|
@@ -207,3 +211,9 @@ export interface FetcherErrorResponse<BODY = unknown> {
|
|
|
207
211
|
}
|
|
208
212
|
export type FetcherResponse<BODY = unknown> = FetcherSuccessResponse<BODY> | FetcherErrorResponse<BODY>;
|
|
209
213
|
export type FetcherResponseType = 'json' | 'text' | 'void' | 'arrayBuffer' | 'blob' | 'readableStream';
|
|
214
|
+
/**
|
|
215
|
+
* Signature for the `fetch` function.
|
|
216
|
+
* Used to be able to override and provide a different implementation,
|
|
217
|
+
* e.g when mocking.
|
|
218
|
+
*/
|
|
219
|
+
export type FetchFunction = (url: string, init: RequestInitNormalized) => Promise<Response>;
|
package/dist-esm/error/assert.js
CHANGED
|
@@ -17,7 +17,7 @@ import { _deepEquals, _isErrorObject, _stringifyAny, AssertionError } from '..';
|
|
|
17
17
|
export function _assert(condition, // will be evaluated as Boolean
|
|
18
18
|
message, errorData) {
|
|
19
19
|
if (!condition) {
|
|
20
|
-
throw new AssertionError(message || '
|
|
20
|
+
throw new AssertionError(message || 'condition failed', Object.assign({ userFriendly: true }, errorData));
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
@@ -56,11 +56,7 @@ export function _assertDeepEquals(actual, expected, message, errorData) {
|
|
|
56
56
|
}
|
|
57
57
|
export function _assertIsError(err, errorClass = Error) {
|
|
58
58
|
if (!(err instanceof errorClass)) {
|
|
59
|
-
|
|
60
|
-
`expected to be instanceof ${errorClass.name}`,
|
|
61
|
-
`actual typeof: ${typeof err}`,
|
|
62
|
-
].join('\n');
|
|
63
|
-
throw new AssertionError(msg, {
|
|
59
|
+
throw new AssertionError(`Expected to be instanceof ${errorClass.name}, actual typeof: ${typeof err}`, {
|
|
64
60
|
userFriendly: true,
|
|
65
61
|
});
|
|
66
62
|
}
|
|
@@ -78,8 +74,7 @@ export function _assertErrorClassOrRethrow(err, errorClass) {
|
|
|
78
74
|
}
|
|
79
75
|
export function _assertIsErrorObject(obj) {
|
|
80
76
|
if (!_isErrorObject(obj)) {
|
|
81
|
-
|
|
82
|
-
throw new AssertionError(msg, {
|
|
77
|
+
throw new AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`, {
|
|
83
78
|
userFriendly: true,
|
|
84
79
|
});
|
|
85
80
|
}
|
|
@@ -92,11 +87,7 @@ export function _assertIsNumber(v, message) {
|
|
|
92
87
|
}
|
|
93
88
|
export function _assertTypeOf(v, expectedType, message) {
|
|
94
89
|
if (typeof v !== expectedType) {
|
|
95
|
-
const msg =
|
|
96
|
-
message || `unexpected type`,
|
|
97
|
-
`expected: ${expectedType}`,
|
|
98
|
-
`got : ${typeof v}`,
|
|
99
|
-
].join('\n');
|
|
90
|
+
const msg = message || `Expected typeof ${expectedType}, actual typeof: ${typeof v}`;
|
|
100
91
|
throw new AssertionError(msg, {
|
|
101
92
|
userFriendly: true,
|
|
102
93
|
});
|
|
@@ -319,8 +319,8 @@ export class TimeoutError extends AppError {
|
|
|
319
319
|
* "Pass" means "no error".
|
|
320
320
|
*/
|
|
321
321
|
export class UnexpectedPassError extends AppError {
|
|
322
|
-
constructor() {
|
|
323
|
-
super('expected error was not thrown', {}, {
|
|
322
|
+
constructor(message) {
|
|
323
|
+
super(message || 'expected error was not thrown', {}, {
|
|
324
324
|
name: 'UnexpectedPassError',
|
|
325
325
|
});
|
|
326
326
|
}
|
package/dist-esm/error/try.js
CHANGED
|
@@ -56,18 +56,16 @@ export async function pTry(promise, errorClass) {
|
|
|
56
56
|
export function _expectedError(fn, errorClass) {
|
|
57
57
|
try {
|
|
58
58
|
fn();
|
|
59
|
-
// Unexpected!
|
|
60
|
-
throw new UnexpectedPassError();
|
|
61
59
|
}
|
|
62
60
|
catch (err) {
|
|
63
|
-
if (err instanceof UnexpectedPassError)
|
|
64
|
-
throw err; // re-throw
|
|
65
61
|
if (errorClass && !(err instanceof errorClass)) {
|
|
66
62
|
console.warn(`_expectedError expected ${errorClass.constructor.name} but got different error class`);
|
|
67
63
|
throw err;
|
|
68
64
|
}
|
|
69
65
|
return err; // this is expected!
|
|
70
66
|
}
|
|
67
|
+
// Unexpected!
|
|
68
|
+
throw new UnexpectedPassError();
|
|
71
69
|
}
|
|
72
70
|
/**
|
|
73
71
|
* Awaits passed `promise`, expects is to throw (reject), catches the expected error and returns.
|
|
@@ -80,18 +78,16 @@ export function _expectedError(fn, errorClass) {
|
|
|
80
78
|
export async function pExpectedError(promise, errorClass) {
|
|
81
79
|
try {
|
|
82
80
|
await promise;
|
|
83
|
-
// Unexpected!
|
|
84
|
-
throw new UnexpectedPassError();
|
|
85
81
|
}
|
|
86
82
|
catch (err) {
|
|
87
|
-
if (err instanceof UnexpectedPassError)
|
|
88
|
-
throw err; // re-throw
|
|
89
83
|
if (errorClass && !(err instanceof errorClass)) {
|
|
90
84
|
console.warn(`pExpectedError expected ${errorClass.constructor.name} but got different error class`);
|
|
91
85
|
throw err;
|
|
92
86
|
}
|
|
93
87
|
return err; // this is expected!
|
|
94
88
|
}
|
|
89
|
+
// Unexpected!
|
|
90
|
+
throw new UnexpectedPassError();
|
|
95
91
|
}
|
|
96
92
|
/**
|
|
97
93
|
* Shortcut function to simplify error snapshot-matching in tests.
|
package/dist-esm/http/fetcher.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
/// <reference lib="dom.iterable"/>
|
|
4
4
|
var _a;
|
|
5
5
|
import { isServerSide } from '../env';
|
|
6
|
-
import { _assertErrorClassOrRethrow } from '../error/assert';
|
|
7
|
-
import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject, HttpRequestError, TimeoutError, } from '../error/error.util';
|
|
6
|
+
import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert';
|
|
7
|
+
import { _anyToError, _anyToErrorObject, _errorLikeToErrorObject, HttpRequestError, TimeoutError, UnexpectedPassError, } from '../error/error.util';
|
|
8
8
|
import { _clamp } from '../number/number.util';
|
|
9
9
|
import { _filterNullishValues, _filterUndefinedValues, _mapKeys, _merge, _omit, _pick, } from '../object/object.util';
|
|
10
10
|
import { pDelay } from '../promise/pDelay';
|
|
@@ -98,6 +98,20 @@ export class Fetcher {
|
|
|
98
98
|
}
|
|
99
99
|
return res.body;
|
|
100
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Execute fetch and expect/assert it to return an Error (which will be wrapped in
|
|
103
|
+
* HttpRequestError as it normally would).
|
|
104
|
+
* If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
|
|
105
|
+
* Useful in unit testing.
|
|
106
|
+
*/
|
|
107
|
+
async expectError(opt) {
|
|
108
|
+
const res = await this.doFetch(opt);
|
|
109
|
+
if (!res.err) {
|
|
110
|
+
throw new UnexpectedPassError('Fetch was expected to error');
|
|
111
|
+
}
|
|
112
|
+
_assertIsError(res.err, HttpRequestError);
|
|
113
|
+
return res.err;
|
|
114
|
+
}
|
|
101
115
|
/**
|
|
102
116
|
* Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
|
|
103
117
|
* err, if defined, is strictly HttpRequestError.
|
|
@@ -146,6 +160,9 @@ export class Fetcher {
|
|
|
146
160
|
// setup timeout
|
|
147
161
|
let timeoutId;
|
|
148
162
|
if (timeoutSeconds) {
|
|
163
|
+
// Used for Request timeout (when timeoutSeconds is set),
|
|
164
|
+
// but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
|
|
165
|
+
// UPD: no, not using for "downloadBody" currently
|
|
149
166
|
const abortController = new AbortController();
|
|
150
167
|
req.init.signal = abortController.signal;
|
|
151
168
|
timeoutId = setTimeout(() => {
|
|
@@ -165,7 +182,8 @@ export class Fetcher {
|
|
|
165
182
|
}
|
|
166
183
|
}
|
|
167
184
|
try {
|
|
168
|
-
|
|
185
|
+
// Calls cfg.fetchFn if set, otherwise Fetcher.callNativeFetch
|
|
186
|
+
res.fetchResponse = await (this.cfg.fetchFn || Fetcher.callNativeFetch)(req.fullUrl, req.init);
|
|
169
187
|
res.ok = res.fetchResponse.ok;
|
|
170
188
|
// important to set it to undefined, otherwise it can keep the previous value (from previous try)
|
|
171
189
|
res.err = undefined;
|
|
@@ -189,10 +207,13 @@ export class Fetcher {
|
|
|
189
207
|
// We are applying a separate Timeout (as long as original Timeout for now) to "download and parse the body"
|
|
190
208
|
await pTimeout(async () => await this.onOkResponse(res), {
|
|
191
209
|
timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
|
|
192
|
-
name: 'Fetcher.
|
|
210
|
+
name: 'Fetcher.downloadBody',
|
|
193
211
|
});
|
|
194
212
|
}
|
|
195
213
|
catch (err) {
|
|
214
|
+
// Important to cancel the original request to not keep it running (and occupying resources)
|
|
215
|
+
// UPD: no, we probably don't need to, because "request" has already completed, it's just the "body" is pending
|
|
216
|
+
// if (err instanceof TimeoutError) {}
|
|
196
217
|
// onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
|
|
197
218
|
res.err = _anyToError(err);
|
|
198
219
|
res.ok = false;
|
|
@@ -275,7 +296,7 @@ export class Fetcher {
|
|
|
275
296
|
return await globalThis.fetch(url, init);
|
|
276
297
|
}
|
|
277
298
|
async onNotOkResponse(res) {
|
|
278
|
-
var _b
|
|
299
|
+
var _b;
|
|
279
300
|
let cause;
|
|
280
301
|
if (res.err) {
|
|
281
302
|
// This is only possible on JSON.parse error, or CORS error,
|
|
@@ -294,10 +315,17 @@ export class Fetcher {
|
|
|
294
315
|
message: 'Fetch failed',
|
|
295
316
|
data: {},
|
|
296
317
|
});
|
|
297
|
-
|
|
318
|
+
let responseStatusCode = ((_b = res.fetchResponse) === null || _b === void 0 ? void 0 : _b.status) || 0;
|
|
319
|
+
if (res.statusFamily === 2) {
|
|
320
|
+
// important to reset httpStatusCode to 0 in this case, as status 2xx can be misleading
|
|
321
|
+
res.statusFamily = undefined;
|
|
322
|
+
res.statusCode = undefined;
|
|
323
|
+
responseStatusCode = 0;
|
|
324
|
+
}
|
|
325
|
+
const message = [res.statusCode, res.signature].filter(Boolean).join(' ');
|
|
298
326
|
res.err = new HttpRequestError(message, _filterNullishValues({
|
|
299
327
|
response: res.fetchResponse,
|
|
300
|
-
responseStatusCode
|
|
328
|
+
responseStatusCode,
|
|
301
329
|
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
302
330
|
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
303
331
|
// Enabled, cause `data` is not printed by default when error is HttpError
|
package/package.json
CHANGED
package/src/error/assert.ts
CHANGED
|
@@ -22,7 +22,7 @@ export function _assert(
|
|
|
22
22
|
errorData?: ErrorData,
|
|
23
23
|
): asserts condition {
|
|
24
24
|
if (!condition) {
|
|
25
|
-
throw new AssertionError(message || '
|
|
25
|
+
throw new AssertionError(message || 'condition failed', {
|
|
26
26
|
userFriendly: true,
|
|
27
27
|
...errorData,
|
|
28
28
|
})
|
|
@@ -90,14 +90,12 @@ export function _assertIsError<ERR extends Error = Error>(
|
|
|
90
90
|
errorClass: Class<ERR> = Error as any,
|
|
91
91
|
): asserts err is ERR {
|
|
92
92
|
if (!(err instanceof errorClass)) {
|
|
93
|
-
|
|
94
|
-
`
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
userFriendly: true,
|
|
100
|
-
})
|
|
93
|
+
throw new AssertionError(
|
|
94
|
+
`Expected to be instanceof ${errorClass.name}, actual typeof: ${typeof err}`,
|
|
95
|
+
{
|
|
96
|
+
userFriendly: true,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
101
99
|
}
|
|
102
100
|
}
|
|
103
101
|
|
|
@@ -120,9 +118,7 @@ export function _assertIsErrorObject<DATA_TYPE extends ErrorData = ErrorData>(
|
|
|
120
118
|
obj: any,
|
|
121
119
|
): asserts obj is ErrorObject<DATA_TYPE> {
|
|
122
120
|
if (!_isErrorObject(obj)) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
throw new AssertionError(msg, {
|
|
121
|
+
throw new AssertionError(`Expected to be ErrorObject, actual typeof: ${typeof obj}`, {
|
|
126
122
|
userFriendly: true,
|
|
127
123
|
})
|
|
128
124
|
}
|
|
@@ -138,12 +134,7 @@ export function _assertIsNumber(v: any, message?: string): asserts v is number {
|
|
|
138
134
|
|
|
139
135
|
export function _assertTypeOf<T>(v: any, expectedType: string, message?: string): asserts v is T {
|
|
140
136
|
if (typeof v !== expectedType) {
|
|
141
|
-
const msg =
|
|
142
|
-
message || `unexpected type`,
|
|
143
|
-
`expected: ${expectedType}`,
|
|
144
|
-
`got : ${typeof v}`,
|
|
145
|
-
].join('\n')
|
|
146
|
-
|
|
137
|
+
const msg = message || `Expected typeof ${expectedType}, actual typeof: ${typeof v}`
|
|
147
138
|
throw new AssertionError(msg, {
|
|
148
139
|
userFriendly: true,
|
|
149
140
|
})
|
package/src/error/error.util.ts
CHANGED
|
@@ -441,9 +441,9 @@ export class TimeoutError extends AppError {
|
|
|
441
441
|
* "Pass" means "no error".
|
|
442
442
|
*/
|
|
443
443
|
export class UnexpectedPassError extends AppError {
|
|
444
|
-
constructor() {
|
|
444
|
+
constructor(message?: string) {
|
|
445
445
|
super(
|
|
446
|
-
'expected error was not thrown',
|
|
446
|
+
message || 'expected error was not thrown',
|
|
447
447
|
{},
|
|
448
448
|
{
|
|
449
449
|
name: 'UnexpectedPassError',
|
package/src/error/try.ts
CHANGED
|
@@ -66,10 +66,7 @@ export async function pTry<T, ERR extends Error = Error>(
|
|
|
66
66
|
export function _expectedError<ERR = Error>(fn: AnyFunction, errorClass?: Class<ERR>): ERR {
|
|
67
67
|
try {
|
|
68
68
|
fn()
|
|
69
|
-
// Unexpected!
|
|
70
|
-
throw new UnexpectedPassError()
|
|
71
69
|
} catch (err) {
|
|
72
|
-
if (err instanceof UnexpectedPassError) throw err // re-throw
|
|
73
70
|
if (errorClass && !(err instanceof errorClass)) {
|
|
74
71
|
console.warn(
|
|
75
72
|
`_expectedError expected ${errorClass.constructor.name} but got different error class`,
|
|
@@ -78,6 +75,9 @@ export function _expectedError<ERR = Error>(fn: AnyFunction, errorClass?: Class<
|
|
|
78
75
|
}
|
|
79
76
|
return err as ERR // this is expected!
|
|
80
77
|
}
|
|
78
|
+
|
|
79
|
+
// Unexpected!
|
|
80
|
+
throw new UnexpectedPassError()
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
/**
|
|
@@ -94,10 +94,7 @@ export async function pExpectedError<ERR = Error>(
|
|
|
94
94
|
): Promise<ERR> {
|
|
95
95
|
try {
|
|
96
96
|
await promise
|
|
97
|
-
// Unexpected!
|
|
98
|
-
throw new UnexpectedPassError()
|
|
99
97
|
} catch (err) {
|
|
100
|
-
if (err instanceof UnexpectedPassError) throw err // re-throw
|
|
101
98
|
if (errorClass && !(err instanceof errorClass)) {
|
|
102
99
|
console.warn(
|
|
103
100
|
`pExpectedError expected ${errorClass.constructor.name} but got different error class`,
|
|
@@ -106,6 +103,9 @@ export async function pExpectedError<ERR = Error>(
|
|
|
106
103
|
}
|
|
107
104
|
return err as ERR // this is expected!
|
|
108
105
|
}
|
|
106
|
+
|
|
107
|
+
// Unexpected!
|
|
108
|
+
throw new UnexpectedPassError()
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
@@ -227,6 +227,11 @@ export interface FetcherOptions {
|
|
|
227
227
|
* If true - enables all possible logging.
|
|
228
228
|
*/
|
|
229
229
|
debug?: boolean
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* If provided - will be used instead of static `Fetcher.callNativeFetch`.
|
|
233
|
+
*/
|
|
234
|
+
fetchFn?: FetchFunction
|
|
230
235
|
}
|
|
231
236
|
|
|
232
237
|
export type RequestInitNormalized = Omit<RequestInit, 'method' | 'headers'> & {
|
|
@@ -269,3 +274,10 @@ export type FetcherResponseType =
|
|
|
269
274
|
| 'arrayBuffer'
|
|
270
275
|
| 'blob'
|
|
271
276
|
| 'readableStream'
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Signature for the `fetch` function.
|
|
280
|
+
* Used to be able to override and provide a different implementation,
|
|
281
|
+
* e.g when mocking.
|
|
282
|
+
*/
|
|
283
|
+
export type FetchFunction = (url: string, init: RequestInitNormalized) => Promise<Response>
|
package/src/http/fetcher.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
/// <reference lib="dom.iterable"/>
|
|
4
4
|
|
|
5
5
|
import { isServerSide } from '../env'
|
|
6
|
-
import { _assertErrorClassOrRethrow } from '../error/assert'
|
|
6
|
+
import { _assertErrorClassOrRethrow, _assertIsError } from '../error/assert'
|
|
7
7
|
import { ErrorLike, ErrorObject } from '../error/error.model'
|
|
8
8
|
import {
|
|
9
9
|
_anyToError,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
_errorLikeToErrorObject,
|
|
12
12
|
HttpRequestError,
|
|
13
13
|
TimeoutError,
|
|
14
|
+
UnexpectedPassError,
|
|
14
15
|
} from '../error/error.util'
|
|
15
16
|
import { _clamp } from '../number/number.util'
|
|
16
17
|
import {
|
|
@@ -186,6 +187,23 @@ export class Fetcher {
|
|
|
186
187
|
return res.body
|
|
187
188
|
}
|
|
188
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Execute fetch and expect/assert it to return an Error (which will be wrapped in
|
|
192
|
+
* HttpRequestError as it normally would).
|
|
193
|
+
* If fetch succeeds, which is unexpected, it'll throw an UnexpectedPass error.
|
|
194
|
+
* Useful in unit testing.
|
|
195
|
+
*/
|
|
196
|
+
async expectError(opt: FetcherOptions): Promise<HttpRequestError> {
|
|
197
|
+
const res = await this.doFetch(opt)
|
|
198
|
+
|
|
199
|
+
if (!res.err) {
|
|
200
|
+
throw new UnexpectedPassError('Fetch was expected to error')
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
_assertIsError(res.err, HttpRequestError)
|
|
204
|
+
return res.err
|
|
205
|
+
}
|
|
206
|
+
|
|
189
207
|
/**
|
|
190
208
|
* Like pTry - returns a [err, data] tuple (aka ErrorDataTuple).
|
|
191
209
|
* err, if defined, is strictly HttpRequestError.
|
|
@@ -242,6 +260,9 @@ export class Fetcher {
|
|
|
242
260
|
// setup timeout
|
|
243
261
|
let timeoutId: number | undefined
|
|
244
262
|
if (timeoutSeconds) {
|
|
263
|
+
// Used for Request timeout (when timeoutSeconds is set),
|
|
264
|
+
// but also for "downloadBody" timeout (even after request returned with 200, but before we loaded the body)
|
|
265
|
+
// UPD: no, not using for "downloadBody" currently
|
|
245
266
|
const abortController = new AbortController()
|
|
246
267
|
req.init.signal = abortController.signal
|
|
247
268
|
timeoutId = setTimeout(() => {
|
|
@@ -265,7 +286,11 @@ export class Fetcher {
|
|
|
265
286
|
}
|
|
266
287
|
|
|
267
288
|
try {
|
|
268
|
-
|
|
289
|
+
// Calls cfg.fetchFn if set, otherwise Fetcher.callNativeFetch
|
|
290
|
+
res.fetchResponse = await (this.cfg.fetchFn || Fetcher.callNativeFetch)(
|
|
291
|
+
req.fullUrl,
|
|
292
|
+
req.init,
|
|
293
|
+
)
|
|
269
294
|
res.ok = res.fetchResponse.ok
|
|
270
295
|
// important to set it to undefined, otherwise it can keep the previous value (from previous try)
|
|
271
296
|
res.err = undefined
|
|
@@ -291,10 +316,14 @@ export class Fetcher {
|
|
|
291
316
|
await this.onOkResponse(res as FetcherResponse<T> & { fetchResponse: Response }),
|
|
292
317
|
{
|
|
293
318
|
timeout: timeoutSeconds * 1000 || Number.POSITIVE_INFINITY,
|
|
294
|
-
name: 'Fetcher.
|
|
319
|
+
name: 'Fetcher.downloadBody',
|
|
295
320
|
},
|
|
296
321
|
)
|
|
297
322
|
} catch (err) {
|
|
323
|
+
// Important to cancel the original request to not keep it running (and occupying resources)
|
|
324
|
+
// UPD: no, we probably don't need to, because "request" has already completed, it's just the "body" is pending
|
|
325
|
+
// if (err instanceof TimeoutError) {}
|
|
326
|
+
|
|
298
327
|
// onOkResponse can still fail, e.g when loading/parsing json, text or doing other response manipulation
|
|
299
328
|
res.err = _anyToError(err)
|
|
300
329
|
res.ok = false
|
|
@@ -405,13 +434,21 @@ export class Fetcher {
|
|
|
405
434
|
data: {},
|
|
406
435
|
}
|
|
407
436
|
|
|
408
|
-
|
|
437
|
+
let responseStatusCode = res.fetchResponse?.status || 0
|
|
438
|
+
if (res.statusFamily === 2) {
|
|
439
|
+
// important to reset httpStatusCode to 0 in this case, as status 2xx can be misleading
|
|
440
|
+
res.statusFamily = undefined
|
|
441
|
+
res.statusCode = undefined
|
|
442
|
+
responseStatusCode = 0
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const message = [res.statusCode, res.signature].filter(Boolean).join(' ')
|
|
409
446
|
|
|
410
447
|
res.err = new HttpRequestError(
|
|
411
448
|
message,
|
|
412
449
|
_filterNullishValues({
|
|
413
450
|
response: res.fetchResponse,
|
|
414
|
-
responseStatusCode
|
|
451
|
+
responseStatusCode,
|
|
415
452
|
// These properties are provided to be used in e.g custom Sentry error grouping
|
|
416
453
|
// Actually, disabled now, to avoid unnecessary error printing when both msg and data are printed
|
|
417
454
|
// Enabled, cause `data` is not printed by default when error is HttpError
|