@microlink/mql 0.14.0 → 0.14.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/lightweight/index.js +859 -230
- package/lightweight/index.umd.js +859 -230
- package/package.json +2 -2
- package/src/node.js +11 -2
package/lightweight/index.umd.js
CHANGED
|
@@ -9,11 +9,15 @@
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
function getAugmentedNamespace(n) {
|
|
12
|
-
if (n
|
|
12
|
+
if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n;
|
|
13
13
|
var f = n.default;
|
|
14
14
|
if (typeof f == "function") {
|
|
15
15
|
var a = function a () {
|
|
16
|
-
|
|
16
|
+
var isInstance = false;
|
|
17
|
+
try {
|
|
18
|
+
isInstance = this instanceof a;
|
|
19
|
+
} catch {}
|
|
20
|
+
if (isInstance) {
|
|
17
21
|
return Reflect.construct(f, arguments, this.constructor);
|
|
18
22
|
}
|
|
19
23
|
return f.apply(this, arguments);
|
|
@@ -33,7 +37,7 @@
|
|
|
33
37
|
return a;
|
|
34
38
|
}
|
|
35
39
|
|
|
36
|
-
var lightweight
|
|
40
|
+
var lightweight = {exports: {}};
|
|
37
41
|
|
|
38
42
|
var dist = {};
|
|
39
43
|
|
|
@@ -71,7 +75,7 @@
|
|
|
71
75
|
options;
|
|
72
76
|
constructor(response, request, options) {
|
|
73
77
|
const code = (response.status || response.status === 0) ? response.status : '';
|
|
74
|
-
const title = response.statusText
|
|
78
|
+
const title = response.statusText ?? '';
|
|
75
79
|
const status = `${code} ${title}`.trim();
|
|
76
80
|
const reason = status ? `status code ${status}` : 'an unknown error';
|
|
77
81
|
super(`Request failed with ${reason}: ${request.method} ${request.url}`);
|
|
@@ -82,82 +86,54 @@
|
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
constructor(request) {
|
|
88
|
-
super(`Request timed out: ${request.method} ${request.url}`);
|
|
89
|
-
this.name = 'TimeoutError';
|
|
90
|
-
this.request = request;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
95
|
-
const isObject$1 = (value) => value !== null && typeof value === 'object';
|
|
89
|
+
/**
|
|
90
|
+
Wrapper for non-Error values that were thrown.
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
92
|
+
In JavaScript, any value can be thrown (not just Error instances). This class wraps such values to ensure consistent error handling.
|
|
93
|
+
*/
|
|
94
|
+
class NonError extends Error {
|
|
95
|
+
name = 'NonError';
|
|
96
|
+
value;
|
|
97
|
+
constructor(value) {
|
|
98
|
+
let message = 'Non-error value was thrown';
|
|
99
|
+
// Intentionally minimal as this error is just an edge-case.
|
|
100
|
+
try {
|
|
101
|
+
if (typeof value === 'string') {
|
|
102
|
+
message = value;
|
|
103
|
+
}
|
|
104
|
+
else if (value && typeof value === 'object' && 'message' in value && typeof value.message === 'string') {
|
|
105
|
+
message = value.message;
|
|
106
|
+
}
|
|
112
107
|
}
|
|
113
|
-
|
|
114
|
-
|
|
108
|
+
catch {
|
|
109
|
+
// Use default message if accessing properties throws
|
|
115
110
|
}
|
|
111
|
+
super(message);
|
|
112
|
+
this.value = value;
|
|
116
113
|
}
|
|
117
|
-
return result;
|
|
118
|
-
};
|
|
119
|
-
function newHookValue(original, incoming, property) {
|
|
120
|
-
return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
|
|
121
|
-
? []
|
|
122
|
-
: deepMerge(original[property] ?? [], incoming[property] ?? []);
|
|
123
114
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (isObject$1(value) && key in returnValue) {
|
|
145
|
-
value = deepMerge(returnValue[key], value);
|
|
146
|
-
}
|
|
147
|
-
returnValue = { ...returnValue, [key]: value };
|
|
148
|
-
}
|
|
149
|
-
if (isObject$1(source.hooks)) {
|
|
150
|
-
hooks = mergeHooks(hooks, source.hooks);
|
|
151
|
-
returnValue.hooks = hooks;
|
|
152
|
-
}
|
|
153
|
-
if (isObject$1(source.headers)) {
|
|
154
|
-
headers = mergeHeaders(headers, source.headers);
|
|
155
|
-
returnValue.headers = headers;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
Internal error used to signal a forced retry from afterResponse hooks.
|
|
118
|
+
This is thrown when a user returns ky.retry() from an afterResponse hook.
|
|
119
|
+
*/
|
|
120
|
+
class ForceRetryError extends Error {
|
|
121
|
+
name = 'ForceRetryError';
|
|
122
|
+
customDelay;
|
|
123
|
+
code;
|
|
124
|
+
customRequest;
|
|
125
|
+
constructor(options) {
|
|
126
|
+
// Runtime protection: wrap non-Error causes in NonError
|
|
127
|
+
// TypeScript type is Error for guidance, but JS users can pass anything
|
|
128
|
+
const cause = options?.cause
|
|
129
|
+
? (options.cause instanceof Error ? options.cause : new NonError(options.cause))
|
|
130
|
+
: undefined;
|
|
131
|
+
super(options?.code ? `Forced retry: ${options.code}` : 'Forced retry', cause ? { cause } : undefined);
|
|
132
|
+
this.customDelay = options?.delay;
|
|
133
|
+
this.code = options?.code;
|
|
134
|
+
this.customRequest = options?.request;
|
|
158
135
|
}
|
|
159
|
-
|
|
160
|
-
};
|
|
136
|
+
}
|
|
161
137
|
|
|
162
138
|
const supportsRequestStreams = (() => {
|
|
163
139
|
let duplexAccessed = false;
|
|
@@ -187,6 +163,7 @@
|
|
|
187
163
|
return duplexAccessed && !hasContentType;
|
|
188
164
|
})();
|
|
189
165
|
const supportsAbortController = typeof globalThis.AbortController === 'function';
|
|
166
|
+
const supportsAbortSignal = typeof globalThis.AbortSignal === 'function' && typeof globalThis.AbortSignal.any === 'function';
|
|
190
167
|
const supportsResponseStreams = typeof globalThis.ReadableStream === 'function';
|
|
191
168
|
const supportsFormData = typeof globalThis.FormData === 'function';
|
|
192
169
|
const requestMethods = ['get', 'post', 'put', 'patch', 'head', 'delete'];
|
|
@@ -196,10 +173,108 @@
|
|
|
196
173
|
formData: 'multipart/form-data',
|
|
197
174
|
arrayBuffer: '*/*',
|
|
198
175
|
blob: '*/*',
|
|
176
|
+
// Supported in modern Fetch implementations (for example, browsers and recent Node.js/undici).
|
|
177
|
+
// We still feature-check at runtime before exposing the shortcut.
|
|
178
|
+
bytes: '*/*',
|
|
199
179
|
};
|
|
200
180
|
// The maximum value of a 32bit int (see issue #117)
|
|
201
181
|
const maxSafeTimeout = 2_147_483_647;
|
|
182
|
+
// Size in bytes of a typical form boundary, used to help estimate upload size
|
|
183
|
+
const usualFormBoundarySize = new TextEncoder().encode('------WebKitFormBoundaryaxpyiPgbbPti10Rw').length;
|
|
202
184
|
const stop = Symbol('stop');
|
|
185
|
+
/**
|
|
186
|
+
Marker returned by ky.retry() to signal a forced retry from afterResponse hooks.
|
|
187
|
+
*/
|
|
188
|
+
class RetryMarker {
|
|
189
|
+
options;
|
|
190
|
+
constructor(options) {
|
|
191
|
+
this.options = options;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
Force a retry from an `afterResponse` hook.
|
|
196
|
+
|
|
197
|
+
This allows you to retry a request based on the response content, even if the response has a successful status code. The retry will respect the `retry.limit` option and skip the `shouldRetry` check. The forced retry is observable in `beforeRetry` hooks, where the error will be a `ForceRetryError`.
|
|
198
|
+
|
|
199
|
+
@param options - Optional configuration for the retry.
|
|
200
|
+
|
|
201
|
+
@example
|
|
202
|
+
```
|
|
203
|
+
import ky, {isForceRetryError} from 'ky';
|
|
204
|
+
|
|
205
|
+
const api = ky.extend({
|
|
206
|
+
hooks: {
|
|
207
|
+
afterResponse: [
|
|
208
|
+
async (request, options, response) => {
|
|
209
|
+
// Retry based on response body content
|
|
210
|
+
if (response.status === 200) {
|
|
211
|
+
const data = await response.clone().json();
|
|
212
|
+
|
|
213
|
+
// Simple retry with default delay
|
|
214
|
+
if (data.error?.code === 'TEMPORARY_ERROR') {
|
|
215
|
+
return ky.retry();
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Retry with custom delay from API response
|
|
219
|
+
if (data.error?.code === 'RATE_LIMIT') {
|
|
220
|
+
return ky.retry({
|
|
221
|
+
delay: data.error.retryAfter * 1000,
|
|
222
|
+
code: 'RATE_LIMIT'
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Retry with a modified request (e.g., fallback endpoint)
|
|
227
|
+
if (data.error?.code === 'FALLBACK_TO_BACKUP') {
|
|
228
|
+
return ky.retry({
|
|
229
|
+
request: new Request('https://backup-api.com/endpoint', {
|
|
230
|
+
method: request.method,
|
|
231
|
+
headers: request.headers,
|
|
232
|
+
}),
|
|
233
|
+
code: 'BACKUP_ENDPOINT'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Retry with refreshed authentication
|
|
238
|
+
if (data.error?.code === 'TOKEN_REFRESH' && data.newToken) {
|
|
239
|
+
return ky.retry({
|
|
240
|
+
request: new Request(request, {
|
|
241
|
+
headers: {
|
|
242
|
+
...Object.fromEntries(request.headers),
|
|
243
|
+
'Authorization': `Bearer ${data.newToken}`
|
|
244
|
+
}
|
|
245
|
+
}),
|
|
246
|
+
code: 'TOKEN_REFRESHED'
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Retry with cause to preserve error chain
|
|
251
|
+
try {
|
|
252
|
+
validateResponse(data);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return ky.retry({
|
|
255
|
+
code: 'VALIDATION_FAILED',
|
|
256
|
+
cause: error
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
],
|
|
262
|
+
beforeRetry: [
|
|
263
|
+
({error, retryCount}) => {
|
|
264
|
+
// Observable in beforeRetry hooks
|
|
265
|
+
if (isForceRetryError(error)) {
|
|
266
|
+
console.log(`Forced retry #${retryCount}: ${error.message}`);
|
|
267
|
+
// Example output: "Forced retry #1: Forced retry: RATE_LIMIT"
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const response = await api.get('https://example.com/api');
|
|
275
|
+
```
|
|
276
|
+
*/
|
|
277
|
+
const retry = (options) => new RetryMarker(options);
|
|
203
278
|
const kyOptionKeys = {
|
|
204
279
|
json: true,
|
|
205
280
|
parseJson: true,
|
|
@@ -211,8 +286,21 @@
|
|
|
211
286
|
hooks: true,
|
|
212
287
|
throwHttpErrors: true,
|
|
213
288
|
onDownloadProgress: true,
|
|
289
|
+
onUploadProgress: true,
|
|
214
290
|
fetch: true,
|
|
291
|
+
context: true,
|
|
292
|
+
};
|
|
293
|
+
// Vendor-specific fetch options that should always be passed to fetch()
|
|
294
|
+
// even if they appear on the Request object due to vendor patching.
|
|
295
|
+
// See: https://github.com/sindresorhus/ky/issues/541
|
|
296
|
+
const vendorSpecificOptions = {
|
|
297
|
+
next: true, // Next.js cache revalidation (revalidate, tags)
|
|
215
298
|
};
|
|
299
|
+
// Standard RequestInit options that should NOT be passed separately to fetch()
|
|
300
|
+
// because they're already applied to the Request object.
|
|
301
|
+
// Note: `dispatcher` and `priority` are NOT included here - they're fetch-only
|
|
302
|
+
// options that the Request constructor doesn't accept, so they need to be passed
|
|
303
|
+
// separately to fetch().
|
|
216
304
|
const requestOptionsRegistry = {
|
|
217
305
|
method: true,
|
|
218
306
|
headers: true,
|
|
@@ -227,9 +315,267 @@
|
|
|
227
315
|
keepalive: true,
|
|
228
316
|
signal: true,
|
|
229
317
|
window: true,
|
|
230
|
-
dispatcher: true,
|
|
231
318
|
duplex: true,
|
|
232
|
-
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
322
|
+
const getBodySize = (body) => {
|
|
323
|
+
if (!body) {
|
|
324
|
+
return 0;
|
|
325
|
+
}
|
|
326
|
+
if (body instanceof FormData) {
|
|
327
|
+
// This is an approximation, as FormData size calculation is not straightforward
|
|
328
|
+
let size = 0;
|
|
329
|
+
for (const [key, value] of body) {
|
|
330
|
+
size += usualFormBoundarySize;
|
|
331
|
+
size += new TextEncoder().encode(`Content-Disposition: form-data; name="${key}"`).length;
|
|
332
|
+
size += typeof value === 'string'
|
|
333
|
+
? new TextEncoder().encode(value).length
|
|
334
|
+
: value.size;
|
|
335
|
+
}
|
|
336
|
+
return size;
|
|
337
|
+
}
|
|
338
|
+
if (body instanceof Blob) {
|
|
339
|
+
return body.size;
|
|
340
|
+
}
|
|
341
|
+
if (body instanceof ArrayBuffer) {
|
|
342
|
+
return body.byteLength;
|
|
343
|
+
}
|
|
344
|
+
if (typeof body === 'string') {
|
|
345
|
+
return new TextEncoder().encode(body).length;
|
|
346
|
+
}
|
|
347
|
+
if (body instanceof URLSearchParams) {
|
|
348
|
+
return new TextEncoder().encode(body.toString()).length;
|
|
349
|
+
}
|
|
350
|
+
if ('byteLength' in body) {
|
|
351
|
+
return (body).byteLength;
|
|
352
|
+
}
|
|
353
|
+
if (typeof body === 'object' && body !== null) {
|
|
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
|
|
363
|
+
};
|
|
364
|
+
const withProgress = (stream, totalBytes, onProgress) => {
|
|
365
|
+
let previousChunk;
|
|
366
|
+
let transferredBytes = 0;
|
|
367
|
+
return stream.pipeThrough(new TransformStream({
|
|
368
|
+
transform(currentChunk, controller) {
|
|
369
|
+
controller.enqueue(currentChunk);
|
|
370
|
+
if (previousChunk) {
|
|
371
|
+
transferredBytes += previousChunk.byteLength;
|
|
372
|
+
let percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes;
|
|
373
|
+
// Avoid reporting 100% progress before the stream is actually finished (in case totalBytes is inaccurate)
|
|
374
|
+
if (percent >= 1) {
|
|
375
|
+
// Epsilon is used here to get as close as possible to 100% without reaching it.
|
|
376
|
+
// If we were to use 0.99 here, percent could potentially go backwards.
|
|
377
|
+
percent = 1 - Number.EPSILON;
|
|
378
|
+
}
|
|
379
|
+
onProgress?.({ percent, totalBytes: Math.max(totalBytes, transferredBytes), transferredBytes }, previousChunk);
|
|
380
|
+
}
|
|
381
|
+
previousChunk = currentChunk;
|
|
382
|
+
},
|
|
383
|
+
flush() {
|
|
384
|
+
if (previousChunk) {
|
|
385
|
+
transferredBytes += previousChunk.byteLength;
|
|
386
|
+
onProgress?.({ percent: 1, totalBytes: Math.max(totalBytes, transferredBytes), transferredBytes }, previousChunk);
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
}));
|
|
390
|
+
};
|
|
391
|
+
const streamResponse = (response, onDownloadProgress) => {
|
|
392
|
+
if (!response.body) {
|
|
393
|
+
return response;
|
|
394
|
+
}
|
|
395
|
+
if (response.status === 204) {
|
|
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), {
|
|
404
|
+
status: response.status,
|
|
405
|
+
statusText: response.statusText,
|
|
406
|
+
headers: response.headers,
|
|
407
|
+
});
|
|
408
|
+
};
|
|
409
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
410
|
+
const streamRequest = (request, onUploadProgress, originalBody) => {
|
|
411
|
+
if (!request.body) {
|
|
412
|
+
return request;
|
|
413
|
+
}
|
|
414
|
+
// Use original body for size calculation since request.body is already a stream
|
|
415
|
+
const totalBytes = getBodySize(originalBody ?? request.body);
|
|
416
|
+
return new Request(request, {
|
|
417
|
+
// @ts-expect-error - Types are outdated.
|
|
418
|
+
duplex: 'half',
|
|
419
|
+
body: withProgress(request.body, totalBytes, onUploadProgress),
|
|
420
|
+
});
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
424
|
+
const isObject$1 = (value) => value !== null && typeof value === 'object';
|
|
425
|
+
|
|
426
|
+
const validateAndMerge = (...sources) => {
|
|
427
|
+
for (const source of sources) {
|
|
428
|
+
if ((!isObject$1(source) || Array.isArray(source)) && source !== undefined) {
|
|
429
|
+
throw new TypeError('The `options` argument must be an object');
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return deepMerge({}, ...sources);
|
|
433
|
+
};
|
|
434
|
+
const mergeHeaders = (source1 = {}, source2 = {}) => {
|
|
435
|
+
const result = new globalThis.Headers(source1);
|
|
436
|
+
const isHeadersInstance = source2 instanceof globalThis.Headers;
|
|
437
|
+
const source = new globalThis.Headers(source2);
|
|
438
|
+
for (const [key, value] of source.entries()) {
|
|
439
|
+
if ((isHeadersInstance && value === 'undefined') || value === undefined) {
|
|
440
|
+
result.delete(key);
|
|
441
|
+
}
|
|
442
|
+
else {
|
|
443
|
+
result.set(key, value);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
return result;
|
|
447
|
+
};
|
|
448
|
+
function newHookValue(original, incoming, property) {
|
|
449
|
+
return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
|
|
450
|
+
? []
|
|
451
|
+
: deepMerge(original[property] ?? [], incoming[property] ?? []);
|
|
452
|
+
}
|
|
453
|
+
const mergeHooks = (original = {}, incoming = {}) => ({
|
|
454
|
+
beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
|
|
455
|
+
beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
|
|
456
|
+
afterResponse: newHookValue(original, incoming, 'afterResponse'),
|
|
457
|
+
beforeError: newHookValue(original, incoming, 'beforeError'),
|
|
458
|
+
});
|
|
459
|
+
const appendSearchParameters = (target, source) => {
|
|
460
|
+
const result = new URLSearchParams();
|
|
461
|
+
for (const input of [target, source]) {
|
|
462
|
+
if (input === undefined) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
if (input instanceof URLSearchParams) {
|
|
466
|
+
for (const [key, value] of input.entries()) {
|
|
467
|
+
result.append(key, value);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
else if (Array.isArray(input)) {
|
|
471
|
+
for (const pair of input) {
|
|
472
|
+
if (!Array.isArray(pair) || pair.length !== 2) {
|
|
473
|
+
throw new TypeError('Array search parameters must be provided in [[key, value], ...] format');
|
|
474
|
+
}
|
|
475
|
+
result.append(String(pair[0]), String(pair[1]));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
else if (isObject$1(input)) {
|
|
479
|
+
for (const [key, value] of Object.entries(input)) {
|
|
480
|
+
if (value !== undefined) {
|
|
481
|
+
result.append(key, String(value));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
// String
|
|
487
|
+
const parameters = new URLSearchParams(input);
|
|
488
|
+
for (const [key, value] of parameters.entries()) {
|
|
489
|
+
result.append(key, value);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
};
|
|
495
|
+
// TODO: Make this strongly-typed (no `any`).
|
|
496
|
+
const deepMerge = (...sources) => {
|
|
497
|
+
let returnValue = {};
|
|
498
|
+
let headers = {};
|
|
499
|
+
let hooks = {};
|
|
500
|
+
let searchParameters;
|
|
501
|
+
const signals = [];
|
|
502
|
+
for (const source of sources) {
|
|
503
|
+
if (Array.isArray(source)) {
|
|
504
|
+
if (!Array.isArray(returnValue)) {
|
|
505
|
+
returnValue = [];
|
|
506
|
+
}
|
|
507
|
+
returnValue = [...returnValue, ...source];
|
|
508
|
+
}
|
|
509
|
+
else if (isObject$1(source)) {
|
|
510
|
+
for (let [key, value] of Object.entries(source)) {
|
|
511
|
+
// Special handling for AbortSignal instances
|
|
512
|
+
if (key === 'signal' && value instanceof globalThis.AbortSignal) {
|
|
513
|
+
signals.push(value);
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
// Special handling for context - shallow merge only
|
|
517
|
+
if (key === 'context') {
|
|
518
|
+
if (value !== undefined && value !== null && (!isObject$1(value) || Array.isArray(value))) {
|
|
519
|
+
throw new TypeError('The `context` option must be an object');
|
|
520
|
+
}
|
|
521
|
+
// Shallow merge: always create a new object to prevent mutation bugs
|
|
522
|
+
returnValue = {
|
|
523
|
+
...returnValue,
|
|
524
|
+
context: (value === undefined || value === null)
|
|
525
|
+
? {}
|
|
526
|
+
: { ...returnValue.context, ...value },
|
|
527
|
+
};
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
// Special handling for searchParams
|
|
531
|
+
if (key === 'searchParams') {
|
|
532
|
+
if (value === undefined || value === null) {
|
|
533
|
+
// Explicit undefined or null removes searchParams
|
|
534
|
+
searchParameters = undefined;
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
// First source: keep as-is to preserve type (string/object/URLSearchParams)
|
|
538
|
+
// Subsequent sources: merge and convert to URLSearchParams
|
|
539
|
+
searchParameters = searchParameters === undefined ? value : appendSearchParameters(searchParameters, value);
|
|
540
|
+
}
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (isObject$1(value) && key in returnValue) {
|
|
544
|
+
value = deepMerge(returnValue[key], value);
|
|
545
|
+
}
|
|
546
|
+
returnValue = { ...returnValue, [key]: value };
|
|
547
|
+
}
|
|
548
|
+
if (isObject$1(source.hooks)) {
|
|
549
|
+
hooks = mergeHooks(hooks, source.hooks);
|
|
550
|
+
returnValue.hooks = hooks;
|
|
551
|
+
}
|
|
552
|
+
if (isObject$1(source.headers)) {
|
|
553
|
+
headers = mergeHeaders(headers, source.headers);
|
|
554
|
+
returnValue.headers = headers;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (searchParameters !== undefined) {
|
|
559
|
+
returnValue.searchParams = searchParameters;
|
|
560
|
+
}
|
|
561
|
+
if (signals.length > 0) {
|
|
562
|
+
if (signals.length === 1) {
|
|
563
|
+
returnValue.signal = signals[0];
|
|
564
|
+
}
|
|
565
|
+
else if (supportsAbortSignal) {
|
|
566
|
+
returnValue.signal = AbortSignal.any(signals);
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
// When AbortSignal.any is not available, use the last signal
|
|
570
|
+
// This maintains the previous behavior before signal merging was added
|
|
571
|
+
// This can be remove when the `supportsAbortSignal` check is removed.`
|
|
572
|
+
returnValue.signal = signals.at(-1);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (returnValue.context === undefined) {
|
|
576
|
+
returnValue.context = {};
|
|
577
|
+
}
|
|
578
|
+
return returnValue;
|
|
233
579
|
};
|
|
234
580
|
|
|
235
581
|
const normalizeRequestMethod = (input) => requestMethods.includes(input) ? input.toUpperCase() : input;
|
|
@@ -244,6 +590,8 @@
|
|
|
244
590
|
maxRetryAfter: Number.POSITIVE_INFINITY,
|
|
245
591
|
backoffLimit: Number.POSITIVE_INFINITY,
|
|
246
592
|
delay: attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000,
|
|
593
|
+
jitter: undefined,
|
|
594
|
+
retryOnTimeout: false,
|
|
247
595
|
};
|
|
248
596
|
const normalizeRetryOptions = (retry = {}) => {
|
|
249
597
|
if (typeof retry === 'number') {
|
|
@@ -255,15 +603,26 @@
|
|
|
255
603
|
if (retry.methods && !Array.isArray(retry.methods)) {
|
|
256
604
|
throw new Error('retry.methods must be an array');
|
|
257
605
|
}
|
|
606
|
+
retry.methods &&= retry.methods.map(method => method.toLowerCase());
|
|
258
607
|
if (retry.statusCodes && !Array.isArray(retry.statusCodes)) {
|
|
259
608
|
throw new Error('retry.statusCodes must be an array');
|
|
260
609
|
}
|
|
610
|
+
const normalizedRetry = Object.fromEntries(Object.entries(retry).filter(([, value]) => value !== undefined));
|
|
261
611
|
return {
|
|
262
612
|
...defaultRetryOptions,
|
|
263
|
-
...
|
|
613
|
+
...normalizedRetry,
|
|
264
614
|
};
|
|
265
615
|
};
|
|
266
616
|
|
|
617
|
+
class TimeoutError extends Error {
|
|
618
|
+
request;
|
|
619
|
+
constructor(request) {
|
|
620
|
+
super(`Request timed out: ${request.method} ${request.url}`);
|
|
621
|
+
this.name = 'TimeoutError';
|
|
622
|
+
this.request = request;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
267
626
|
// `Promise.race()` workaround (#91)
|
|
268
627
|
async function timeout(request, init, abortController, options) {
|
|
269
628
|
return new Promise((resolve, reject) => {
|
|
@@ -304,163 +663,396 @@
|
|
|
304
663
|
const findUnknownOptions = (request, options) => {
|
|
305
664
|
const unknownOptions = {};
|
|
306
665
|
for (const key in options) {
|
|
307
|
-
|
|
666
|
+
// Skip inherited properties
|
|
667
|
+
if (!Object.hasOwn(options, key)) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
// An option is passed to fetch() if:
|
|
671
|
+
// 1. It's not a standard RequestInit option (not in requestOptionsRegistry)
|
|
672
|
+
// 2. It's not a ky-specific option (not in kyOptionKeys)
|
|
673
|
+
// 3. Either:
|
|
674
|
+
// a. It's not on the Request object, OR
|
|
675
|
+
// b. It's a vendor-specific option that should always be passed (in vendorSpecificOptions)
|
|
676
|
+
if (!(key in requestOptionsRegistry) && !(key in kyOptionKeys) && (!(key in request) || key in vendorSpecificOptions)) {
|
|
308
677
|
unknownOptions[key] = options[key];
|
|
309
678
|
}
|
|
310
679
|
}
|
|
311
680
|
return unknownOptions;
|
|
312
681
|
};
|
|
682
|
+
const hasSearchParameters = (search) => {
|
|
683
|
+
if (search === undefined) {
|
|
684
|
+
return false;
|
|
685
|
+
}
|
|
686
|
+
// The `typeof array` still gives "object", so we need different checking for array.
|
|
687
|
+
if (Array.isArray(search)) {
|
|
688
|
+
return search.length > 0;
|
|
689
|
+
}
|
|
690
|
+
if (search instanceof URLSearchParams) {
|
|
691
|
+
return search.size > 0;
|
|
692
|
+
}
|
|
693
|
+
// Record
|
|
694
|
+
if (typeof search === 'object') {
|
|
695
|
+
return Object.keys(search).length > 0;
|
|
696
|
+
}
|
|
697
|
+
if (typeof search === 'string') {
|
|
698
|
+
return search.trim().length > 0;
|
|
699
|
+
}
|
|
700
|
+
return Boolean(search);
|
|
701
|
+
};
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
Type guard to check if an error is a Ky error.
|
|
705
|
+
|
|
706
|
+
@param error - The error to check
|
|
707
|
+
@returns `true` if the error is a Ky error, `false` otherwise
|
|
708
|
+
|
|
709
|
+
@example
|
|
710
|
+
```
|
|
711
|
+
import ky, {isKyError} from 'ky';
|
|
712
|
+
try {
|
|
713
|
+
const response = await ky.get('/api/data');
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (isKyError(error)) {
|
|
716
|
+
// Handle Ky-specific errors
|
|
717
|
+
console.log('Ky error occurred:', error.message);
|
|
718
|
+
} else {
|
|
719
|
+
// Handle other errors
|
|
720
|
+
console.log('Unknown error:', error);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
```
|
|
724
|
+
*/
|
|
725
|
+
function isKyError(error) {
|
|
726
|
+
return isHTTPError(error) || isTimeoutError(error) || isForceRetryError(error);
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
Type guard to check if an error is an HTTPError.
|
|
730
|
+
|
|
731
|
+
@param error - The error to check
|
|
732
|
+
@returns `true` if the error is an HTTPError, `false` otherwise
|
|
733
|
+
|
|
734
|
+
@example
|
|
735
|
+
```
|
|
736
|
+
import ky, {isHTTPError} from 'ky';
|
|
737
|
+
try {
|
|
738
|
+
const response = await ky.get('/api/data');
|
|
739
|
+
} catch (error) {
|
|
740
|
+
if (isHTTPError(error)) {
|
|
741
|
+
console.log('HTTP error status:', error.response.status);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
*/
|
|
746
|
+
function isHTTPError(error) {
|
|
747
|
+
return error instanceof HTTPError || (error?.name === HTTPError.name);
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
Type guard to check if an error is a TimeoutError.
|
|
751
|
+
|
|
752
|
+
@param error - The error to check
|
|
753
|
+
@returns `true` if the error is a TimeoutError, `false` otherwise
|
|
754
|
+
|
|
755
|
+
@example
|
|
756
|
+
```
|
|
757
|
+
import ky, {isTimeoutError} from 'ky';
|
|
758
|
+
try {
|
|
759
|
+
const response = await ky.get('/api/data', { timeout: 1000 });
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (isTimeoutError(error)) {
|
|
762
|
+
console.log('Request timed out:', error.request.url);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
```
|
|
766
|
+
*/
|
|
767
|
+
function isTimeoutError(error) {
|
|
768
|
+
return error instanceof TimeoutError || (error?.name === TimeoutError.name);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
Type guard to check if an error is a ForceRetryError.
|
|
772
|
+
|
|
773
|
+
@param error - The error to check
|
|
774
|
+
@returns `true` if the error is a ForceRetryError, `false` otherwise
|
|
775
|
+
|
|
776
|
+
@example
|
|
777
|
+
```
|
|
778
|
+
import ky, {isForceRetryError} from 'ky';
|
|
779
|
+
|
|
780
|
+
const api = ky.extend({
|
|
781
|
+
hooks: {
|
|
782
|
+
beforeRetry: [
|
|
783
|
+
({error, retryCount}) => {
|
|
784
|
+
if (isForceRetryError(error)) {
|
|
785
|
+
console.log(`Forced retry #${retryCount}: ${error.code}`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
```
|
|
792
|
+
*/
|
|
793
|
+
function isForceRetryError(error) {
|
|
794
|
+
return error instanceof ForceRetryError || (error?.name === ForceRetryError.name);
|
|
795
|
+
}
|
|
313
796
|
|
|
314
797
|
class Ky {
|
|
315
798
|
static create(input, options) {
|
|
316
799
|
const ky = new Ky(input, options);
|
|
317
800
|
const function_ = async () => {
|
|
318
|
-
if (typeof ky.
|
|
801
|
+
if (typeof ky.#options.timeout === 'number' && ky.#options.timeout > maxSafeTimeout) {
|
|
319
802
|
throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`);
|
|
320
803
|
}
|
|
321
804
|
// Delay the fetch so that body method shortcuts can set the Accept header
|
|
322
805
|
await Promise.resolve();
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
806
|
+
// Before using ky.request, _fetch clones it and saves the clone for future retries to use.
|
|
807
|
+
// If retry is not needed, close the cloned request's ReadableStream for memory safety.
|
|
808
|
+
let response = await ky.#fetch();
|
|
809
|
+
for (const hook of ky.#options.hooks.afterResponse) {
|
|
810
|
+
// Clone the response before passing to hook so we can cancel it if needed
|
|
811
|
+
const clonedResponse = ky.#decorateResponse(response.clone());
|
|
812
|
+
let modifiedResponse;
|
|
813
|
+
try {
|
|
814
|
+
// eslint-disable-next-line no-await-in-loop
|
|
815
|
+
modifiedResponse = await hook(ky.request, ky.#getNormalizedOptions(), clonedResponse, { retryCount: ky.#retryCount });
|
|
329
816
|
}
|
|
817
|
+
catch (error) {
|
|
818
|
+
// Cancel both responses to prevent memory leaks when hook throws
|
|
819
|
+
ky.#cancelResponseBody(clonedResponse);
|
|
820
|
+
ky.#cancelResponseBody(response);
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
if (modifiedResponse instanceof RetryMarker) {
|
|
824
|
+
// Cancel both the cloned response passed to the hook and the current response to prevent resource leaks (especially important in Deno/Bun).
|
|
825
|
+
// Do not await cancellation since hooks can clone the response, leaving extra tee branches that keep cancel promises pending per the Streams spec.
|
|
826
|
+
ky.#cancelResponseBody(clonedResponse);
|
|
827
|
+
ky.#cancelResponseBody(response);
|
|
828
|
+
throw new ForceRetryError(modifiedResponse.options);
|
|
829
|
+
}
|
|
830
|
+
// Determine which response to use going forward
|
|
831
|
+
const nextResponse = modifiedResponse instanceof globalThis.Response ? modifiedResponse : response;
|
|
832
|
+
// Cancel any response bodies we won't use to prevent memory leaks.
|
|
833
|
+
// Uses fire-and-forget since hooks may have cloned the response, creating tee branches that block cancellation.
|
|
834
|
+
if (clonedResponse !== nextResponse) {
|
|
835
|
+
ky.#cancelResponseBody(clonedResponse);
|
|
836
|
+
}
|
|
837
|
+
if (response !== nextResponse) {
|
|
838
|
+
ky.#cancelResponseBody(response);
|
|
839
|
+
}
|
|
840
|
+
response = nextResponse;
|
|
330
841
|
}
|
|
331
|
-
ky
|
|
332
|
-
if (!response.ok && ky.
|
|
333
|
-
|
|
334
|
-
|
|
842
|
+
ky.#decorateResponse(response);
|
|
843
|
+
if (!response.ok && (typeof ky.#options.throwHttpErrors === 'function'
|
|
844
|
+
? ky.#options.throwHttpErrors(response.status)
|
|
845
|
+
: ky.#options.throwHttpErrors)) {
|
|
846
|
+
let error = new HTTPError(response, ky.request, ky.#getNormalizedOptions());
|
|
847
|
+
for (const hook of ky.#options.hooks.beforeError) {
|
|
335
848
|
// eslint-disable-next-line no-await-in-loop
|
|
336
|
-
error = await hook(error);
|
|
849
|
+
error = await hook(error, { retryCount: ky.#retryCount });
|
|
337
850
|
}
|
|
338
851
|
throw error;
|
|
339
852
|
}
|
|
340
853
|
// If `onDownloadProgress` is passed, it uses the stream API internally
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if (typeof ky._options.onDownloadProgress !== 'function') {
|
|
854
|
+
if (ky.#options.onDownloadProgress) {
|
|
855
|
+
if (typeof ky.#options.onDownloadProgress !== 'function') {
|
|
344
856
|
throw new TypeError('The `onDownloadProgress` option must be a function');
|
|
345
857
|
}
|
|
346
858
|
if (!supportsResponseStreams) {
|
|
347
859
|
throw new Error('Streams are not supported in your environment. `ReadableStream` is missing.');
|
|
348
860
|
}
|
|
349
|
-
|
|
861
|
+
const progressResponse = response.clone();
|
|
862
|
+
ky.#cancelResponseBody(response);
|
|
863
|
+
return streamResponse(progressResponse, ky.#options.onDownloadProgress);
|
|
350
864
|
}
|
|
351
865
|
return response;
|
|
352
866
|
};
|
|
353
|
-
|
|
354
|
-
|
|
867
|
+
// Always wrap in #retry to catch forced retries from afterResponse hooks
|
|
868
|
+
// Method retriability is checked in #calculateRetryDelay for non-forced retries
|
|
869
|
+
const result = ky.#retry(function_)
|
|
870
|
+
.finally(() => {
|
|
871
|
+
const originalRequest = ky.#originalRequest;
|
|
872
|
+
// Ignore cancellation errors from already-locked or already-consumed streams.
|
|
873
|
+
ky.#cancelBody(originalRequest?.body ?? undefined);
|
|
874
|
+
ky.#cancelBody(ky.request.body ?? undefined);
|
|
875
|
+
});
|
|
355
876
|
for (const [type, mimeType] of Object.entries(responseTypes)) {
|
|
877
|
+
// Only expose `.bytes()` when the environment implements it.
|
|
878
|
+
if (type === 'bytes'
|
|
879
|
+
&& typeof globalThis.Response?.prototype?.bytes !== 'function') {
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
356
882
|
result[type] = async () => {
|
|
357
883
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
358
884
|
ky.request.headers.set('accept', ky.request.headers.get('accept') || mimeType);
|
|
359
|
-
const
|
|
360
|
-
const response = awaitedResult.clone();
|
|
885
|
+
const response = await result;
|
|
361
886
|
if (type === 'json') {
|
|
362
887
|
if (response.status === 204) {
|
|
363
888
|
return '';
|
|
364
889
|
}
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
if (responseSize === 0) {
|
|
890
|
+
const text = await response.text();
|
|
891
|
+
if (text === '') {
|
|
368
892
|
return '';
|
|
369
893
|
}
|
|
370
894
|
if (options.parseJson) {
|
|
371
|
-
return options.parseJson(
|
|
895
|
+
return options.parseJson(text);
|
|
372
896
|
}
|
|
897
|
+
return JSON.parse(text);
|
|
373
898
|
}
|
|
374
899
|
return response[type]();
|
|
375
900
|
};
|
|
376
901
|
}
|
|
377
902
|
return result;
|
|
378
903
|
}
|
|
904
|
+
// eslint-disable-next-line unicorn/prevent-abbreviations
|
|
905
|
+
static #normalizeSearchParams(searchParams) {
|
|
906
|
+
// Filter out undefined values from plain objects
|
|
907
|
+
if (searchParams && typeof searchParams === 'object' && !Array.isArray(searchParams) && !(searchParams instanceof URLSearchParams)) {
|
|
908
|
+
return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => value !== undefined));
|
|
909
|
+
}
|
|
910
|
+
return searchParams;
|
|
911
|
+
}
|
|
379
912
|
request;
|
|
380
|
-
abortController;
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
913
|
+
#abortController;
|
|
914
|
+
#retryCount = 0;
|
|
915
|
+
// eslint-disable-next-line @typescript-eslint/prefer-readonly -- False positive: #input is reassigned on line 202
|
|
916
|
+
#input;
|
|
917
|
+
#options;
|
|
918
|
+
#originalRequest;
|
|
919
|
+
#userProvidedAbortSignal;
|
|
920
|
+
#cachedNormalizedOptions;
|
|
384
921
|
// eslint-disable-next-line complexity
|
|
385
922
|
constructor(input, options = {}) {
|
|
386
|
-
this
|
|
387
|
-
this
|
|
923
|
+
this.#input = input;
|
|
924
|
+
this.#options = {
|
|
388
925
|
...options,
|
|
389
|
-
headers: mergeHeaders(this.
|
|
926
|
+
headers: mergeHeaders(this.#input.headers, options.headers),
|
|
390
927
|
hooks: mergeHooks({
|
|
391
928
|
beforeRequest: [],
|
|
392
929
|
beforeRetry: [],
|
|
393
930
|
beforeError: [],
|
|
394
931
|
afterResponse: [],
|
|
395
932
|
}, options.hooks),
|
|
396
|
-
method: normalizeRequestMethod(options.method ?? this.
|
|
933
|
+
method: normalizeRequestMethod(options.method ?? this.#input.method ?? 'GET'),
|
|
397
934
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
398
935
|
prefixUrl: String(options.prefixUrl || ''),
|
|
399
936
|
retry: normalizeRetryOptions(options.retry),
|
|
400
|
-
throwHttpErrors: options.throwHttpErrors
|
|
937
|
+
throwHttpErrors: options.throwHttpErrors ?? true,
|
|
401
938
|
timeout: options.timeout ?? 10_000,
|
|
402
939
|
fetch: options.fetch ?? globalThis.fetch.bind(globalThis),
|
|
940
|
+
context: options.context ?? {},
|
|
403
941
|
};
|
|
404
|
-
if (typeof this
|
|
942
|
+
if (typeof this.#input !== 'string' && !(this.#input instanceof URL || this.#input instanceof globalThis.Request)) {
|
|
405
943
|
throw new TypeError('`input` must be a string, URL, or Request');
|
|
406
944
|
}
|
|
407
|
-
if (this.
|
|
408
|
-
if (this.
|
|
945
|
+
if (this.#options.prefixUrl && typeof this.#input === 'string') {
|
|
946
|
+
if (this.#input.startsWith('/')) {
|
|
409
947
|
throw new Error('`input` must not begin with a slash when using `prefixUrl`');
|
|
410
948
|
}
|
|
411
|
-
if (!this.
|
|
412
|
-
this.
|
|
949
|
+
if (!this.#options.prefixUrl.endsWith('/')) {
|
|
950
|
+
this.#options.prefixUrl += '/';
|
|
413
951
|
}
|
|
414
|
-
this
|
|
952
|
+
this.#input = this.#options.prefixUrl + this.#input;
|
|
415
953
|
}
|
|
416
|
-
if (supportsAbortController) {
|
|
417
|
-
this
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
this.abortController.abort(originalSignal.reason);
|
|
421
|
-
});
|
|
422
|
-
this._options.signal = this.abortController.signal;
|
|
954
|
+
if (supportsAbortController && supportsAbortSignal) {
|
|
955
|
+
this.#userProvidedAbortSignal = this.#options.signal ?? this.#input.signal;
|
|
956
|
+
this.#abortController = new globalThis.AbortController();
|
|
957
|
+
this.#options.signal = this.#userProvidedAbortSignal ? AbortSignal.any([this.#userProvidedAbortSignal, this.#abortController.signal]) : this.#abortController.signal;
|
|
423
958
|
}
|
|
424
959
|
if (supportsRequestStreams) {
|
|
425
960
|
// @ts-expect-error - Types are outdated.
|
|
426
|
-
this.
|
|
961
|
+
this.#options.duplex = 'half';
|
|
427
962
|
}
|
|
428
|
-
if (this.
|
|
429
|
-
this.
|
|
430
|
-
this.
|
|
963
|
+
if (this.#options.json !== undefined) {
|
|
964
|
+
this.#options.body = this.#options.stringifyJson?.(this.#options.json) ?? JSON.stringify(this.#options.json);
|
|
965
|
+
this.#options.headers.set('content-type', this.#options.headers.get('content-type') ?? 'application/json');
|
|
431
966
|
}
|
|
432
|
-
|
|
433
|
-
if
|
|
967
|
+
// To provide correct form boundary, Content-Type header should be deleted when creating Request from another Request with FormData/URLSearchParams body
|
|
968
|
+
// Only delete if user didn't explicitly provide a custom content-type
|
|
969
|
+
const userProvidedContentType = options.headers && new globalThis.Headers(options.headers).has('content-type');
|
|
970
|
+
if (this.#input instanceof globalThis.Request
|
|
971
|
+
&& ((supportsFormData && this.#options.body instanceof globalThis.FormData) || this.#options.body instanceof URLSearchParams)
|
|
972
|
+
&& !userProvidedContentType) {
|
|
973
|
+
this.#options.headers.delete('content-type');
|
|
974
|
+
}
|
|
975
|
+
this.request = new globalThis.Request(this.#input, this.#options);
|
|
976
|
+
if (hasSearchParameters(this.#options.searchParams)) {
|
|
434
977
|
// eslint-disable-next-line unicorn/prevent-abbreviations
|
|
435
|
-
const textSearchParams = typeof this.
|
|
436
|
-
? this.
|
|
437
|
-
: new URLSearchParams(this.
|
|
978
|
+
const textSearchParams = typeof this.#options.searchParams === 'string'
|
|
979
|
+
? this.#options.searchParams.replace(/^\?/, '')
|
|
980
|
+
: new URLSearchParams(Ky.#normalizeSearchParams(this.#options.searchParams)).toString();
|
|
438
981
|
// eslint-disable-next-line unicorn/prevent-abbreviations
|
|
439
982
|
const searchParams = '?' + textSearchParams;
|
|
440
983
|
const url = this.request.url.replace(/(?:\?.*?)?(?=#|$)/, searchParams);
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
984
|
+
// Recreate request with the updated URL. We already have all options in this.#options, including duplex.
|
|
985
|
+
this.request = new globalThis.Request(url, this.#options);
|
|
986
|
+
}
|
|
987
|
+
// If `onUploadProgress` is passed, it uses the stream API internally
|
|
988
|
+
if (this.#options.onUploadProgress) {
|
|
989
|
+
if (typeof this.#options.onUploadProgress !== 'function') {
|
|
990
|
+
throw new TypeError('The `onUploadProgress` option must be a function');
|
|
991
|
+
}
|
|
992
|
+
if (!supportsRequestStreams) {
|
|
993
|
+
throw new Error('Request streams are not supported in your environment. The `duplex` option for `Request` is not available.');
|
|
994
|
+
}
|
|
995
|
+
this.request = this.#wrapRequestWithUploadProgress(this.request, this.#options.body ?? undefined);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
#calculateDelay() {
|
|
999
|
+
const retryDelay = this.#options.retry.delay(this.#retryCount);
|
|
1000
|
+
let jitteredDelay = retryDelay;
|
|
1001
|
+
if (this.#options.retry.jitter === true) {
|
|
1002
|
+
jitteredDelay = Math.random() * retryDelay;
|
|
1003
|
+
}
|
|
1004
|
+
else if (typeof this.#options.retry.jitter === 'function') {
|
|
1005
|
+
jitteredDelay = this.#options.retry.jitter(retryDelay);
|
|
1006
|
+
if (!Number.isFinite(jitteredDelay) || jitteredDelay < 0) {
|
|
1007
|
+
jitteredDelay = retryDelay;
|
|
445
1008
|
}
|
|
446
|
-
// The spread of `this.request` is required as otherwise it misses the `duplex` option for some reason and throws.
|
|
447
|
-
this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options);
|
|
448
1009
|
}
|
|
1010
|
+
// Handle undefined backoffLimit by treating it as no limit (Infinity)
|
|
1011
|
+
const backoffLimit = this.#options.retry.backoffLimit ?? Number.POSITIVE_INFINITY;
|
|
1012
|
+
return Math.min(backoffLimit, jitteredDelay);
|
|
449
1013
|
}
|
|
450
|
-
|
|
451
|
-
this
|
|
452
|
-
if (this
|
|
1014
|
+
async #calculateRetryDelay(error) {
|
|
1015
|
+
this.#retryCount++;
|
|
1016
|
+
if (this.#retryCount > this.#options.retry.limit) {
|
|
453
1017
|
throw error;
|
|
454
1018
|
}
|
|
455
|
-
|
|
456
|
-
|
|
1019
|
+
// Wrap non-Error throws to ensure consistent error handling
|
|
1020
|
+
const errorObject = error instanceof Error ? error : new NonError(error);
|
|
1021
|
+
// Handle forced retry from afterResponse hook - skip method check and shouldRetry
|
|
1022
|
+
if (errorObject instanceof ForceRetryError) {
|
|
1023
|
+
return errorObject.customDelay ?? this.#calculateDelay();
|
|
1024
|
+
}
|
|
1025
|
+
// Check if method is retriable for non-forced retries
|
|
1026
|
+
if (!this.#options.retry.methods.includes(this.request.method.toLowerCase())) {
|
|
1027
|
+
throw error;
|
|
1028
|
+
}
|
|
1029
|
+
// User-provided shouldRetry function takes precedence over all other checks
|
|
1030
|
+
if (this.#options.retry.shouldRetry !== undefined) {
|
|
1031
|
+
const result = await this.#options.retry.shouldRetry({ error: errorObject, retryCount: this.#retryCount });
|
|
1032
|
+
// Strict boolean checking - only exact true/false are handled specially
|
|
1033
|
+
if (result === false) {
|
|
1034
|
+
throw error;
|
|
1035
|
+
}
|
|
1036
|
+
if (result === true) {
|
|
1037
|
+
// Force retry - skip all other validation and return delay
|
|
1038
|
+
return this.#calculateDelay();
|
|
1039
|
+
}
|
|
1040
|
+
// If undefined or any other value, fall through to default behavior
|
|
1041
|
+
}
|
|
1042
|
+
// Default timeout behavior
|
|
1043
|
+
if (isTimeoutError(error) && !this.#options.retry.retryOnTimeout) {
|
|
1044
|
+
throw error;
|
|
1045
|
+
}
|
|
1046
|
+
if (isHTTPError(error)) {
|
|
1047
|
+
if (!this.#options.retry.statusCodes.includes(error.response.status)) {
|
|
457
1048
|
throw error;
|
|
458
1049
|
}
|
|
459
1050
|
const retryAfter = error.response.headers.get('Retry-After')
|
|
460
1051
|
?? error.response.headers.get('RateLimit-Reset')
|
|
1052
|
+
?? error.response.headers.get('X-RateLimit-Retry-After') // Symfony-based services
|
|
461
1053
|
?? error.response.headers.get('X-RateLimit-Reset') // GitHub
|
|
462
1054
|
?? error.response.headers.get('X-Rate-Limit-Reset'); // Twitter
|
|
463
|
-
if (retryAfter && this.
|
|
1055
|
+
if (retryAfter && this.#options.retry.afterStatusCodes.includes(error.response.status)) {
|
|
464
1056
|
let after = Number(retryAfter) * 1000;
|
|
465
1057
|
if (Number.isNaN(after)) {
|
|
466
1058
|
after = Date.parse(retryAfter) - Date.now();
|
|
@@ -469,110 +1061,120 @@
|
|
|
469
1061
|
// A large number is treated as a timestamp (fixed threshold protects against clock skew)
|
|
470
1062
|
after -= Date.now();
|
|
471
1063
|
}
|
|
472
|
-
const max = this.
|
|
1064
|
+
const max = this.#options.retry.maxRetryAfter ?? after;
|
|
1065
|
+
// Don't apply jitter when server provides explicit retry timing
|
|
473
1066
|
return after < max ? after : max;
|
|
474
1067
|
}
|
|
475
1068
|
if (error.response.status === 413) {
|
|
476
1069
|
throw error;
|
|
477
1070
|
}
|
|
478
1071
|
}
|
|
479
|
-
|
|
480
|
-
return Math.min(this._options.retry.backoffLimit, retryDelay);
|
|
1072
|
+
return this.#calculateDelay();
|
|
481
1073
|
}
|
|
482
|
-
|
|
483
|
-
if (this.
|
|
484
|
-
response.json = async () => this.
|
|
1074
|
+
#decorateResponse(response) {
|
|
1075
|
+
if (this.#options.parseJson) {
|
|
1076
|
+
response.json = async () => this.#options.parseJson(await response.text());
|
|
485
1077
|
}
|
|
486
1078
|
return response;
|
|
487
1079
|
}
|
|
488
|
-
|
|
1080
|
+
#cancelBody(body) {
|
|
1081
|
+
if (!body) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
// Ignore cancellation failures from already-locked or already-consumed streams.
|
|
1085
|
+
void body.cancel().catch(() => undefined);
|
|
1086
|
+
}
|
|
1087
|
+
#cancelResponseBody(response) {
|
|
1088
|
+
// Ignore cancellation failures from already-locked or already-consumed streams.
|
|
1089
|
+
this.#cancelBody(response.body ?? undefined);
|
|
1090
|
+
}
|
|
1091
|
+
async #retry(function_) {
|
|
489
1092
|
try {
|
|
490
1093
|
return await function_();
|
|
491
1094
|
}
|
|
492
1095
|
catch (error) {
|
|
493
|
-
const ms = Math.min(this
|
|
494
|
-
if (this
|
|
1096
|
+
const ms = Math.min(await this.#calculateRetryDelay(error), maxSafeTimeout);
|
|
1097
|
+
if (this.#retryCount < 1) {
|
|
495
1098
|
throw error;
|
|
496
1099
|
}
|
|
497
|
-
|
|
498
|
-
|
|
1100
|
+
// Only use user-provided signal for delay, not our internal abortController
|
|
1101
|
+
await delay(ms, this.#userProvidedAbortSignal ? { signal: this.#userProvidedAbortSignal } : {});
|
|
1102
|
+
// Apply custom request from forced retry before beforeRetry hooks
|
|
1103
|
+
// Ensure the custom request has the correct managed signal for timeouts and user aborts
|
|
1104
|
+
if (error instanceof ForceRetryError && error.customRequest) {
|
|
1105
|
+
const managedRequest = this.#options.signal
|
|
1106
|
+
? new globalThis.Request(error.customRequest, { signal: this.#options.signal })
|
|
1107
|
+
: new globalThis.Request(error.customRequest);
|
|
1108
|
+
this.#assignRequest(managedRequest);
|
|
1109
|
+
}
|
|
1110
|
+
for (const hook of this.#options.hooks.beforeRetry) {
|
|
499
1111
|
// eslint-disable-next-line no-await-in-loop
|
|
500
1112
|
const hookResult = await hook({
|
|
501
1113
|
request: this.request,
|
|
502
|
-
options: this
|
|
1114
|
+
options: this.#getNormalizedOptions(),
|
|
503
1115
|
error: error,
|
|
504
|
-
retryCount: this
|
|
1116
|
+
retryCount: this.#retryCount,
|
|
505
1117
|
});
|
|
1118
|
+
if (hookResult instanceof globalThis.Request) {
|
|
1119
|
+
this.#assignRequest(hookResult);
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
// If a Response is returned, use it and skip the retry
|
|
1123
|
+
if (hookResult instanceof globalThis.Response) {
|
|
1124
|
+
return hookResult;
|
|
1125
|
+
}
|
|
506
1126
|
// If `stop` is returned from the hook, the retry process is stopped
|
|
507
1127
|
if (hookResult === stop) {
|
|
508
1128
|
return;
|
|
509
1129
|
}
|
|
510
1130
|
}
|
|
511
|
-
return this
|
|
1131
|
+
return this.#retry(function_);
|
|
512
1132
|
}
|
|
513
1133
|
}
|
|
514
|
-
async
|
|
515
|
-
|
|
1134
|
+
async #fetch() {
|
|
1135
|
+
// Reset abortController if it was aborted (happens on timeout retry)
|
|
1136
|
+
if (this.#abortController?.signal.aborted) {
|
|
1137
|
+
this.#abortController = new globalThis.AbortController();
|
|
1138
|
+
this.#options.signal = this.#userProvidedAbortSignal ? AbortSignal.any([this.#userProvidedAbortSignal, this.#abortController.signal]) : this.#abortController.signal;
|
|
1139
|
+
// Recreate request with new signal
|
|
1140
|
+
this.request = new globalThis.Request(this.request, { signal: this.#options.signal });
|
|
1141
|
+
}
|
|
1142
|
+
for (const hook of this.#options.hooks.beforeRequest) {
|
|
516
1143
|
// eslint-disable-next-line no-await-in-loop
|
|
517
|
-
const result = await hook(this.request, this
|
|
518
|
-
if (result instanceof Request) {
|
|
519
|
-
this.request = result;
|
|
520
|
-
break;
|
|
521
|
-
}
|
|
1144
|
+
const result = await hook(this.request, this.#getNormalizedOptions(), { retryCount: this.#retryCount });
|
|
522
1145
|
if (result instanceof Response) {
|
|
523
1146
|
return result;
|
|
524
1147
|
}
|
|
1148
|
+
if (result instanceof globalThis.Request) {
|
|
1149
|
+
this.#assignRequest(result);
|
|
1150
|
+
break;
|
|
1151
|
+
}
|
|
525
1152
|
}
|
|
526
|
-
const nonRequestOptions = findUnknownOptions(this.request, this
|
|
1153
|
+
const nonRequestOptions = findUnknownOptions(this.request, this.#options);
|
|
527
1154
|
// Cloning is done here to prepare in advance for retries
|
|
528
|
-
|
|
529
|
-
this.request =
|
|
530
|
-
if (this.
|
|
531
|
-
return this.
|
|
532
|
-
}
|
|
533
|
-
return timeout(
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
const reader = response.body.getReader();
|
|
552
|
-
if (onDownloadProgress) {
|
|
553
|
-
onDownloadProgress({ percent: 0, transferredBytes: 0, totalBytes }, new Uint8Array());
|
|
554
|
-
}
|
|
555
|
-
async function read() {
|
|
556
|
-
const { done, value } = await reader.read();
|
|
557
|
-
if (done) {
|
|
558
|
-
controller.close();
|
|
559
|
-
return;
|
|
560
|
-
}
|
|
561
|
-
if (onDownloadProgress) {
|
|
562
|
-
transferredBytes += value.byteLength;
|
|
563
|
-
const percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes;
|
|
564
|
-
onDownloadProgress({ percent, transferredBytes, totalBytes }, value);
|
|
565
|
-
}
|
|
566
|
-
controller.enqueue(value);
|
|
567
|
-
await read();
|
|
568
|
-
}
|
|
569
|
-
await read();
|
|
570
|
-
},
|
|
571
|
-
}), {
|
|
572
|
-
status: response.status,
|
|
573
|
-
statusText: response.statusText,
|
|
574
|
-
headers: response.headers,
|
|
575
|
-
});
|
|
1155
|
+
this.#originalRequest = this.request;
|
|
1156
|
+
this.request = this.#originalRequest.clone();
|
|
1157
|
+
if (this.#options.timeout === false) {
|
|
1158
|
+
return this.#options.fetch(this.#originalRequest, nonRequestOptions);
|
|
1159
|
+
}
|
|
1160
|
+
return timeout(this.#originalRequest, nonRequestOptions, this.#abortController, this.#options);
|
|
1161
|
+
}
|
|
1162
|
+
#getNormalizedOptions() {
|
|
1163
|
+
if (!this.#cachedNormalizedOptions) {
|
|
1164
|
+
const { hooks, ...normalizedOptions } = this.#options;
|
|
1165
|
+
this.#cachedNormalizedOptions = Object.freeze(normalizedOptions);
|
|
1166
|
+
}
|
|
1167
|
+
return this.#cachedNormalizedOptions;
|
|
1168
|
+
}
|
|
1169
|
+
#assignRequest(request) {
|
|
1170
|
+
this.#cachedNormalizedOptions = undefined;
|
|
1171
|
+
this.request = this.#wrapRequestWithUploadProgress(request);
|
|
1172
|
+
}
|
|
1173
|
+
#wrapRequestWithUploadProgress(request, originalBody) {
|
|
1174
|
+
if (!this.#options.onUploadProgress || !request.body) {
|
|
1175
|
+
return request;
|
|
1176
|
+
}
|
|
1177
|
+
return streamRequest(request, this.#options.onUploadProgress, originalBody ?? this.#options.body ?? undefined);
|
|
576
1178
|
}
|
|
577
1179
|
}
|
|
578
1180
|
|
|
@@ -592,19 +1194,39 @@
|
|
|
592
1194
|
return createInstance(validateAndMerge(defaults, newDefaults));
|
|
593
1195
|
};
|
|
594
1196
|
ky.stop = stop;
|
|
1197
|
+
ky.retry = retry;
|
|
595
1198
|
return ky;
|
|
596
1199
|
};
|
|
597
1200
|
const ky$1 = createInstance();
|
|
1201
|
+
// Intentionally not exporting this for now as it's just an implementation detail and we don't want to commit to a certain API yet at least.
|
|
1202
|
+
// export {NonError} from './errors/NonError.js';
|
|
598
1203
|
|
|
599
1204
|
var distribution = /*#__PURE__*/Object.freeze({
|
|
600
1205
|
__proto__: null,
|
|
1206
|
+
ForceRetryError: ForceRetryError,
|
|
601
1207
|
HTTPError: HTTPError,
|
|
602
1208
|
TimeoutError: TimeoutError,
|
|
603
|
-
default: ky$1
|
|
1209
|
+
default: ky$1,
|
|
1210
|
+
isForceRetryError: isForceRetryError,
|
|
1211
|
+
isHTTPError: isHTTPError,
|
|
1212
|
+
isKyError: isKyError,
|
|
1213
|
+
isTimeoutError: isTimeoutError
|
|
604
1214
|
});
|
|
605
1215
|
|
|
606
1216
|
var require$$1 = /*@__PURE__*/getAugmentedNamespace(distribution);
|
|
607
1217
|
|
|
1218
|
+
const VERSION$1 = '0.14.1';
|
|
1219
|
+
|
|
1220
|
+
var constants = {
|
|
1221
|
+
VERSION: VERSION$1,
|
|
1222
|
+
USER_AGENT: `mql/${VERSION$1}`,
|
|
1223
|
+
/**
|
|
1224
|
+
* Based on require('got').defaults.options.retry.statusCodes
|
|
1225
|
+
* but without 429 (too many requests)
|
|
1226
|
+
*/
|
|
1227
|
+
RETRY_STATUS_CODES: [408, 413, 500, 502, 503, 504, 521, 522, 524]
|
|
1228
|
+
};
|
|
1229
|
+
|
|
608
1230
|
const ENDPOINT = {
|
|
609
1231
|
FREE: 'https://api.microlink.io/',
|
|
610
1232
|
PRO: 'https://pro.microlink.io/'
|
|
@@ -753,6 +1375,13 @@
|
|
|
753
1375
|
const { flattie: flatten } = dist;
|
|
754
1376
|
const { default: ky } = require$$1;
|
|
755
1377
|
|
|
1378
|
+
const { VERSION, USER_AGENT, RETRY_STATUS_CODES } = constants;
|
|
1379
|
+
|
|
1380
|
+
const kyInstance = ky.extend({
|
|
1381
|
+
headers: { 'user-agent': USER_AGENT },
|
|
1382
|
+
retry: { statusCodes: RETRY_STATUS_CODES }
|
|
1383
|
+
});
|
|
1384
|
+
|
|
756
1385
|
const factory = factory_1('arrayBuffer');
|
|
757
1386
|
|
|
758
1387
|
class MicrolinkError extends Error {
|
|
@@ -770,7 +1399,7 @@
|
|
|
770
1399
|
const got = async (url, { responseType, ...opts }) => {
|
|
771
1400
|
try {
|
|
772
1401
|
if (opts.timeout === undefined) opts.timeout = false;
|
|
773
|
-
const response = await
|
|
1402
|
+
const response = await kyInstance(url, opts);
|
|
774
1403
|
const body = await response[responseType]();
|
|
775
1404
|
const { headers, status: statusCode } = response;
|
|
776
1405
|
return { url: response.url, body, headers, statusCode }
|
|
@@ -794,30 +1423,30 @@
|
|
|
794
1423
|
}
|
|
795
1424
|
};
|
|
796
1425
|
|
|
797
|
-
got.stream = (...args) =>
|
|
1426
|
+
got.stream = (...args) => kyInstance(...args).then(res => res.body);
|
|
798
1427
|
|
|
799
1428
|
const mql = factory({
|
|
800
1429
|
MicrolinkError,
|
|
801
1430
|
got,
|
|
802
1431
|
flatten,
|
|
803
|
-
VERSION
|
|
1432
|
+
VERSION
|
|
804
1433
|
});
|
|
805
1434
|
|
|
806
|
-
lightweight
|
|
807
|
-
var arrayBuffer = lightweight
|
|
808
|
-
var extend = lightweight
|
|
809
|
-
var fetchFromApi = lightweight
|
|
810
|
-
var getApiUrl = lightweight
|
|
811
|
-
var mapRules = lightweight
|
|
812
|
-
var MicrolinkError_1 = lightweight
|
|
813
|
-
var version = lightweight
|
|
1435
|
+
lightweight.exports = mql;
|
|
1436
|
+
var arrayBuffer = lightweight.exports.arrayBuffer = mql.extend({ responseType: 'arrayBuffer' });
|
|
1437
|
+
var extend = lightweight.exports.extend = mql.extend;
|
|
1438
|
+
var fetchFromApi = lightweight.exports.fetchFromApi = mql.fetchFromApi;
|
|
1439
|
+
var getApiUrl = lightweight.exports.getApiUrl = mql.getApiUrl;
|
|
1440
|
+
var mapRules = lightweight.exports.mapRules = mql.mapRules;
|
|
1441
|
+
var MicrolinkError_1 = lightweight.exports.MicrolinkError = mql.MicrolinkError;
|
|
1442
|
+
var version = lightweight.exports.version = mql.version;
|
|
814
1443
|
|
|
815
|
-
var lightweightExports = lightweight
|
|
816
|
-
var
|
|
1444
|
+
var lightweightExports = lightweight.exports;
|
|
1445
|
+
var lightweight_default = /*@__PURE__*/getDefaultExportFromCjs(lightweightExports);
|
|
817
1446
|
|
|
818
1447
|
exports.MicrolinkError = MicrolinkError_1;
|
|
819
1448
|
exports.arrayBuffer = arrayBuffer;
|
|
820
|
-
exports.default =
|
|
1449
|
+
exports.default = lightweight_default;
|
|
821
1450
|
exports.extend = extend;
|
|
822
1451
|
exports.fetchFromApi = fetchFromApi;
|
|
823
1452
|
exports.getApiUrl = getApiUrl;
|