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