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