@microlink/mql 0.15.1 → 0.16.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/LICENSE.md +0 -0
- package/dist/index.js +1005 -406
- package/dist/index.umd.js +1005 -406
- package/package.json +19 -21
- package/src/constants.js +8 -2
- package/src/index.js +164 -31
- package/src/factory.js +0 -144
package/dist/index.js
CHANGED
|
@@ -63,23 +63,53 @@ function flattie(input, glue, toNull) {
|
|
|
63
63
|
|
|
64
64
|
dist.flattie = flattie;
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
/**
|
|
67
|
+
Base class for all Ky-specific errors. `HTTPError`, `NetworkError`, `TimeoutError`, and `ForceRetryError` extend this class.
|
|
68
|
+
*/
|
|
69
|
+
class KyError extends Error {
|
|
70
|
+
name = 'KyError';
|
|
71
|
+
get isKyError() {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
Error thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled.
|
|
78
|
+
|
|
79
|
+
The error has a `response` property with the `Response` object, a `request` property with the `Request` object, an `options` property with the normalized options, and a `data` property with the pre-parsed response body. The response body is automatically consumed when populating `data`, so `response.json()` and other body methods will not work. Use `data` instead.
|
|
80
|
+
*/
|
|
81
|
+
class HTTPError extends KyError {
|
|
82
|
+
name = 'HTTPError';
|
|
67
83
|
response;
|
|
68
84
|
request;
|
|
69
85
|
options;
|
|
86
|
+
data;
|
|
70
87
|
constructor(response, request, options) {
|
|
71
88
|
const code = (response.status || response.status === 0) ? response.status : '';
|
|
72
89
|
const title = response.statusText ?? '';
|
|
73
90
|
const status = `${code} ${title}`.trim();
|
|
74
91
|
const reason = status ? `status code ${status}` : 'an unknown error';
|
|
75
92
|
super(`Request failed with ${reason}: ${request.method} ${request.url}`);
|
|
76
|
-
this.name = 'HTTPError';
|
|
77
93
|
this.response = response;
|
|
78
94
|
this.request = request;
|
|
79
95
|
this.options = options;
|
|
80
96
|
}
|
|
81
97
|
}
|
|
82
98
|
|
|
99
|
+
/**
|
|
100
|
+
Error thrown when a network error occurs during the request (e.g., DNS failure, connection refused, offline).
|
|
101
|
+
|
|
102
|
+
The error has a `request` property with the `Request` object. The original error is available via the standard `cause` property.
|
|
103
|
+
*/
|
|
104
|
+
class NetworkError extends KyError {
|
|
105
|
+
name = 'NetworkError';
|
|
106
|
+
request;
|
|
107
|
+
constructor(request, options) {
|
|
108
|
+
super(`Request failed due to a network error: ${request.method} ${request.url}`, options);
|
|
109
|
+
this.request = request;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
83
113
|
/**
|
|
84
114
|
Wrapper for non-Error values that were thrown.
|
|
85
115
|
|
|
@@ -108,10 +138,11 @@ class NonError extends Error {
|
|
|
108
138
|
}
|
|
109
139
|
|
|
110
140
|
/**
|
|
111
|
-
|
|
112
|
-
|
|
141
|
+
Error used to signal a forced retry from `afterResponse` hooks.
|
|
142
|
+
|
|
143
|
+
This is thrown when `ky.retry()` is returned from an `afterResponse` hook. It is observable in `beforeRetry` and `beforeError` hooks via the `isForceRetryError()` type guard.
|
|
113
144
|
*/
|
|
114
|
-
class ForceRetryError extends
|
|
145
|
+
class ForceRetryError extends KyError {
|
|
115
146
|
name = 'ForceRetryError';
|
|
116
147
|
customDelay;
|
|
117
148
|
code;
|
|
@@ -129,6 +160,51 @@ class ForceRetryError extends Error {
|
|
|
129
160
|
}
|
|
130
161
|
}
|
|
131
162
|
|
|
163
|
+
/**
|
|
164
|
+
Thrown when a response body fails validation against a user-provided Standard Schema.
|
|
165
|
+
|
|
166
|
+
This error intentionally does not extend `KyError` because it does not represent a failure in Ky's HTTP lifecycle. The request succeeded; the user's schema rejected the data. As such, it is not matched by `isKyError()`.
|
|
167
|
+
|
|
168
|
+
@example
|
|
169
|
+
```
|
|
170
|
+
import ky, {SchemaValidationError} from 'ky';
|
|
171
|
+
import {z} from 'zod';
|
|
172
|
+
|
|
173
|
+
const userSchema = z.object({name: z.string()});
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const user = await ky('/api/user').json(userSchema);
|
|
177
|
+
console.log(user.name);
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (error instanceof SchemaValidationError) {
|
|
180
|
+
console.error(error.issues);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
*/
|
|
185
|
+
class SchemaValidationError extends Error {
|
|
186
|
+
name = 'SchemaValidationError';
|
|
187
|
+
issues;
|
|
188
|
+
constructor(issues) {
|
|
189
|
+
super('Response schema validation failed');
|
|
190
|
+
this.issues = issues;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
Error thrown when the request times out.
|
|
196
|
+
|
|
197
|
+
The error has a `request` property with the `Request` object.
|
|
198
|
+
*/
|
|
199
|
+
class TimeoutError extends KyError {
|
|
200
|
+
name = 'TimeoutError';
|
|
201
|
+
request;
|
|
202
|
+
constructor(request) {
|
|
203
|
+
super(`Request timed out: ${request.method} ${request.url}`);
|
|
204
|
+
this.request = request;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
132
208
|
const supportsRequestStreams = (() => {
|
|
133
209
|
let duplexAccessed = false;
|
|
134
210
|
let hasContentType = false;
|
|
@@ -173,11 +249,14 @@ const responseTypes = {
|
|
|
173
249
|
};
|
|
174
250
|
// The maximum value of a 32bit int (see issue #117)
|
|
175
251
|
const maxSafeTimeout = 2_147_483_647;
|
|
176
|
-
// Size in bytes of a typical form boundary, used to help estimate upload size
|
|
177
|
-
const usualFormBoundarySize =
|
|
252
|
+
// Size in bytes of a typical form boundary (e.g., '------WebKitFormBoundaryaxpyiPgbbPti10Rw'), used to help estimate upload size
|
|
253
|
+
const usualFormBoundarySize = 40;
|
|
254
|
+
/**
|
|
255
|
+
Symbol that can be returned by a `beforeRetry` hook to stop retrying without throwing an error.
|
|
256
|
+
*/
|
|
178
257
|
const stop = Symbol('stop');
|
|
179
258
|
/**
|
|
180
|
-
Marker returned by ky.retry() to signal a forced retry from afterResponse hooks.
|
|
259
|
+
Marker returned by `ky.retry()` to signal a forced retry from `afterResponse` hooks.
|
|
181
260
|
*/
|
|
182
261
|
class RetryMarker {
|
|
183
262
|
options;
|
|
@@ -199,7 +278,7 @@ import ky, {isForceRetryError} from 'ky';
|
|
|
199
278
|
const api = ky.extend({
|
|
200
279
|
hooks: {
|
|
201
280
|
afterResponse: [
|
|
202
|
-
async (request,
|
|
281
|
+
async ({request, response}) => {
|
|
203
282
|
// Retry based on response body content
|
|
204
283
|
if (response.status === 200) {
|
|
205
284
|
const data = await response.clone().json();
|
|
@@ -274,9 +353,11 @@ const kyOptionKeys = {
|
|
|
274
353
|
parseJson: true,
|
|
275
354
|
stringifyJson: true,
|
|
276
355
|
searchParams: true,
|
|
277
|
-
|
|
356
|
+
baseUrl: true,
|
|
357
|
+
prefix: true,
|
|
278
358
|
retry: true,
|
|
279
359
|
timeout: true,
|
|
360
|
+
totalTimeout: true,
|
|
280
361
|
hooks: true,
|
|
281
362
|
throwHttpErrors: true,
|
|
282
363
|
onDownloadProgress: true,
|
|
@@ -312,7 +393,8 @@ const requestOptionsRegistry = {
|
|
|
312
393
|
duplex: true,
|
|
313
394
|
};
|
|
314
395
|
|
|
315
|
-
|
|
396
|
+
const encoder = new TextEncoder();
|
|
397
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
316
398
|
const getBodySize = (body) => {
|
|
317
399
|
if (!body) {
|
|
318
400
|
return 0;
|
|
@@ -322,9 +404,9 @@ const getBodySize = (body) => {
|
|
|
322
404
|
let size = 0;
|
|
323
405
|
for (const [key, value] of body) {
|
|
324
406
|
size += usualFormBoundarySize;
|
|
325
|
-
size +=
|
|
407
|
+
size += encoder.encode(`Content-Disposition: form-data; name="${key}"`).byteLength;
|
|
326
408
|
size += typeof value === 'string'
|
|
327
|
-
?
|
|
409
|
+
? encoder.encode(value).byteLength
|
|
328
410
|
: value.size;
|
|
329
411
|
}
|
|
330
412
|
return size;
|
|
@@ -332,28 +414,16 @@ const getBodySize = (body) => {
|
|
|
332
414
|
if (body instanceof Blob) {
|
|
333
415
|
return body.size;
|
|
334
416
|
}
|
|
335
|
-
if (body instanceof ArrayBuffer) {
|
|
417
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
|
|
336
418
|
return body.byteLength;
|
|
337
419
|
}
|
|
338
420
|
if (typeof body === 'string') {
|
|
339
|
-
return
|
|
421
|
+
return encoder.encode(body).byteLength;
|
|
340
422
|
}
|
|
341
423
|
if (body instanceof URLSearchParams) {
|
|
342
|
-
return
|
|
343
|
-
}
|
|
344
|
-
if ('byteLength' in body) {
|
|
345
|
-
return (body).byteLength;
|
|
424
|
+
return encoder.encode(body.toString()).byteLength;
|
|
346
425
|
}
|
|
347
|
-
|
|
348
|
-
try {
|
|
349
|
-
const jsonString = JSON.stringify(body);
|
|
350
|
-
return new TextEncoder().encode(jsonString).length;
|
|
351
|
-
}
|
|
352
|
-
catch {
|
|
353
|
-
return 0;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return 0; // Default case, unable to determine size
|
|
426
|
+
return 0;
|
|
357
427
|
};
|
|
358
428
|
const withProgress = (stream, totalBytes, onProgress) => {
|
|
359
429
|
let previousChunk;
|
|
@@ -386,21 +456,18 @@ const streamResponse = (response, onDownloadProgress) => {
|
|
|
386
456
|
if (!response.body) {
|
|
387
457
|
return response;
|
|
388
458
|
}
|
|
389
|
-
|
|
390
|
-
return new Response(null, {
|
|
391
|
-
status: response.status,
|
|
392
|
-
statusText: response.statusText,
|
|
393
|
-
headers: response.headers,
|
|
394
|
-
});
|
|
395
|
-
}
|
|
396
|
-
const totalBytes = Math.max(0, Number(response.headers.get('content-length')) || 0);
|
|
397
|
-
return new Response(withProgress(response.body, totalBytes, onDownloadProgress), {
|
|
459
|
+
const responseInit = {
|
|
398
460
|
status: response.status,
|
|
399
461
|
statusText: response.statusText,
|
|
400
462
|
headers: response.headers,
|
|
401
|
-
}
|
|
463
|
+
};
|
|
464
|
+
if (response.status === 204) {
|
|
465
|
+
return new Response(null, responseInit);
|
|
466
|
+
}
|
|
467
|
+
const totalBytes = Math.max(0, Number(response.headers.get('content-length')) || 0);
|
|
468
|
+
return new Response(withProgress(response.body, totalBytes, onDownloadProgress), responseInit);
|
|
402
469
|
};
|
|
403
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
470
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
404
471
|
const streamRequest = (request, onUploadProgress, originalBody) => {
|
|
405
472
|
if (!request.body) {
|
|
406
473
|
return request;
|
|
@@ -414,12 +481,46 @@ const streamRequest = (request, onUploadProgress, originalBody) => {
|
|
|
414
481
|
});
|
|
415
482
|
};
|
|
416
483
|
|
|
417
|
-
// eslint-disable-next-line @typescript-eslint/
|
|
418
|
-
const isObject
|
|
484
|
+
// eslint-disable-next-line @typescript-eslint/no-restricted-types
|
|
485
|
+
const isObject = (value) => value !== null && typeof value === 'object';
|
|
486
|
+
|
|
487
|
+
const replaceSymbol = Symbol('replaceOption');
|
|
488
|
+
const getReplaceState = (value) => isObject(value) && value[replaceSymbol] === true
|
|
489
|
+
? {
|
|
490
|
+
isReplace: true,
|
|
491
|
+
value: value.value,
|
|
492
|
+
}
|
|
493
|
+
: {
|
|
494
|
+
isReplace: false,
|
|
495
|
+
value,
|
|
496
|
+
};
|
|
497
|
+
/**
|
|
498
|
+
Wraps a value so that `ky.extend()` will replace the parent value instead of merging with it.
|
|
499
|
+
|
|
500
|
+
By default, `.extend()` deep-merges options with the parent instance: hooks get appended, headers get merged, and search parameters get accumulated. Use `replaceOption` when you want to fully replace a merged property instead.
|
|
501
|
+
|
|
502
|
+
@example
|
|
503
|
+
```
|
|
504
|
+
import ky, {replaceOption} from 'ky';
|
|
505
|
+
|
|
506
|
+
const base = ky.create({
|
|
507
|
+
hooks: {beforeRequest: [addAuth, addTracking]},
|
|
508
|
+
});
|
|
419
509
|
|
|
510
|
+
// Replaces instead of appending
|
|
511
|
+
const extended = base.extend({
|
|
512
|
+
hooks: replaceOption({beforeRequest: [onlyThis]}),
|
|
513
|
+
});
|
|
514
|
+
// hooks.beforeRequest is now [onlyThis], not [addAuth, addTracking, onlyThis]
|
|
515
|
+
```
|
|
516
|
+
*/
|
|
517
|
+
const replaceOption = (value) => {
|
|
518
|
+
const markedValue = { [replaceSymbol]: true, value };
|
|
519
|
+
return markedValue;
|
|
520
|
+
};
|
|
420
521
|
const validateAndMerge = (...sources) => {
|
|
421
522
|
for (const source of sources) {
|
|
422
|
-
if ((!isObject
|
|
523
|
+
if ((!isObject(source) || Array.isArray(source)) && source !== undefined) {
|
|
423
524
|
throw new TypeError('The `options` argument must be an object');
|
|
424
525
|
}
|
|
425
526
|
}
|
|
@@ -439,19 +540,52 @@ const mergeHeaders = (source1 = {}, source2 = {}) => {
|
|
|
439
540
|
}
|
|
440
541
|
return result;
|
|
441
542
|
};
|
|
543
|
+
const isPlainObject = (value) => {
|
|
544
|
+
if (!isObject(value) || Array.isArray(value)) {
|
|
545
|
+
return false;
|
|
546
|
+
}
|
|
547
|
+
const prototype = Object.getPrototypeOf(value);
|
|
548
|
+
return prototype === Object.prototype || prototype === null;
|
|
549
|
+
};
|
|
550
|
+
const cloneShallow = (value) => {
|
|
551
|
+
if (value instanceof URLSearchParams) {
|
|
552
|
+
return new URLSearchParams(value);
|
|
553
|
+
}
|
|
554
|
+
if (value instanceof globalThis.Headers) {
|
|
555
|
+
return new globalThis.Headers(value);
|
|
556
|
+
}
|
|
557
|
+
if (Array.isArray(value)) {
|
|
558
|
+
return [...value];
|
|
559
|
+
}
|
|
560
|
+
if (isPlainObject(value)) {
|
|
561
|
+
const copy = { ...value };
|
|
562
|
+
return copy;
|
|
563
|
+
}
|
|
564
|
+
return value;
|
|
565
|
+
};
|
|
566
|
+
const normalizeHeaderObject = (headers) => Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== undefined));
|
|
567
|
+
const mergeHeaderContainers = (source1, source2) => {
|
|
568
|
+
if (isPlainObject(source1) && isPlainObject(source2)) {
|
|
569
|
+
return normalizeHeaderObject({ ...source1, ...source2 });
|
|
570
|
+
}
|
|
571
|
+
return mergeHeaders(source1, source2);
|
|
572
|
+
};
|
|
442
573
|
function newHookValue(original, incoming, property) {
|
|
443
574
|
return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
|
|
444
575
|
? []
|
|
445
576
|
: deepMerge(original[property] ?? [], incoming[property] ?? []);
|
|
446
577
|
}
|
|
447
578
|
const mergeHooks = (original = {}, incoming = {}) => ({
|
|
579
|
+
init: newHookValue(original, incoming, 'init'),
|
|
448
580
|
beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
|
|
449
581
|
beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
|
|
450
|
-
afterResponse: newHookValue(original, incoming, 'afterResponse'),
|
|
451
582
|
beforeError: newHookValue(original, incoming, 'beforeError'),
|
|
583
|
+
afterResponse: newHookValue(original, incoming, 'afterResponse'),
|
|
452
584
|
});
|
|
585
|
+
const deletedParametersSymbol = Symbol('deletedParameters');
|
|
453
586
|
const appendSearchParameters = (target, source) => {
|
|
454
587
|
const result = new URLSearchParams();
|
|
588
|
+
const deleted = new Set();
|
|
455
589
|
for (const input of [target, source]) {
|
|
456
590
|
if (input === undefined) {
|
|
457
591
|
continue;
|
|
@@ -459,6 +593,14 @@ const appendSearchParameters = (target, source) => {
|
|
|
459
593
|
if (input instanceof URLSearchParams) {
|
|
460
594
|
for (const [key, value] of input.entries()) {
|
|
461
595
|
result.append(key, value);
|
|
596
|
+
deleted.delete(key);
|
|
597
|
+
}
|
|
598
|
+
const inputDeleted = input[deletedParametersSymbol];
|
|
599
|
+
if (inputDeleted) {
|
|
600
|
+
for (const key of inputDeleted) {
|
|
601
|
+
result.delete(key);
|
|
602
|
+
deleted.add(key);
|
|
603
|
+
}
|
|
462
604
|
}
|
|
463
605
|
}
|
|
464
606
|
else if (Array.isArray(input)) {
|
|
@@ -467,12 +609,18 @@ const appendSearchParameters = (target, source) => {
|
|
|
467
609
|
throw new TypeError('Array search parameters must be provided in [[key, value], ...] format');
|
|
468
610
|
}
|
|
469
611
|
result.append(String(pair[0]), String(pair[1]));
|
|
612
|
+
deleted.delete(String(pair[0]));
|
|
470
613
|
}
|
|
471
614
|
}
|
|
472
|
-
else if (isObject
|
|
615
|
+
else if (isObject(input)) {
|
|
473
616
|
for (const [key, value] of Object.entries(input)) {
|
|
474
|
-
if (value
|
|
617
|
+
if (value === undefined) {
|
|
618
|
+
result.delete(key);
|
|
619
|
+
deleted.add(key);
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
475
622
|
result.append(key, String(value));
|
|
623
|
+
deleted.delete(key);
|
|
476
624
|
}
|
|
477
625
|
}
|
|
478
626
|
}
|
|
@@ -481,9 +629,13 @@ const appendSearchParameters = (target, source) => {
|
|
|
481
629
|
const parameters = new URLSearchParams(input);
|
|
482
630
|
for (const [key, value] of parameters.entries()) {
|
|
483
631
|
result.append(key, value);
|
|
632
|
+
deleted.delete(key);
|
|
484
633
|
}
|
|
485
634
|
}
|
|
486
635
|
}
|
|
636
|
+
if (deleted.size > 0) {
|
|
637
|
+
result[deletedParametersSymbol] = deleted;
|
|
638
|
+
}
|
|
487
639
|
return result;
|
|
488
640
|
};
|
|
489
641
|
// TODO: Make this strongly-typed (no `any`).
|
|
@@ -500,16 +652,19 @@ const deepMerge = (...sources) => {
|
|
|
500
652
|
}
|
|
501
653
|
returnValue = [...returnValue, ...source];
|
|
502
654
|
}
|
|
503
|
-
else if (isObject
|
|
655
|
+
else if (isObject(source)) {
|
|
504
656
|
for (let [key, value] of Object.entries(source)) {
|
|
505
657
|
// Special handling for AbortSignal instances
|
|
506
658
|
if (key === 'signal' && value instanceof globalThis.AbortSignal) {
|
|
507
659
|
signals.push(value);
|
|
508
660
|
continue;
|
|
509
661
|
}
|
|
662
|
+
const replaceState = getReplaceState(value);
|
|
663
|
+
const { isReplace } = replaceState;
|
|
664
|
+
value = replaceState.value;
|
|
510
665
|
// Special handling for context - shallow merge only
|
|
511
666
|
if (key === 'context') {
|
|
512
|
-
if (value !== undefined && value !== null && (!isObject
|
|
667
|
+
if (value !== undefined && value !== null && (!isObject(value) || Array.isArray(value))) {
|
|
513
668
|
throw new TypeError('The `context` option must be an object');
|
|
514
669
|
}
|
|
515
670
|
// Shallow merge: always create a new object to prevent mutation bugs
|
|
@@ -517,7 +672,9 @@ const deepMerge = (...sources) => {
|
|
|
517
672
|
...returnValue,
|
|
518
673
|
context: (value === undefined || value === null)
|
|
519
674
|
? {}
|
|
520
|
-
:
|
|
675
|
+
: (isReplace
|
|
676
|
+
? { ...value }
|
|
677
|
+
: { ...returnValue.context, ...value }),
|
|
521
678
|
};
|
|
522
679
|
continue;
|
|
523
680
|
}
|
|
@@ -527,6 +684,9 @@ const deepMerge = (...sources) => {
|
|
|
527
684
|
// Explicit undefined or null removes searchParams
|
|
528
685
|
searchParameters = undefined;
|
|
529
686
|
}
|
|
687
|
+
else if (isReplace) {
|
|
688
|
+
searchParameters = value;
|
|
689
|
+
}
|
|
530
690
|
else {
|
|
531
691
|
// First source: keep as-is to preserve type (string/object/URLSearchParams)
|
|
532
692
|
// Subsequent sources: merge and convert to URLSearchParams
|
|
@@ -534,17 +694,23 @@ const deepMerge = (...sources) => {
|
|
|
534
694
|
}
|
|
535
695
|
continue;
|
|
536
696
|
}
|
|
537
|
-
if (isObject
|
|
697
|
+
if (isObject(value) && !isReplace && key in returnValue) {
|
|
538
698
|
value = deepMerge(returnValue[key], value);
|
|
539
699
|
}
|
|
540
700
|
returnValue = { ...returnValue, [key]: value };
|
|
541
701
|
}
|
|
542
|
-
if (isObject
|
|
543
|
-
|
|
702
|
+
if (isObject(source.hooks)) {
|
|
703
|
+
const { value: hookValue, isReplace } = getReplaceState(source.hooks);
|
|
704
|
+
hooks = isReplace
|
|
705
|
+
? mergeHooks({}, hookValue)
|
|
706
|
+
: mergeHooks(hooks, hookValue);
|
|
544
707
|
returnValue.hooks = hooks;
|
|
545
708
|
}
|
|
546
|
-
if (isObject
|
|
547
|
-
|
|
709
|
+
if (isObject(source.headers)) {
|
|
710
|
+
const { value: headerValue, isReplace } = getReplaceState(source.headers);
|
|
711
|
+
headers = isReplace
|
|
712
|
+
? cloneShallow(headerValue)
|
|
713
|
+
: mergeHeaderContainers(headers, headerValue);
|
|
548
714
|
returnValue.headers = headers;
|
|
549
715
|
}
|
|
550
716
|
}
|
|
@@ -605,15 +771,6 @@ const normalizeRetryOptions = (retry = {}) => {
|
|
|
605
771
|
};
|
|
606
772
|
};
|
|
607
773
|
|
|
608
|
-
class TimeoutError extends Error {
|
|
609
|
-
request;
|
|
610
|
-
constructor(request) {
|
|
611
|
-
super(`Request timed out: ${request.method} ${request.url}`);
|
|
612
|
-
this.name = 'TimeoutError';
|
|
613
|
-
this.request = request;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
774
|
// `Promise.race()` workaround (#91)
|
|
618
775
|
async function timeout(request, init, abortController, options) {
|
|
619
776
|
return new Promise((resolve, reject) => {
|
|
@@ -679,7 +836,7 @@ const hasSearchParameters = (search) => {
|
|
|
679
836
|
return search.length > 0;
|
|
680
837
|
}
|
|
681
838
|
if (search instanceof URLSearchParams) {
|
|
682
|
-
return search.size > 0;
|
|
839
|
+
return search.size > 0 || Boolean(search[deletedParametersSymbol]?.size);
|
|
683
840
|
}
|
|
684
841
|
// Record
|
|
685
842
|
if (typeof search === 'object') {
|
|
@@ -691,8 +848,52 @@ const hasSearchParameters = (search) => {
|
|
|
691
848
|
return Boolean(search);
|
|
692
849
|
};
|
|
693
850
|
|
|
851
|
+
// Inlined from https://github.com/sindresorhus/is-network-error v1.3.1
|
|
852
|
+
const objectToString = Object.prototype.toString;
|
|
853
|
+
const isError = (value) => objectToString.call(value) === '[object Error]';
|
|
854
|
+
const errorMessages = new Set([
|
|
855
|
+
'network error', // Chrome
|
|
856
|
+
'NetworkError when attempting to fetch resource.', // Firefox
|
|
857
|
+
'The Internet connection appears to be offline.', // Safari 16
|
|
858
|
+
'Network request failed', // `cross-fetch`
|
|
859
|
+
'fetch failed', // Undici (Node.js)
|
|
860
|
+
'terminated', // Undici (Node.js)
|
|
861
|
+
' A network error occurred.', // Bun (WebKit) - leading space is intentional
|
|
862
|
+
'Network connection lost', // Cloudflare Workers (fetch)
|
|
863
|
+
]);
|
|
864
|
+
function isRawNetworkError(error) {
|
|
865
|
+
const isValid = error
|
|
866
|
+
&& isError(error)
|
|
867
|
+
&& error.name === 'TypeError'
|
|
868
|
+
&& typeof error.message === 'string';
|
|
869
|
+
if (!isValid) {
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
const { message, stack } = error;
|
|
873
|
+
// Safari 17+ has generic message but no stack for network errors
|
|
874
|
+
if (message === 'Load failed') {
|
|
875
|
+
return stack === undefined
|
|
876
|
+
// Sentry adds its own stack trace to the fetch error, so also check for that
|
|
877
|
+
|| '__sentry_captured__' in error;
|
|
878
|
+
}
|
|
879
|
+
// Deno network errors start with specific text
|
|
880
|
+
if (message.startsWith('error sending request for url')) {
|
|
881
|
+
return true;
|
|
882
|
+
}
|
|
883
|
+
// Chrome: exact "Failed to fetch" or with hostname: "Failed to fetch (example.com)"
|
|
884
|
+
if (message === 'Failed to fetch' || (message.startsWith('Failed to fetch (') && message.endsWith(')'))) {
|
|
885
|
+
return true;
|
|
886
|
+
}
|
|
887
|
+
// Standard network error messages
|
|
888
|
+
return errorMessages.has(message);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Handles cross-realm cases (e.g., iframes, different JS contexts) where `instanceof` fails.
|
|
892
|
+
const isErrorType = (error, cls) => error instanceof cls || error?.name === cls.name;
|
|
694
893
|
/**
|
|
695
|
-
Type guard to check if an error is a
|
|
894
|
+
Type guard to check if an error is a `KyError`.
|
|
895
|
+
|
|
896
|
+
Note: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself.
|
|
696
897
|
|
|
697
898
|
@param error - The error to check
|
|
698
899
|
@returns `true` if the error is a Ky error, `false` otherwise
|
|
@@ -714,13 +915,13 @@ try {
|
|
|
714
915
|
```
|
|
715
916
|
*/
|
|
716
917
|
function isKyError(error) {
|
|
717
|
-
return isHTTPError(error) || isTimeoutError(error) || isForceRetryError(error);
|
|
918
|
+
return error?.isKyError === true || isHTTPError(error) || isNetworkError(error) || isTimeoutError(error) || isForceRetryError(error);
|
|
718
919
|
}
|
|
719
920
|
/**
|
|
720
|
-
Type guard to check if an error is an HTTPError
|
|
921
|
+
Type guard to check if an error is an `HTTPError`.
|
|
721
922
|
|
|
722
923
|
@param error - The error to check
|
|
723
|
-
@returns `true` if the error is an HTTPError
|
|
924
|
+
@returns `true` if the error is an `HTTPError`, `false` otherwise
|
|
724
925
|
|
|
725
926
|
@example
|
|
726
927
|
```
|
|
@@ -735,13 +936,34 @@ try {
|
|
|
735
936
|
```
|
|
736
937
|
*/
|
|
737
938
|
function isHTTPError(error) {
|
|
738
|
-
return
|
|
939
|
+
return isErrorType(error, HTTPError);
|
|
739
940
|
}
|
|
740
941
|
/**
|
|
741
|
-
Type guard to check if an error is a
|
|
942
|
+
Type guard to check if an error is a `NetworkError`.
|
|
742
943
|
|
|
743
944
|
@param error - The error to check
|
|
744
|
-
@returns `true` if the error is a
|
|
945
|
+
@returns `true` if the error is a `NetworkError`, `false` otherwise
|
|
946
|
+
|
|
947
|
+
@example
|
|
948
|
+
```
|
|
949
|
+
import ky, {isNetworkError} from 'ky';
|
|
950
|
+
try {
|
|
951
|
+
const response = await ky.get('/api/data');
|
|
952
|
+
} catch (error) {
|
|
953
|
+
if (isNetworkError(error)) {
|
|
954
|
+
console.log('Network error:', error.request.url);
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
*/
|
|
959
|
+
function isNetworkError(error) {
|
|
960
|
+
return isErrorType(error, NetworkError);
|
|
961
|
+
}
|
|
962
|
+
/**
|
|
963
|
+
Type guard to check if an error is a `TimeoutError`.
|
|
964
|
+
|
|
965
|
+
@param error - The error to check
|
|
966
|
+
@returns `true` if the error is a `TimeoutError`, `false` otherwise
|
|
745
967
|
|
|
746
968
|
@example
|
|
747
969
|
```
|
|
@@ -756,13 +978,13 @@ try {
|
|
|
756
978
|
```
|
|
757
979
|
*/
|
|
758
980
|
function isTimeoutError(error) {
|
|
759
|
-
return
|
|
981
|
+
return isErrorType(error, TimeoutError);
|
|
760
982
|
}
|
|
761
983
|
/**
|
|
762
|
-
Type guard to check if an error is a ForceRetryError
|
|
984
|
+
Type guard to check if an error is a `ForceRetryError`.
|
|
763
985
|
|
|
764
986
|
@param error - The error to check
|
|
765
|
-
@returns `true` if the error is a ForceRetryError
|
|
987
|
+
@returns `true` if the error is a `ForceRetryError`, `false` otherwise
|
|
766
988
|
|
|
767
989
|
@example
|
|
768
990
|
```
|
|
@@ -782,65 +1004,120 @@ const api = ky.extend({
|
|
|
782
1004
|
```
|
|
783
1005
|
*/
|
|
784
1006
|
function isForceRetryError(error) {
|
|
785
|
-
return
|
|
1007
|
+
return isErrorType(error, ForceRetryError);
|
|
786
1008
|
}
|
|
787
1009
|
|
|
1010
|
+
const maxErrorResponseBodySize = 10 * 1024 * 1024;
|
|
1011
|
+
const prefixUrlRenamedErrorMessage = 'The `prefixUrl` option has been renamed `prefix` in v2 and enhanced to allow slashes in input. See also the new `baseUrl` option for improved flexibility with standard URL resolution: https://github.com/sindresorhus/ky#baseurl';
|
|
1012
|
+
const timedOutResponseData = Symbol('timedOutResponseData');
|
|
1013
|
+
const createTextDecoder = (contentType) => {
|
|
1014
|
+
const match = /;\s*charset\s*=\s*(?:"([^"]+)"|([^;,\s]+))/i.exec(contentType);
|
|
1015
|
+
const charset = match?.[1] ?? match?.[2];
|
|
1016
|
+
if (charset) {
|
|
1017
|
+
try {
|
|
1018
|
+
return new TextDecoder(charset);
|
|
1019
|
+
}
|
|
1020
|
+
catch { }
|
|
1021
|
+
}
|
|
1022
|
+
return new TextDecoder();
|
|
1023
|
+
};
|
|
1024
|
+
const invalidSchemaMessage = 'The `schema` argument must follow the Standard Schema specification';
|
|
1025
|
+
// Shallow-clone mutable option properties so init hook mutations don't leak across requests.
|
|
1026
|
+
function cloneInitHookOptions(options) {
|
|
1027
|
+
return {
|
|
1028
|
+
...options,
|
|
1029
|
+
json: cloneShallow(options.json),
|
|
1030
|
+
retry: cloneShallow(options.retry),
|
|
1031
|
+
context: cloneShallow(options.context),
|
|
1032
|
+
headers: cloneShallow(options.headers),
|
|
1033
|
+
searchParams: cloneShallow(options.searchParams),
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const validateJsonWithSchema = async (jsonValue, schema) => {
|
|
1037
|
+
if ((typeof schema !== 'object'
|
|
1038
|
+
&& typeof schema !== 'function')
|
|
1039
|
+
|| schema === null) {
|
|
1040
|
+
throw new TypeError(invalidSchemaMessage);
|
|
1041
|
+
}
|
|
1042
|
+
const standardSchema = schema['~standard'];
|
|
1043
|
+
if (typeof standardSchema !== 'object'
|
|
1044
|
+
|| standardSchema === null
|
|
1045
|
+
|| typeof standardSchema.validate !== 'function') {
|
|
1046
|
+
throw new TypeError(invalidSchemaMessage);
|
|
1047
|
+
}
|
|
1048
|
+
const validationResult = await standardSchema.validate(jsonValue);
|
|
1049
|
+
if (validationResult.issues) {
|
|
1050
|
+
throw new SchemaValidationError(validationResult.issues);
|
|
1051
|
+
}
|
|
1052
|
+
return validationResult.value;
|
|
1053
|
+
};
|
|
788
1054
|
class Ky {
|
|
789
1055
|
static create(input, options) {
|
|
790
|
-
const
|
|
1056
|
+
const initHooks = options.hooks?.init ?? [];
|
|
1057
|
+
const initHookOptions = initHooks.length > 0 ? cloneInitHookOptions(options) : options;
|
|
1058
|
+
for (const hook of initHooks) {
|
|
1059
|
+
hook(initHookOptions);
|
|
1060
|
+
}
|
|
1061
|
+
const ky = new Ky(input, initHookOptions);
|
|
791
1062
|
const function_ = async () => {
|
|
792
1063
|
if (typeof ky.#options.timeout === 'number' && ky.#options.timeout > maxSafeTimeout) {
|
|
793
1064
|
throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`);
|
|
794
1065
|
}
|
|
1066
|
+
if (typeof ky.#options.totalTimeout === 'number' && ky.#options.totalTimeout > maxSafeTimeout) {
|
|
1067
|
+
throw new RangeError(`The \`totalTimeout\` option cannot be greater than ${maxSafeTimeout}`);
|
|
1068
|
+
}
|
|
795
1069
|
// Delay the fetch so that body method shortcuts can set the Accept header
|
|
796
1070
|
await Promise.resolve();
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
let
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
1071
|
+
const beforeRequestResponse = await ky.#runBeforeRequestHooks();
|
|
1072
|
+
let response = beforeRequestResponse ?? await ky.#retry(async () => ky.#fetch());
|
|
1073
|
+
let responseFromHook = beforeRequestResponse !== undefined
|
|
1074
|
+
|| ky.#consumeReturnedResponseFromBeforeRetryHook();
|
|
1075
|
+
if (!(response instanceof globalThis.Response)) {
|
|
1076
|
+
return response;
|
|
1077
|
+
}
|
|
1078
|
+
for (;;) {
|
|
804
1079
|
try {
|
|
805
1080
|
// eslint-disable-next-line no-await-in-loop
|
|
806
|
-
|
|
1081
|
+
response = await ky.#runAfterResponseHooks(response);
|
|
807
1082
|
}
|
|
808
1083
|
catch (error) {
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
ky.#
|
|
819
|
-
|
|
820
|
-
}
|
|
821
|
-
// Determine which response to use going forward
|
|
822
|
-
const nextResponse = modifiedResponse instanceof globalThis.Response ? modifiedResponse : response;
|
|
823
|
-
// Cancel any response bodies we won't use to prevent memory leaks.
|
|
824
|
-
// Uses fire-and-forget since hooks may have cloned the response, creating tee branches that block cancellation.
|
|
825
|
-
if (clonedResponse !== nextResponse) {
|
|
826
|
-
ky.#cancelResponseBody(clonedResponse);
|
|
827
|
-
}
|
|
828
|
-
if (response !== nextResponse) {
|
|
829
|
-
ky.#cancelResponseBody(response);
|
|
1084
|
+
if (!(error instanceof ForceRetryError)) {
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1088
|
+
const retriedResponse = await ky.#retryFromError(error, async () => ky.#fetch());
|
|
1089
|
+
if (!(retriedResponse instanceof globalThis.Response)) {
|
|
1090
|
+
return retriedResponse;
|
|
1091
|
+
}
|
|
1092
|
+
response = retriedResponse;
|
|
1093
|
+
responseFromHook = ky.#consumeReturnedResponseFromBeforeRetryHook();
|
|
1094
|
+
continue;
|
|
830
1095
|
}
|
|
831
|
-
response
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
1096
|
+
// Opaque responses (`response.type === 'opaque'`) from `no-cors` requests always have `status: 0` and `ok: false`, but this is not a failure - the actual status is hidden by the browser.
|
|
1097
|
+
if (!response.ok && response.type !== 'opaque' && (typeof ky.#options.throwHttpErrors === 'function'
|
|
1098
|
+
? ky.#options.throwHttpErrors(response.status)
|
|
1099
|
+
: ky.#options.throwHttpErrors)) {
|
|
1100
|
+
// `request` must reflect the request that actually failed, but `options` stays as Ky's
|
|
1101
|
+
// normalized options snapshot. Replacement `Request` instances do not preserve the
|
|
1102
|
+
// original `BodyInit`, so trying to make `options` mirror arbitrary requests would be lossy.
|
|
1103
|
+
const error = new HTTPError(response, ky.#getResponseRequest(response), ky.#getNormalizedOptions());
|
|
839
1104
|
// eslint-disable-next-line no-await-in-loop
|
|
840
|
-
error = await
|
|
1105
|
+
error.data = await ky.#getResponseData(response);
|
|
1106
|
+
if (responseFromHook) {
|
|
1107
|
+
throw error;
|
|
1108
|
+
}
|
|
1109
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1110
|
+
const retriedResponse = await ky.#retryFromError(error, async () => ky.#fetch());
|
|
1111
|
+
if (!(retriedResponse instanceof globalThis.Response)) {
|
|
1112
|
+
return retriedResponse;
|
|
1113
|
+
}
|
|
1114
|
+
response = retriedResponse;
|
|
1115
|
+
responseFromHook = ky.#consumeReturnedResponseFromBeforeRetryHook();
|
|
1116
|
+
continue;
|
|
841
1117
|
}
|
|
842
|
-
|
|
1118
|
+
break;
|
|
843
1119
|
}
|
|
1120
|
+
ky.#decorateResponse(response);
|
|
844
1121
|
// If `onDownloadProgress` is passed, it uses the stream API internally
|
|
845
1122
|
if (ky.#options.onDownloadProgress) {
|
|
846
1123
|
if (typeof ky.#options.onDownloadProgress !== 'function') {
|
|
@@ -855,39 +1132,71 @@ class Ky {
|
|
|
855
1132
|
}
|
|
856
1133
|
return response;
|
|
857
1134
|
};
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
1135
|
+
const result = (async () => {
|
|
1136
|
+
try {
|
|
1137
|
+
return await function_();
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
// Non-Error throws (e.g., thrown strings) pass through unchanged
|
|
1141
|
+
if (!(error instanceof Error)) {
|
|
1142
|
+
throw error;
|
|
1143
|
+
}
|
|
1144
|
+
// Errors thrown by beforeRetry hooks must propagate unchanged.
|
|
1145
|
+
if (ky.#beforeRetryHookErrors.has(error)) {
|
|
1146
|
+
throw error;
|
|
1147
|
+
}
|
|
1148
|
+
let processedError = error;
|
|
1149
|
+
for (const hook of ky.#options.hooks.beforeError) {
|
|
1150
|
+
// `request` is the current failing request. `options` intentionally remains the
|
|
1151
|
+
// stable normalized Ky options snapshot for the same reason as `HTTPError` above.
|
|
1152
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1153
|
+
const hookResult = await hook({
|
|
1154
|
+
request: ky.request,
|
|
1155
|
+
options: ky.#getNormalizedOptions(),
|
|
1156
|
+
error: processedError,
|
|
1157
|
+
retryCount: ky.#retryCount,
|
|
1158
|
+
});
|
|
1159
|
+
// Only overwrite if the hook returns a valid Error instance.
|
|
1160
|
+
if (hookResult instanceof Error) {
|
|
1161
|
+
processedError = hookResult;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
throw processedError;
|
|
1165
|
+
}
|
|
1166
|
+
finally {
|
|
1167
|
+
const originalRequest = ky.#originalRequest;
|
|
1168
|
+
// Ignore cancellation errors from already-locked or already-consumed streams.
|
|
1169
|
+
ky.#cancelBody(originalRequest?.body ?? undefined);
|
|
1170
|
+
// Only cancel the current request body if it's distinct from the original (i.e. it was cloned for retries).
|
|
1171
|
+
if (ky.request !== originalRequest) {
|
|
1172
|
+
ky.#cancelBody(ky.request.body ?? undefined);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
})();
|
|
867
1176
|
for (const [type, mimeType] of Object.entries(responseTypes)) {
|
|
868
1177
|
// Only expose `.bytes()` when the environment implements it.
|
|
869
1178
|
if (type === 'bytes'
|
|
870
1179
|
&& typeof globalThis.Response?.prototype?.bytes !== 'function') {
|
|
871
1180
|
continue;
|
|
872
1181
|
}
|
|
873
|
-
result[type] = async () => {
|
|
1182
|
+
result[type] = async (schema) => {
|
|
874
1183
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
875
1184
|
ky.request.headers.set('accept', ky.request.headers.get('accept') || mimeType);
|
|
876
1185
|
const response = await result;
|
|
877
|
-
if (type
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
if (
|
|
883
|
-
return
|
|
884
|
-
}
|
|
885
|
-
if (options.parseJson) {
|
|
886
|
-
return options.parseJson(text);
|
|
1186
|
+
if (type !== 'json') {
|
|
1187
|
+
return response[type]();
|
|
1188
|
+
}
|
|
1189
|
+
const text = await response.text();
|
|
1190
|
+
if (text === '') {
|
|
1191
|
+
if (schema !== undefined) {
|
|
1192
|
+
return validateJsonWithSchema(undefined, schema);
|
|
887
1193
|
}
|
|
888
1194
|
return JSON.parse(text);
|
|
889
1195
|
}
|
|
890
|
-
|
|
1196
|
+
const jsonValue = initHookOptions.parseJson
|
|
1197
|
+
? await initHookOptions.parseJson(text, { request: ky.#getResponseRequest(response), response })
|
|
1198
|
+
: JSON.parse(text);
|
|
1199
|
+
return schema === undefined ? jsonValue : validateJsonWithSchema(jsonValue, schema);
|
|
891
1200
|
};
|
|
892
1201
|
}
|
|
893
1202
|
return result;
|
|
@@ -908,44 +1217,55 @@ class Ky {
|
|
|
908
1217
|
#options;
|
|
909
1218
|
#originalRequest;
|
|
910
1219
|
#userProvidedAbortSignal;
|
|
1220
|
+
#beforeRetryHookErrors = new WeakSet();
|
|
911
1221
|
#cachedNormalizedOptions;
|
|
1222
|
+
#startTime;
|
|
1223
|
+
#returnedResponseFromBeforeRetryHook = false;
|
|
1224
|
+
#responseRequests = new WeakMap();
|
|
912
1225
|
// eslint-disable-next-line complexity
|
|
913
1226
|
constructor(input, options = {}) {
|
|
914
1227
|
this.#input = input;
|
|
1228
|
+
if (Object.hasOwn(options, 'prefixUrl')) {
|
|
1229
|
+
throw new Error(prefixUrlRenamedErrorMessage);
|
|
1230
|
+
}
|
|
915
1231
|
this.#options = {
|
|
916
1232
|
...options,
|
|
917
1233
|
headers: mergeHeaders(this.#input.headers, options.headers),
|
|
918
|
-
hooks: mergeHooks({
|
|
919
|
-
beforeRequest: [],
|
|
920
|
-
beforeRetry: [],
|
|
921
|
-
beforeError: [],
|
|
922
|
-
afterResponse: [],
|
|
923
|
-
}, options.hooks),
|
|
1234
|
+
hooks: mergeHooks({}, options.hooks),
|
|
924
1235
|
method: normalizeRequestMethod(options.method ?? this.#input.method ?? 'GET'),
|
|
925
1236
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
926
|
-
|
|
1237
|
+
prefix: String(options.prefix || ''),
|
|
927
1238
|
retry: normalizeRetryOptions(options.retry),
|
|
928
1239
|
throwHttpErrors: options.throwHttpErrors ?? true,
|
|
929
1240
|
timeout: options.timeout ?? 10_000,
|
|
1241
|
+
totalTimeout: options.totalTimeout ?? false,
|
|
930
1242
|
fetch: options.fetch ?? globalThis.fetch.bind(globalThis),
|
|
931
1243
|
context: options.context ?? {},
|
|
932
1244
|
};
|
|
933
1245
|
if (typeof this.#input !== 'string' && !(this.#input instanceof URL || this.#input instanceof globalThis.Request)) {
|
|
934
1246
|
throw new TypeError('`input` must be a string, URL, or Request');
|
|
935
1247
|
}
|
|
936
|
-
if (
|
|
937
|
-
if (this.#
|
|
938
|
-
|
|
1248
|
+
if (typeof this.#input === 'string') {
|
|
1249
|
+
if (this.#options.prefix) {
|
|
1250
|
+
const normalizedPrefix = this.#options.prefix.replace(/\/+$/, '');
|
|
1251
|
+
const normalizedInput = this.#input.replace(/^\/+/, '');
|
|
1252
|
+
this.#input = `${normalizedPrefix}/${normalizedInput}`;
|
|
939
1253
|
}
|
|
940
|
-
if (
|
|
941
|
-
|
|
1254
|
+
if (this.#options.baseUrl) {
|
|
1255
|
+
let absoluteInput;
|
|
1256
|
+
try {
|
|
1257
|
+
absoluteInput = new URL(this.#input);
|
|
1258
|
+
}
|
|
1259
|
+
catch { }
|
|
1260
|
+
if (!absoluteInput) {
|
|
1261
|
+
this.#input = new URL(this.#input, (new Request(this.#options.baseUrl)).url);
|
|
1262
|
+
}
|
|
942
1263
|
}
|
|
943
|
-
this.#input = this.#options.prefixUrl + this.#input;
|
|
944
1264
|
}
|
|
945
1265
|
if (supportsAbortController && supportsAbortSignal) {
|
|
946
1266
|
this.#userProvidedAbortSignal = this.#options.signal ?? this.#input.signal;
|
|
947
1267
|
this.#abortController = new globalThis.AbortController();
|
|
948
|
-
this.#options.signal = this.#
|
|
1268
|
+
this.#options.signal = this.#createManagedSignal();
|
|
949
1269
|
}
|
|
950
1270
|
if (supportsRequestStreams) {
|
|
951
1271
|
// @ts-expect-error - Types are outdated.
|
|
@@ -965,29 +1285,45 @@ class Ky {
|
|
|
965
1285
|
}
|
|
966
1286
|
this.request = new globalThis.Request(this.#input, this.#options);
|
|
967
1287
|
if (hasSearchParameters(this.#options.searchParams)) {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1288
|
+
const url = new URL(this.request.url);
|
|
1289
|
+
if (typeof this.#options.searchParams === 'string') {
|
|
1290
|
+
const stringSearchParameters = this.#options.searchParams.replace(/^\?/, '');
|
|
1291
|
+
if (stringSearchParameters !== '') {
|
|
1292
|
+
url.search = url.search ? `${url.search}&${stringSearchParameters}` : `?${stringSearchParameters}`;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
else {
|
|
1296
|
+
const optionsSearchParameters = new URLSearchParams(Ky.#normalizeSearchParams(this.#options.searchParams));
|
|
1297
|
+
for (const [key, value] of optionsSearchParameters.entries()) {
|
|
1298
|
+
url.searchParams.append(key, value);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
if (this.#options.searchParams
|
|
1302
|
+
&& typeof this.#options.searchParams === 'object'
|
|
1303
|
+
&& !Array.isArray(this.#options.searchParams)
|
|
1304
|
+
&& !(this.#options.searchParams instanceof URLSearchParams)) {
|
|
1305
|
+
for (const [key, value] of Object.entries(this.#options.searchParams)) {
|
|
1306
|
+
if (value === undefined) {
|
|
1307
|
+
url.searchParams.delete(key);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
const deleted = this.#options.searchParams?.[deletedParametersSymbol];
|
|
1312
|
+
if (deleted) {
|
|
1313
|
+
for (const key of deleted) {
|
|
1314
|
+
url.searchParams.delete(key);
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
975
1317
|
// Recreate request with the updated URL. We already have all options in this.#options, including duplex.
|
|
976
1318
|
this.request = new globalThis.Request(url, this.#options);
|
|
977
1319
|
}
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
if (typeof this.#options.onUploadProgress !== 'function') {
|
|
981
|
-
throw new TypeError('The `onUploadProgress` option must be a function');
|
|
982
|
-
}
|
|
983
|
-
if (!supportsRequestStreams) {
|
|
984
|
-
throw new Error('Request streams are not supported in your environment. The `duplex` option for `Request` is not available.');
|
|
985
|
-
}
|
|
986
|
-
this.request = this.#wrapRequestWithUploadProgress(this.request, this.#options.body ?? undefined);
|
|
1320
|
+
if (this.#options.onUploadProgress && typeof this.#options.onUploadProgress !== 'function') {
|
|
1321
|
+
throw new TypeError('The `onUploadProgress` option must be a function');
|
|
987
1322
|
}
|
|
1323
|
+
this.#startTime = typeof this.#options.totalTimeout === 'number' ? this.#getCurrentTime() : undefined;
|
|
988
1324
|
}
|
|
989
1325
|
#calculateDelay() {
|
|
990
|
-
const retryDelay = this.#options.retry.delay(this.#retryCount);
|
|
1326
|
+
const retryDelay = this.#options.retry.delay(this.#retryCount + 1);
|
|
991
1327
|
let jitteredDelay = retryDelay;
|
|
992
1328
|
if (this.#options.retry.jitter === true) {
|
|
993
1329
|
jitteredDelay = Math.random() * retryDelay;
|
|
@@ -998,13 +1334,10 @@ class Ky {
|
|
|
998
1334
|
jitteredDelay = retryDelay;
|
|
999
1335
|
}
|
|
1000
1336
|
}
|
|
1001
|
-
|
|
1002
|
-
const backoffLimit = this.#options.retry.backoffLimit ?? Number.POSITIVE_INFINITY;
|
|
1003
|
-
return Math.min(backoffLimit, jitteredDelay);
|
|
1337
|
+
return Math.min(this.#options.retry.backoffLimit, jitteredDelay);
|
|
1004
1338
|
}
|
|
1005
1339
|
async #calculateRetryDelay(error) {
|
|
1006
|
-
this.#retryCount
|
|
1007
|
-
if (this.#retryCount > this.#options.retry.limit) {
|
|
1340
|
+
if (this.#retryCount >= this.#options.retry.limit) {
|
|
1008
1341
|
throw error;
|
|
1009
1342
|
}
|
|
1010
1343
|
// Wrap non-Error throws to ensure consistent error handling
|
|
@@ -1017,9 +1350,9 @@ class Ky {
|
|
|
1017
1350
|
if (!this.#options.retry.methods.includes(this.request.method.toLowerCase())) {
|
|
1018
1351
|
throw error;
|
|
1019
1352
|
}
|
|
1020
|
-
// User-provided shouldRetry function takes precedence over
|
|
1353
|
+
// User-provided shouldRetry function takes precedence over default checks (retryOnTimeout, status codes, etc.)
|
|
1021
1354
|
if (this.#options.retry.shouldRetry !== undefined) {
|
|
1022
|
-
const result = await this.#options.retry.shouldRetry({ error: errorObject, retryCount: this.#retryCount });
|
|
1355
|
+
const result = await this.#options.retry.shouldRetry({ error: errorObject, retryCount: this.#retryCount + 1 });
|
|
1023
1356
|
// Strict boolean checking - only exact true/false are handled specially
|
|
1024
1357
|
if (result === false) {
|
|
1025
1358
|
throw error;
|
|
@@ -1031,8 +1364,11 @@ class Ky {
|
|
|
1031
1364
|
// If undefined or any other value, fall through to default behavior
|
|
1032
1365
|
}
|
|
1033
1366
|
// Default timeout behavior
|
|
1034
|
-
if (isTimeoutError(error)
|
|
1035
|
-
|
|
1367
|
+
if (isTimeoutError(error)) {
|
|
1368
|
+
if (!this.#options.retry.retryOnTimeout) {
|
|
1369
|
+
throw error;
|
|
1370
|
+
}
|
|
1371
|
+
return this.#calculateDelay();
|
|
1036
1372
|
}
|
|
1037
1373
|
if (isHTTPError(error)) {
|
|
1038
1374
|
if (!this.#options.retry.statusCodes.includes(error.response.status)) {
|
|
@@ -1052,22 +1388,152 @@ class Ky {
|
|
|
1052
1388
|
// A large number is treated as a timestamp (fixed threshold protects against clock skew)
|
|
1053
1389
|
after -= Date.now();
|
|
1054
1390
|
}
|
|
1055
|
-
|
|
1391
|
+
if (!Number.isFinite(after)) {
|
|
1392
|
+
return Math.min(this.#options.retry.maxRetryAfter, this.#calculateDelay());
|
|
1393
|
+
}
|
|
1394
|
+
after = Math.max(0, after);
|
|
1056
1395
|
// Don't apply jitter when server provides explicit retry timing
|
|
1057
|
-
return
|
|
1396
|
+
return Math.min(this.#options.retry.maxRetryAfter, after);
|
|
1058
1397
|
}
|
|
1059
1398
|
if (error.response.status === 413) {
|
|
1060
1399
|
throw error;
|
|
1061
1400
|
}
|
|
1401
|
+
return this.#calculateDelay();
|
|
1402
|
+
}
|
|
1403
|
+
// Only retry known retriable error types. Unknown errors (e.g., programming bugs) are not retried.
|
|
1404
|
+
if (!isNetworkError(error)) {
|
|
1405
|
+
throw error;
|
|
1062
1406
|
}
|
|
1063
1407
|
return this.#calculateDelay();
|
|
1064
1408
|
}
|
|
1065
1409
|
#decorateResponse(response) {
|
|
1410
|
+
const request = this.#getResponseRequest(response);
|
|
1066
1411
|
if (this.#options.parseJson) {
|
|
1067
|
-
response.json = async () =>
|
|
1412
|
+
response.json = async () => {
|
|
1413
|
+
const text = await response.text();
|
|
1414
|
+
if (text === '') {
|
|
1415
|
+
return JSON.parse(text);
|
|
1416
|
+
}
|
|
1417
|
+
return this.#options.parseJson(text, { request, response });
|
|
1418
|
+
};
|
|
1068
1419
|
}
|
|
1069
1420
|
return response;
|
|
1070
1421
|
}
|
|
1422
|
+
async #getResponseData(response) {
|
|
1423
|
+
// Even with request timeouts disabled, bound error-body reads so retries and error propagation
|
|
1424
|
+
// cannot be stalled indefinitely by never-ending response streams.
|
|
1425
|
+
const text = await this.#readResponseText(response, this.#getErrorDataTimeout());
|
|
1426
|
+
if (text === timedOutResponseData) {
|
|
1427
|
+
this.#throwIfTotalTimeoutExhausted();
|
|
1428
|
+
return undefined;
|
|
1429
|
+
}
|
|
1430
|
+
if (!text) {
|
|
1431
|
+
return undefined;
|
|
1432
|
+
}
|
|
1433
|
+
if (!this.#isJsonContentType(response.headers.get('content-type') ?? '')) {
|
|
1434
|
+
return text;
|
|
1435
|
+
}
|
|
1436
|
+
const data = await this.#parseJson(text, response, this.#getErrorDataTimeout(), this.#getResponseRequest(response));
|
|
1437
|
+
if (data === timedOutResponseData) {
|
|
1438
|
+
this.#throwIfTotalTimeoutExhausted();
|
|
1439
|
+
return undefined;
|
|
1440
|
+
}
|
|
1441
|
+
return data;
|
|
1442
|
+
}
|
|
1443
|
+
#getErrorDataTimeout() {
|
|
1444
|
+
const errorDataTimeout = this.#options.timeout === false ? 10_000 : this.#options.timeout;
|
|
1445
|
+
const remainingTotal = this.#getRemainingTotalTimeout();
|
|
1446
|
+
if (remainingTotal === undefined) {
|
|
1447
|
+
return errorDataTimeout;
|
|
1448
|
+
}
|
|
1449
|
+
if (remainingTotal <= 0) {
|
|
1450
|
+
throw new TimeoutError(this.request);
|
|
1451
|
+
}
|
|
1452
|
+
return Math.min(errorDataTimeout, remainingTotal);
|
|
1453
|
+
}
|
|
1454
|
+
#isJsonContentType(contentType) {
|
|
1455
|
+
// Match JSON subtypes like `json`, `problem+json`, and `vnd.api+json`.
|
|
1456
|
+
const mimeType = (contentType.split(';', 1)[0] ?? '').trim().toLowerCase();
|
|
1457
|
+
return /\/(?:.*[.+-])?json$/.test(mimeType);
|
|
1458
|
+
}
|
|
1459
|
+
async #readResponseText(response, timeoutMs) {
|
|
1460
|
+
const { body } = response;
|
|
1461
|
+
if (!body) {
|
|
1462
|
+
try {
|
|
1463
|
+
return await response.text();
|
|
1464
|
+
}
|
|
1465
|
+
catch {
|
|
1466
|
+
return undefined;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
let reader;
|
|
1470
|
+
try {
|
|
1471
|
+
reader = body.getReader();
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
// Another consumer already locked the stream.
|
|
1475
|
+
return undefined;
|
|
1476
|
+
}
|
|
1477
|
+
const decoder = createTextDecoder(response.headers.get('content-type') ?? '');
|
|
1478
|
+
const chunks = [];
|
|
1479
|
+
let totalBytes = 0;
|
|
1480
|
+
const readAll = (async () => {
|
|
1481
|
+
try {
|
|
1482
|
+
for (;;) {
|
|
1483
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1484
|
+
const { done, value } = await reader.read();
|
|
1485
|
+
if (done) {
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
totalBytes += value.byteLength;
|
|
1489
|
+
if (totalBytes > maxErrorResponseBodySize) {
|
|
1490
|
+
void reader.cancel().catch(() => undefined);
|
|
1491
|
+
return undefined;
|
|
1492
|
+
}
|
|
1493
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
catch {
|
|
1497
|
+
return undefined;
|
|
1498
|
+
}
|
|
1499
|
+
chunks.push(decoder.decode());
|
|
1500
|
+
return chunks.join('');
|
|
1501
|
+
})();
|
|
1502
|
+
const timeoutPromise = new Promise(resolve => {
|
|
1503
|
+
const timeoutId = setTimeout(() => {
|
|
1504
|
+
resolve(timedOutResponseData);
|
|
1505
|
+
}, timeoutMs);
|
|
1506
|
+
void readAll.finally(() => {
|
|
1507
|
+
clearTimeout(timeoutId);
|
|
1508
|
+
});
|
|
1509
|
+
});
|
|
1510
|
+
const result = await Promise.race([readAll, timeoutPromise]);
|
|
1511
|
+
if (result === timedOutResponseData) {
|
|
1512
|
+
void reader.cancel().catch(() => undefined);
|
|
1513
|
+
}
|
|
1514
|
+
return result;
|
|
1515
|
+
}
|
|
1516
|
+
async #parseJson(text, response, timeoutMs, request) {
|
|
1517
|
+
let timeoutId;
|
|
1518
|
+
try {
|
|
1519
|
+
return await Promise.race([
|
|
1520
|
+
Promise.resolve().then(() => this.#options.parseJson
|
|
1521
|
+
? this.#options.parseJson(text, { request, response })
|
|
1522
|
+
: JSON.parse(text)),
|
|
1523
|
+
new Promise(resolve => {
|
|
1524
|
+
timeoutId = setTimeout(() => {
|
|
1525
|
+
resolve(timedOutResponseData);
|
|
1526
|
+
}, timeoutMs);
|
|
1527
|
+
}),
|
|
1528
|
+
]);
|
|
1529
|
+
}
|
|
1530
|
+
catch {
|
|
1531
|
+
return undefined;
|
|
1532
|
+
}
|
|
1533
|
+
finally {
|
|
1534
|
+
clearTimeout(timeoutId);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1071
1537
|
#cancelBody(body) {
|
|
1072
1538
|
if (!body) {
|
|
1073
1539
|
return;
|
|
@@ -1079,90 +1545,224 @@ class Ky {
|
|
|
1079
1545
|
// Ignore cancellation failures from already-locked or already-consumed streams.
|
|
1080
1546
|
this.#cancelBody(response.body ?? undefined);
|
|
1081
1547
|
}
|
|
1548
|
+
#createManagedSignal() {
|
|
1549
|
+
return this.#userProvidedAbortSignal
|
|
1550
|
+
? AbortSignal.any([this.#userProvidedAbortSignal, this.#abortController.signal])
|
|
1551
|
+
: this.#abortController.signal;
|
|
1552
|
+
}
|
|
1553
|
+
#throwIfTotalTimeoutExhausted() {
|
|
1554
|
+
const remaining = this.#getRemainingTotalTimeout();
|
|
1555
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
1556
|
+
throw new TimeoutError(this.request);
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
async #runBeforeRequestHooks() {
|
|
1560
|
+
for (const hook of this.#options.hooks.beforeRequest) {
|
|
1561
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1562
|
+
const result = await hook({
|
|
1563
|
+
request: this.request,
|
|
1564
|
+
options: this.#getNormalizedOptions(),
|
|
1565
|
+
retryCount: 0,
|
|
1566
|
+
});
|
|
1567
|
+
if (result instanceof Response) {
|
|
1568
|
+
return result;
|
|
1569
|
+
}
|
|
1570
|
+
if (result instanceof globalThis.Request) {
|
|
1571
|
+
this.#assignRequest(result);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return undefined;
|
|
1575
|
+
}
|
|
1576
|
+
async #runAfterResponseHooks(response) {
|
|
1577
|
+
const responseRequest = this.#getResponseRequest(response);
|
|
1578
|
+
for (const hook of this.#options.hooks.afterResponse) {
|
|
1579
|
+
// Clone the response before passing to hook so we can cancel it if needed
|
|
1580
|
+
const clonedResponse = this.#setResponseRequest(response.clone(), responseRequest);
|
|
1581
|
+
this.#decorateResponse(clonedResponse);
|
|
1582
|
+
let modifiedResponse;
|
|
1583
|
+
try {
|
|
1584
|
+
// eslint-disable-next-line no-await-in-loop
|
|
1585
|
+
modifiedResponse = await hook({
|
|
1586
|
+
request: this.request,
|
|
1587
|
+
options: this.#getNormalizedOptions(),
|
|
1588
|
+
response: clonedResponse,
|
|
1589
|
+
retryCount: this.#retryCount,
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
catch (error) {
|
|
1593
|
+
// Cancel both responses to prevent memory leaks when hook throws
|
|
1594
|
+
this.#cancelResponseBody(clonedResponse);
|
|
1595
|
+
this.#cancelResponseBody(response);
|
|
1596
|
+
throw error;
|
|
1597
|
+
}
|
|
1598
|
+
if (modifiedResponse instanceof RetryMarker) {
|
|
1599
|
+
// Cancel both the cloned response passed to the hook and the current response to prevent resource leaks (especially important in Deno/Bun).
|
|
1600
|
+
// Do not await cancellation since hooks can clone the response, leaving extra tee branches that keep cancel promises pending per the Streams spec.
|
|
1601
|
+
this.#cancelResponseBody(clonedResponse);
|
|
1602
|
+
this.#cancelResponseBody(response);
|
|
1603
|
+
throw new ForceRetryError(modifiedResponse.options);
|
|
1604
|
+
}
|
|
1605
|
+
// Determine which response to use going forward
|
|
1606
|
+
const nextResponse = this.#setResponseRequest(modifiedResponse instanceof globalThis.Response ? modifiedResponse : response, responseRequest);
|
|
1607
|
+
// Cancel any response bodies we won't use to prevent memory leaks.
|
|
1608
|
+
// Uses fire-and-forget since hooks may have cloned the response, creating tee branches that block cancellation.
|
|
1609
|
+
if (clonedResponse !== nextResponse) {
|
|
1610
|
+
this.#cancelResponseBody(clonedResponse);
|
|
1611
|
+
}
|
|
1612
|
+
if (response !== nextResponse) {
|
|
1613
|
+
this.#cancelResponseBody(response);
|
|
1614
|
+
}
|
|
1615
|
+
response = nextResponse;
|
|
1616
|
+
}
|
|
1617
|
+
return response;
|
|
1618
|
+
}
|
|
1082
1619
|
async #retry(function_) {
|
|
1083
1620
|
try {
|
|
1084
1621
|
return await function_();
|
|
1085
1622
|
}
|
|
1086
1623
|
catch (error) {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1624
|
+
return this.#retryFromError(error, function_);
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
async #retryFromError(error, function_) {
|
|
1628
|
+
this.#returnedResponseFromBeforeRetryHook = false;
|
|
1629
|
+
const retryDelay = Math.min(await this.#calculateRetryDelay(error), maxSafeTimeout);
|
|
1630
|
+
const delayOptions = { signal: this.#userProvidedAbortSignal };
|
|
1631
|
+
const remainingTimeout = this.#getRemainingTotalTimeout();
|
|
1632
|
+
if (remainingTimeout !== undefined) {
|
|
1633
|
+
if (remainingTimeout <= 0) {
|
|
1634
|
+
throw new TimeoutError(this.request);
|
|
1090
1635
|
}
|
|
1091
|
-
//
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
if (error instanceof ForceRetryError && error.customRequest) {
|
|
1096
|
-
const managedRequest = this.#options.signal
|
|
1097
|
-
? new globalThis.Request(error.customRequest, { signal: this.#options.signal })
|
|
1098
|
-
: new globalThis.Request(error.customRequest);
|
|
1099
|
-
this.#assignRequest(managedRequest);
|
|
1636
|
+
// If waiting would consume all remaining budget, time out without starting another request.
|
|
1637
|
+
if (retryDelay >= remainingTimeout) {
|
|
1638
|
+
await delay(remainingTimeout, delayOptions);
|
|
1639
|
+
throw new TimeoutError(this.request);
|
|
1100
1640
|
}
|
|
1101
|
-
|
|
1641
|
+
}
|
|
1642
|
+
// Only use user-provided signal for delay, not our internal abortController
|
|
1643
|
+
await delay(retryDelay, delayOptions);
|
|
1644
|
+
this.#throwIfTotalTimeoutExhausted();
|
|
1645
|
+
// Apply custom request from forced retry before beforeRetry hooks
|
|
1646
|
+
// Ensure the custom request has the correct managed signal for timeouts and user aborts
|
|
1647
|
+
if (error instanceof ForceRetryError && error.customRequest) {
|
|
1648
|
+
this.#assignRequest(new globalThis.Request(error.customRequest, this.#options.signal ? { signal: this.#options.signal } : undefined));
|
|
1649
|
+
}
|
|
1650
|
+
for (const hook of this.#options.hooks.beforeRetry) {
|
|
1651
|
+
let hookResult;
|
|
1652
|
+
try {
|
|
1102
1653
|
// eslint-disable-next-line no-await-in-loop
|
|
1103
|
-
|
|
1654
|
+
hookResult = await hook({
|
|
1104
1655
|
request: this.request,
|
|
1105
1656
|
options: this.#getNormalizedOptions(),
|
|
1106
1657
|
error: error,
|
|
1107
|
-
retryCount: this.#retryCount,
|
|
1658
|
+
retryCount: this.#retryCount + 1,
|
|
1108
1659
|
});
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
if (hookResult instanceof globalThis.Response) {
|
|
1115
|
-
return hookResult;
|
|
1116
|
-
}
|
|
1117
|
-
// If `stop` is returned from the hook, the retry process is stopped
|
|
1118
|
-
if (hookResult === stop) {
|
|
1119
|
-
return;
|
|
1660
|
+
}
|
|
1661
|
+
catch (hookError) {
|
|
1662
|
+
// Preserve the original request error path (`throw error`) so beforeError hooks can still run.
|
|
1663
|
+
if (hookError instanceof Error && hookError !== error) {
|
|
1664
|
+
this.#beforeRetryHookErrors.add(hookError);
|
|
1120
1665
|
}
|
|
1666
|
+
throw hookError;
|
|
1667
|
+
}
|
|
1668
|
+
if (hookResult instanceof globalThis.Request) {
|
|
1669
|
+
this.#assignRequest(hookResult);
|
|
1670
|
+
break;
|
|
1671
|
+
}
|
|
1672
|
+
// If a Response is returned, use it and skip the retry
|
|
1673
|
+
if (hookResult instanceof globalThis.Response) {
|
|
1674
|
+
this.#returnedResponseFromBeforeRetryHook = true;
|
|
1675
|
+
this.#retryCount++;
|
|
1676
|
+
return hookResult;
|
|
1677
|
+
}
|
|
1678
|
+
// If `stop` is returned from the hook, the retry process is stopped
|
|
1679
|
+
if (hookResult === stop) {
|
|
1680
|
+
return;
|
|
1121
1681
|
}
|
|
1122
|
-
return this.#retry(function_);
|
|
1123
1682
|
}
|
|
1683
|
+
this.#throwIfTotalTimeoutExhausted();
|
|
1684
|
+
this.#retryCount++;
|
|
1685
|
+
return this.#retry(function_);
|
|
1686
|
+
}
|
|
1687
|
+
#consumeReturnedResponseFromBeforeRetryHook() {
|
|
1688
|
+
const value = this.#returnedResponseFromBeforeRetryHook;
|
|
1689
|
+
this.#returnedResponseFromBeforeRetryHook = false;
|
|
1690
|
+
return value;
|
|
1124
1691
|
}
|
|
1125
1692
|
async #fetch() {
|
|
1126
1693
|
// Reset abortController if it was aborted (happens on timeout retry)
|
|
1127
1694
|
if (this.#abortController?.signal.aborted) {
|
|
1128
1695
|
this.#abortController = new globalThis.AbortController();
|
|
1129
|
-
this.#options.signal = this.#
|
|
1696
|
+
this.#options.signal = this.#createManagedSignal();
|
|
1130
1697
|
// Recreate request with new signal
|
|
1131
1698
|
this.request = new globalThis.Request(this.request, { signal: this.#options.signal });
|
|
1132
1699
|
}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1700
|
+
const nonRequestOptions = findUnknownOptions(this.request, this.#options);
|
|
1701
|
+
const retryRequest = this.#options.retry.limit > 0 ? this.request.clone() : undefined;
|
|
1702
|
+
const request = this.#wrapRequestWithUploadProgress(this.request, this.#options.body ?? undefined);
|
|
1703
|
+
// Cloning is done here to prepare in advance for retries.
|
|
1704
|
+
// Skip cloning when retries are disabled - cloning a streaming body calls ReadableStream#tee()
|
|
1705
|
+
// which buffers the entire stream in memory, causing excessive memory usage for large uploads.
|
|
1706
|
+
this.#originalRequest = request;
|
|
1707
|
+
if (retryRequest) {
|
|
1708
|
+
this.request = retryRequest;
|
|
1709
|
+
}
|
|
1710
|
+
try {
|
|
1711
|
+
const remainingTotal = this.#getRemainingTotalTimeout();
|
|
1712
|
+
if (remainingTotal !== undefined && remainingTotal <= 0) {
|
|
1713
|
+
throw new TimeoutError(this.request);
|
|
1138
1714
|
}
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1715
|
+
const effectiveTimeout = this.#options.timeout === false
|
|
1716
|
+
? remainingTotal
|
|
1717
|
+
: (remainingTotal === undefined
|
|
1718
|
+
? this.#options.timeout
|
|
1719
|
+
: Math.min(this.#options.timeout, remainingTotal));
|
|
1720
|
+
const response = effectiveTimeout === undefined
|
|
1721
|
+
? await this.#options.fetch(request, nonRequestOptions)
|
|
1722
|
+
: await timeout(request, nonRequestOptions, this.#abortController, {
|
|
1723
|
+
timeout: effectiveTimeout,
|
|
1724
|
+
fetch: this.#options.fetch,
|
|
1725
|
+
});
|
|
1726
|
+
return this.#setResponseRequest(response, request);
|
|
1727
|
+
}
|
|
1728
|
+
catch (error) {
|
|
1729
|
+
if (isRawNetworkError(error)) {
|
|
1730
|
+
throw new NetworkError(this.request, { cause: error });
|
|
1142
1731
|
}
|
|
1732
|
+
throw error;
|
|
1143
1733
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
this.#
|
|
1147
|
-
|
|
1148
|
-
if (this.#options.timeout === false) {
|
|
1149
|
-
return this.#options.fetch(this.#originalRequest, nonRequestOptions);
|
|
1734
|
+
}
|
|
1735
|
+
#getRemainingTotalTimeout() {
|
|
1736
|
+
if (this.#startTime === undefined) {
|
|
1737
|
+
return undefined;
|
|
1150
1738
|
}
|
|
1151
|
-
|
|
1739
|
+
const elapsed = this.#getCurrentTime() - this.#startTime;
|
|
1740
|
+
return Math.max(0, this.#options.totalTimeout - elapsed);
|
|
1741
|
+
}
|
|
1742
|
+
#getCurrentTime() {
|
|
1743
|
+
return globalThis.performance?.now() ?? Date.now();
|
|
1152
1744
|
}
|
|
1153
1745
|
#getNormalizedOptions() {
|
|
1154
1746
|
if (!this.#cachedNormalizedOptions) {
|
|
1155
|
-
|
|
1747
|
+
// Exclude Ky-specific options that are not part of `RequestInit`.
|
|
1748
|
+
const { hooks, json, parseJson, stringifyJson, searchParams, timeout, totalTimeout, throwHttpErrors, fetch, ...normalizedOptions } = this.#options;
|
|
1156
1749
|
this.#cachedNormalizedOptions = Object.freeze(normalizedOptions);
|
|
1157
1750
|
}
|
|
1158
1751
|
return this.#cachedNormalizedOptions;
|
|
1159
1752
|
}
|
|
1160
1753
|
#assignRequest(request) {
|
|
1161
1754
|
this.#cachedNormalizedOptions = undefined;
|
|
1162
|
-
this.request =
|
|
1755
|
+
this.request = request;
|
|
1756
|
+
}
|
|
1757
|
+
#getResponseRequest(response) {
|
|
1758
|
+
return this.#responseRequests.get(response) ?? this.request;
|
|
1759
|
+
}
|
|
1760
|
+
#setResponseRequest(response, request) {
|
|
1761
|
+
this.#responseRequests.set(response, request);
|
|
1762
|
+
return response;
|
|
1163
1763
|
}
|
|
1164
1764
|
#wrapRequestWithUploadProgress(request, originalBody) {
|
|
1165
|
-
if (!this.#options.onUploadProgress || !request.body) {
|
|
1765
|
+
if (!this.#options.onUploadProgress || !request.body || !supportsRequestStreams) {
|
|
1166
1766
|
return request;
|
|
1167
1767
|
}
|
|
1168
1768
|
return streamRequest(request, this.#options.onUploadProgress, originalBody ?? this.#options.body ?? undefined);
|
|
@@ -1196,185 +1796,96 @@ var distribution = /*#__PURE__*/Object.freeze({
|
|
|
1196
1796
|
__proto__: null,
|
|
1197
1797
|
ForceRetryError: ForceRetryError,
|
|
1198
1798
|
HTTPError: HTTPError,
|
|
1799
|
+
KyError: KyError,
|
|
1800
|
+
NetworkError: NetworkError,
|
|
1801
|
+
SchemaValidationError: SchemaValidationError,
|
|
1199
1802
|
TimeoutError: TimeoutError,
|
|
1200
1803
|
default: ky,
|
|
1201
1804
|
isForceRetryError: isForceRetryError,
|
|
1202
1805
|
isHTTPError: isHTTPError,
|
|
1203
1806
|
isKyError: isKyError,
|
|
1204
|
-
|
|
1807
|
+
isNetworkError: isNetworkError,
|
|
1808
|
+
isTimeoutError: isTimeoutError,
|
|
1809
|
+
replaceOption: replaceOption
|
|
1205
1810
|
});
|
|
1206
1811
|
|
|
1207
1812
|
var require$$1 = /*@__PURE__*/getAugmentedNamespace(distribution);
|
|
1208
1813
|
|
|
1209
|
-
const VERSION = '0.
|
|
1814
|
+
const VERSION = '0.16.0';
|
|
1210
1815
|
|
|
1211
1816
|
var constants = {
|
|
1212
1817
|
VERSION,
|
|
1213
1818
|
USER_AGENT: `mql/${VERSION}`,
|
|
1214
1819
|
/**
|
|
1215
|
-
*
|
|
1820
|
+
* Based on Ky default retry status codes, excluding 429:
|
|
1821
|
+
* https://github.com/sindresorhus/ky/blob/main/source/core/constants.ts
|
|
1216
1822
|
*/
|
|
1217
|
-
RETRY_STATUS_CODES: [408, 413, 500, 502, 503, 504, 521, 522, 524]
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
};
|
|
1224
|
-
|
|
1225
|
-
const isObject = input => input !== null && typeof input === 'object';
|
|
1226
|
-
|
|
1227
|
-
const isBuffer = input =>
|
|
1228
|
-
input != null &&
|
|
1229
|
-
input.constructor != null &&
|
|
1230
|
-
typeof input.constructor.isBuffer === 'function' &&
|
|
1231
|
-
input.constructor.isBuffer(input);
|
|
1232
|
-
|
|
1233
|
-
const parseBody = (input, error, url) => {
|
|
1234
|
-
try {
|
|
1235
|
-
return JSON.parse(input)
|
|
1236
|
-
} catch (_) {
|
|
1237
|
-
const message = input || error.message;
|
|
1238
|
-
|
|
1239
|
-
return {
|
|
1240
|
-
status: 'error',
|
|
1241
|
-
data: { url: message },
|
|
1242
|
-
more: 'https://microlink.io/efatalclient',
|
|
1243
|
-
code: 'EFATALCLIENT',
|
|
1244
|
-
message,
|
|
1245
|
-
url
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
};
|
|
1249
|
-
|
|
1250
|
-
const isURL = url => {
|
|
1251
|
-
try {
|
|
1252
|
-
return /^https?:\/\//i.test(new URL(url).href)
|
|
1253
|
-
} catch (_) {
|
|
1254
|
-
return false
|
|
1255
|
-
}
|
|
1256
|
-
};
|
|
1257
|
-
|
|
1258
|
-
const factory = streamResponseType => ({
|
|
1259
|
-
VERSION,
|
|
1260
|
-
MicrolinkError,
|
|
1261
|
-
got,
|
|
1262
|
-
flatten
|
|
1263
|
-
}) => {
|
|
1264
|
-
const assertUrl = (url = '') => {
|
|
1265
|
-
if (!isURL(url)) {
|
|
1266
|
-
const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`;
|
|
1267
|
-
throw new MicrolinkError({
|
|
1268
|
-
status: 'fail',
|
|
1269
|
-
data: { url: message },
|
|
1270
|
-
more: 'https://microlink.io/einvalurlclient',
|
|
1271
|
-
code: 'EINVALURLCLIENT',
|
|
1272
|
-
message,
|
|
1273
|
-
url
|
|
1274
|
-
})
|
|
1275
|
-
}
|
|
1276
|
-
};
|
|
1277
|
-
|
|
1278
|
-
const mapRules = rules => {
|
|
1279
|
-
if (!isObject(rules)) return
|
|
1280
|
-
const flatRules = flatten(rules);
|
|
1281
|
-
return Object.keys(flatRules).reduce((acc, key) => {
|
|
1282
|
-
acc[`data.${key}`] = flatRules[key].toString();
|
|
1283
|
-
return acc
|
|
1284
|
-
}, {})
|
|
1285
|
-
};
|
|
1286
|
-
|
|
1287
|
-
const fetchFromApi = async (apiUrl, opts = {}) => {
|
|
1288
|
-
try {
|
|
1289
|
-
const response = await got(apiUrl, opts);
|
|
1290
|
-
return opts.responseType === streamResponseType
|
|
1291
|
-
? response
|
|
1292
|
-
: { ...response.body, response }
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
const { response = {} } = error;
|
|
1295
|
-
const {
|
|
1296
|
-
statusCode,
|
|
1297
|
-
body: rawBody,
|
|
1298
|
-
headers = {},
|
|
1299
|
-
url: uri = apiUrl
|
|
1300
|
-
} = response;
|
|
1301
|
-
const isBodyBuffer = isBuffer(rawBody);
|
|
1302
|
-
|
|
1303
|
-
const body =
|
|
1304
|
-
isObject(rawBody) && !isBodyBuffer
|
|
1305
|
-
? rawBody
|
|
1306
|
-
: parseBody(isBodyBuffer ? rawBody.toString() : rawBody, error, uri);
|
|
1307
|
-
|
|
1308
|
-
throw new MicrolinkError({
|
|
1309
|
-
...body,
|
|
1310
|
-
message: body.message,
|
|
1311
|
-
url: uri,
|
|
1312
|
-
statusCode,
|
|
1313
|
-
headers
|
|
1314
|
-
})
|
|
1315
|
-
}
|
|
1316
|
-
};
|
|
1317
|
-
|
|
1318
|
-
const getApiUrl = (
|
|
1319
|
-
url,
|
|
1320
|
-
{ data, apiKey, endpoint, ...opts } = {},
|
|
1321
|
-
{ responseType = 'json', headers: gotHeaders, ...gotOpts } = {}
|
|
1322
|
-
) => {
|
|
1323
|
-
const isPro = !!apiKey;
|
|
1324
|
-
const apiEndpoint = endpoint || ENDPOINT[isPro ? 'PRO' : 'FREE'];
|
|
1325
|
-
|
|
1326
|
-
const apiUrl = `${apiEndpoint}?${new URLSearchParams({
|
|
1327
|
-
url,
|
|
1328
|
-
...mapRules(data),
|
|
1329
|
-
...flatten(opts)
|
|
1330
|
-
}).toString()}`;
|
|
1331
|
-
|
|
1332
|
-
const headers = isPro
|
|
1333
|
-
? { ...gotHeaders, 'x-api-key': apiKey }
|
|
1334
|
-
: { ...gotHeaders };
|
|
1335
|
-
|
|
1336
|
-
if (opts.stream) {
|
|
1337
|
-
responseType = streamResponseType;
|
|
1338
|
-
}
|
|
1339
|
-
return [apiUrl, { ...gotOpts, responseType, headers }]
|
|
1340
|
-
};
|
|
1341
|
-
|
|
1342
|
-
const createMql = defaultOpts => async (url, opts, gotOpts) => {
|
|
1343
|
-
assertUrl(url);
|
|
1344
|
-
const [apiUrl, fetchOpts] = getApiUrl(url, opts, {
|
|
1345
|
-
...defaultOpts,
|
|
1346
|
-
...gotOpts
|
|
1347
|
-
});
|
|
1348
|
-
return fetchFromApi(apiUrl, fetchOpts)
|
|
1349
|
-
};
|
|
1350
|
-
|
|
1351
|
-
const mql = createMql();
|
|
1352
|
-
mql.extend = createMql;
|
|
1353
|
-
mql.MicrolinkError = MicrolinkError;
|
|
1354
|
-
mql.getApiUrl = getApiUrl;
|
|
1355
|
-
mql.fetchFromApi = fetchFromApi;
|
|
1356
|
-
mql.mapRules = mapRules;
|
|
1357
|
-
mql.version = VERSION;
|
|
1358
|
-
mql.stream = got.stream;
|
|
1359
|
-
|
|
1360
|
-
return mql
|
|
1823
|
+
RETRY_STATUS_CODES: [408, 413, 500, 502, 503, 504, 521, 522, 524],
|
|
1824
|
+
/**
|
|
1825
|
+
* Based on Ky default Retry-After status codes, excluding 429:
|
|
1826
|
+
* https://github.com/sindresorhus/ky/blob/main/source/core/constants.ts
|
|
1827
|
+
*/
|
|
1828
|
+
RETRY_AFTER_STATUS_CODES: [413, 503]
|
|
1361
1829
|
};
|
|
1362
1830
|
|
|
1363
|
-
var factory_1 = factory;
|
|
1364
|
-
|
|
1365
1831
|
(function (module) {
|
|
1366
1832
|
|
|
1367
1833
|
const { flattie: flatten } = dist;
|
|
1368
1834
|
const { default: ky } = require$$1;
|
|
1369
1835
|
|
|
1370
|
-
const {
|
|
1836
|
+
const {
|
|
1837
|
+
VERSION,
|
|
1838
|
+
USER_AGENT,
|
|
1839
|
+
RETRY_STATUS_CODES,
|
|
1840
|
+
RETRY_AFTER_STATUS_CODES
|
|
1841
|
+
} = constants;
|
|
1842
|
+
|
|
1843
|
+
const ENDPOINT = {
|
|
1844
|
+
FREE: 'https://api.microlink.io/',
|
|
1845
|
+
PRO: 'https://pro.microlink.io/'
|
|
1846
|
+
};
|
|
1847
|
+
|
|
1848
|
+
const STREAM_RESPONSE_TYPE = 'arrayBuffer';
|
|
1371
1849
|
|
|
1372
1850
|
const kyInstance = ky.extend({
|
|
1373
1851
|
headers: { 'user-agent': USER_AGENT },
|
|
1374
|
-
retry: {
|
|
1852
|
+
retry: {
|
|
1853
|
+
statusCodes: RETRY_STATUS_CODES,
|
|
1854
|
+
afterStatusCodes: RETRY_AFTER_STATUS_CODES
|
|
1855
|
+
}
|
|
1375
1856
|
});
|
|
1376
1857
|
|
|
1377
|
-
const
|
|
1858
|
+
const isObject = input => input !== null && typeof input === 'object';
|
|
1859
|
+
|
|
1860
|
+
const isBuffer = input =>
|
|
1861
|
+
typeof input?.constructor?.isBuffer === 'function' &&
|
|
1862
|
+
input.constructor.isBuffer(input);
|
|
1863
|
+
|
|
1864
|
+
const parseBody = (input, error, url) => {
|
|
1865
|
+
try {
|
|
1866
|
+
return JSON.parse(input)
|
|
1867
|
+
} catch (_) {
|
|
1868
|
+
const message = input || error.message;
|
|
1869
|
+
|
|
1870
|
+
return {
|
|
1871
|
+
status: 'error',
|
|
1872
|
+
data: { url: message },
|
|
1873
|
+
more: 'https://microlink.io/efatalclient',
|
|
1874
|
+
code: 'EFATALCLIENT',
|
|
1875
|
+
message,
|
|
1876
|
+
url
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
|
|
1881
|
+
const isURL = url => {
|
|
1882
|
+
try {
|
|
1883
|
+
const { protocol } = new URL(url);
|
|
1884
|
+
return protocol === 'http:' || protocol === 'https:'
|
|
1885
|
+
} catch (_) {
|
|
1886
|
+
return false
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1378
1889
|
|
|
1379
1890
|
class MicrolinkError extends Error {
|
|
1380
1891
|
constructor (props) {
|
|
@@ -1388,44 +1899,132 @@ var factory_1 = factory;
|
|
|
1388
1899
|
}
|
|
1389
1900
|
}
|
|
1390
1901
|
|
|
1391
|
-
const
|
|
1902
|
+
const assertUrl = (url = '') => {
|
|
1903
|
+
if (!isURL(url)) {
|
|
1904
|
+
const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`;
|
|
1905
|
+
throw new MicrolinkError({
|
|
1906
|
+
status: 'fail',
|
|
1907
|
+
data: { url: message },
|
|
1908
|
+
more: 'https://microlink.io/einvalurlclient',
|
|
1909
|
+
code: 'EINVALURLCLIENT',
|
|
1910
|
+
message,
|
|
1911
|
+
url
|
|
1912
|
+
})
|
|
1913
|
+
}
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
const mapRules = rules => {
|
|
1917
|
+
if (!isObject(rules)) return
|
|
1918
|
+
return Object.fromEntries(
|
|
1919
|
+
Object.entries(flatten(rules)).map(([key, value]) => [
|
|
1920
|
+
`data.${key}`,
|
|
1921
|
+
value.toString()
|
|
1922
|
+
])
|
|
1923
|
+
)
|
|
1924
|
+
};
|
|
1925
|
+
|
|
1926
|
+
const doFetch = async (apiUrl, { responseType, ...opts }) => {
|
|
1927
|
+
if (opts.timeout === undefined) opts.timeout = false;
|
|
1928
|
+
const response = await kyInstance(apiUrl, opts);
|
|
1929
|
+
const body = await response[responseType]();
|
|
1930
|
+
const { headers, status: statusCode } = response;
|
|
1931
|
+
return { url: response.url, body, headers, statusCode }
|
|
1932
|
+
};
|
|
1933
|
+
|
|
1934
|
+
const fetchFromApi = async (apiUrl, opts = {}) => {
|
|
1392
1935
|
try {
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
return { url: response.url, body, headers, statusCode }
|
|
1936
|
+
const response = await doFetch(apiUrl, opts);
|
|
1937
|
+
return opts.responseType === STREAM_RESPONSE_TYPE
|
|
1938
|
+
? response
|
|
1939
|
+
: { ...response.body, response }
|
|
1398
1940
|
} catch (error) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1941
|
+
const { response = {} } = error;
|
|
1942
|
+
const { statusCode: responseStatusCode, status } = response;
|
|
1943
|
+
const {
|
|
1944
|
+
body: rawBody,
|
|
1945
|
+
headers: responseHeaders,
|
|
1946
|
+
url: uri = apiUrl
|
|
1947
|
+
} = response;
|
|
1948
|
+
|
|
1949
|
+
const statusCode = responseStatusCode ?? status;
|
|
1950
|
+
const headers =
|
|
1951
|
+
typeof responseHeaders?.entries === 'function'
|
|
1952
|
+
? Object.fromEntries(responseHeaders.entries())
|
|
1953
|
+
: responseHeaders || {};
|
|
1954
|
+
|
|
1955
|
+
let bodyInput = error.data ?? rawBody;
|
|
1956
|
+
const isBodyReadableStream = typeof bodyInput?.getReader === 'function';
|
|
1957
|
+
|
|
1958
|
+
if (
|
|
1959
|
+
(bodyInput === undefined || isBodyReadableStream) &&
|
|
1960
|
+
typeof response.text === 'function'
|
|
1961
|
+
) {
|
|
1962
|
+
try {
|
|
1963
|
+
bodyInput = await response.text();
|
|
1964
|
+
} catch (_) {
|
|
1965
|
+
bodyInput = undefined;
|
|
1966
|
+
}
|
|
1413
1967
|
}
|
|
1414
|
-
|
|
1968
|
+
|
|
1969
|
+
const isBodyBuffer = isBuffer(bodyInput);
|
|
1970
|
+
const body =
|
|
1971
|
+
isObject(bodyInput) && !isBodyBuffer
|
|
1972
|
+
? bodyInput
|
|
1973
|
+
: parseBody(isBodyBuffer ? bodyInput.toString() : bodyInput, error, uri);
|
|
1974
|
+
|
|
1975
|
+
throw new MicrolinkError({
|
|
1976
|
+
...body,
|
|
1977
|
+
url: uri,
|
|
1978
|
+
statusCode,
|
|
1979
|
+
headers
|
|
1980
|
+
})
|
|
1415
1981
|
}
|
|
1416
1982
|
};
|
|
1417
1983
|
|
|
1418
|
-
|
|
1984
|
+
const getApiUrl = (
|
|
1985
|
+
url,
|
|
1986
|
+
{ data, apiKey, endpoint, ...opts } = {},
|
|
1987
|
+
{ responseType = 'json', headers: responseHeaders = {}, ...gotOpts } = {}
|
|
1988
|
+
) => {
|
|
1989
|
+
const isPro = !!apiKey;
|
|
1990
|
+
const apiEndpoint = endpoint || ENDPOINT[isPro ? 'PRO' : 'FREE'];
|
|
1419
1991
|
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1992
|
+
const apiUrl = `${apiEndpoint}?${new URLSearchParams({
|
|
1993
|
+
url,
|
|
1994
|
+
...mapRules(data),
|
|
1995
|
+
...flatten(opts)
|
|
1996
|
+
})}`;
|
|
1997
|
+
|
|
1998
|
+
const headers = isPro
|
|
1999
|
+
? { ...responseHeaders, 'x-api-key': apiKey }
|
|
2000
|
+
: responseHeaders;
|
|
2001
|
+
|
|
2002
|
+
if (opts.stream) responseType = STREAM_RESPONSE_TYPE;
|
|
2003
|
+
|
|
2004
|
+
return [apiUrl, { ...gotOpts, responseType, headers }]
|
|
2005
|
+
};
|
|
2006
|
+
|
|
2007
|
+
const createMql = defaultOpts => async (url, opts, gotOpts) => {
|
|
2008
|
+
assertUrl(url);
|
|
2009
|
+
const [apiUrl, fetchOpts] = getApiUrl(url, opts, {
|
|
2010
|
+
...defaultOpts,
|
|
2011
|
+
...gotOpts
|
|
2012
|
+
});
|
|
2013
|
+
return fetchFromApi(apiUrl, fetchOpts)
|
|
2014
|
+
};
|
|
2015
|
+
|
|
2016
|
+
const mql = createMql();
|
|
2017
|
+
|
|
2018
|
+
mql.extend = createMql;
|
|
2019
|
+
mql.MicrolinkError = MicrolinkError;
|
|
2020
|
+
mql.getApiUrl = getApiUrl;
|
|
2021
|
+
mql.fetchFromApi = fetchFromApi;
|
|
2022
|
+
mql.mapRules = mapRules;
|
|
2023
|
+
mql.version = VERSION;
|
|
2024
|
+
mql.stream = (...args) => kyInstance(...args).then(res => res.body);
|
|
1426
2025
|
|
|
1427
2026
|
module.exports = mql;
|
|
1428
|
-
module.exports.arrayBuffer = mql.extend({ responseType:
|
|
2027
|
+
module.exports.arrayBuffer = mql.extend({ responseType: STREAM_RESPONSE_TYPE });
|
|
1429
2028
|
module.exports.buffer = module.exports.arrayBuffer;
|
|
1430
2029
|
module.exports.extend = mql.extend;
|
|
1431
2030
|
module.exports.fetchFromApi = mql.fetchFromApi;
|