ky
Version:
Tiny and elegant HTTP client based on the Fetch API
77 lines • 4.21 kB
JavaScript
import { requestMethods } from '../core/constants.js';
import { isNonArrayObject, isNonNegativeNumber } from './is.js';
export const normalizeRequestMethod = (input) => requestMethods.includes(input.toLowerCase()) ? input.toUpperCase() : input;
const caseInsensitiveMethods = new Set([...requestMethods, 'options', 'trace']);
/**
Normalizes common methods for retry matching while preserving case-sensitive custom methods.
*/
export const normalizeRetryMethod = (method) => caseInsensitiveMethods.has(method.toLowerCase()) ? method.toLowerCase() : method;
const retryMethods = ['get', 'put', 'head', 'delete', 'options', 'trace', 'query'];
const retryStatusCodes = [408, 413, 429, 500, 502, 503, 504];
const retryAfterStatusCodes = [413, 429, 503];
const defaultRetryOptions = {
limit: 2,
methods: retryMethods,
statusCodes: retryStatusCodes,
afterStatusCodes: retryAfterStatusCodes,
maxRetryAfter: Number.POSITIVE_INFINITY,
backoffLimit: Number.POSITIVE_INFINITY,
delay: attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000,
jitter: undefined,
retryOnTimeout: false,
};
export const normalizeRetryOptions = (retry = {}) => {
if (typeof retry === 'number') {
retry = { limit: retry };
}
if (!isNonArrayObject(retry)) {
throw new TypeError('`retry` must be a number or an object');
}
const normalizedRetry = {
...defaultRetryOptions,
...Object.fromEntries(Object.entries(retry).filter(([, value]) => value !== undefined)),
};
// Validates the retry limit. An omitted one already got the default above, since `undefined` values are filtered out.
if (!Number.isInteger(normalizedRetry.limit) || normalizedRetry.limit < 0) {
throw new TypeError('`retry.limit` must be a finite, non-negative integer');
}
for (const key of ['methods', 'statusCodes', 'afterStatusCodes']) {
if (!Array.isArray(normalizedRetry[key])) {
// eslint-disable-next-line unicorn/prefer-type-error -- Preserve the existing error type.
throw new Error(`retry.${key} must be an array`);
}
}
// A mistyped entry only ever fails to match, so `statusCodes: ['429']` quietly disabled retrying instead of reporting the typo.
for (const [key, isValid] of [
['methods', (value) => typeof value === 'string'],
['statusCodes', (value) => Number.isInteger(value)],
['afterStatusCodes', (value) => Number.isInteger(value)],
]) {
if (normalizedRetry[key].some(value => !isValid(value))) {
throw new TypeError(`\`retry.${key}\` must only contain ${key === 'methods' ? 'strings' : 'numbers'}`);
}
}
// Both limits are passed to `Math.min()`, so a non-number silently turns every delay into `NaN`, which `setTimeout()` clamps to 1ms. That defeats the whole point of the limits.
for (const key of ['maxRetryAfter', 'backoffLimit']) {
if (!isNonNegativeNumber(normalizedRetry[key])) {
throw new TypeError(`\`retry.${key}\` must be a non-negative number or \`Infinity\``);
}
}
// These run while deciding what to do with the original failure, so an invalid shape would replace the `HTTPError`/`NetworkError` that caused the retry with a `TypeError` from deep inside Ky.
for (const key of ['delay', 'shouldRetry']) {
if (normalizedRetry[key] !== undefined && typeof normalizedRetry[key] !== 'function') {
throw new TypeError(`\`retry.${key}\` must be a function`);
}
}
if (normalizedRetry.jitter !== undefined && typeof normalizedRetry.jitter !== 'boolean' && typeof normalizedRetry.jitter !== 'function') {
throw new TypeError('`retry.jitter` must be a boolean or a function');
}
if (typeof normalizedRetry.retryOnTimeout !== 'boolean') {
throw new TypeError('`retry.retryOnTimeout` must be a boolean');
}
normalizedRetry.methods = normalizedRetry.methods.map(method => normalizeRetryMethod(method));
normalizedRetry.statusCodes = [...normalizedRetry.statusCodes];
normalizedRetry.afterStatusCodes = [...normalizedRetry.afterStatusCodes];
return normalizedRetry;
};
//# sourceMappingURL=normalize.js.map