UNPKG

retry-axios

Version:
243 lines 9.32 kB
import axios, { isCancel, } from 'axios'; /** * Attach the interceptor to the Axios instance. * @param instance The optional Axios instance on which to attach the * interceptor. * @returns The id of the interceptor attached to the axios instance. */ export function attach(instance) { instance = instance || axios; return instance.interceptors.response.use(onFulfilled, async (error) => onError(instance, error)); } /** * Eject the Axios interceptor that is providing retry capabilities. * @param interceptorId The interceptorId provided in the config. * @param instance The axios instance using this interceptor. */ export function detach(interceptorId, instance) { instance = instance || axios; instance.interceptors.response.eject(interceptorId); } function onFulfilled(result) { return result; } /** * Some versions of axios are converting arrays into objects during retries. * This will attempt to convert an object with the following structure into * an array, where the keys correspond to the indices: * { * 0: { * // some property * }, * 1: { * // another * } * } * @param obj The object that (may) have integers that correspond to an index * @returns An array with the pucked values */ function normalizeArray(object) { const array = []; if (!object) { return undefined; } if (Array.isArray(object)) { return object; } if (typeof object === 'object') { for (const key of Object.keys(object)) { const number_ = Number.parseInt(key, 10); if (!Number.isNaN(number_)) { array[number_] = object[key]; } } } return array; } /** * Parse the Retry-After header. * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After * @param header Retry-After header value * @returns Number of milliseconds, or undefined if invalid */ function parseRetryAfter(header) { // Header value may be string containing integer seconds const value = Number(header); if (!Number.isNaN(value)) { return value * 1000; } // Or HTTP date time string const dateTime = Date.parse(header); if (!Number.isNaN(dateTime)) { return dateTime - Date.now(); } return undefined; } async function onError(instance, error) { if (isCancel(error)) { throw error; } const config = getConfig(error) || {}; config.currentRetryAttempt = config.currentRetryAttempt || 0; config.retry = typeof config.retry === 'number' ? config.retry : 3; config.retryDelay = typeof config.retryDelay === 'number' ? config.retryDelay : 100; config.instance = config.instance || instance; config.backoffType = config.backoffType || 'exponential'; config.httpMethodsToRetry = normalizeArray(config.httpMethodsToRetry) || [ 'GET', 'HEAD', 'PUT', 'OPTIONS', 'DELETE', ]; config.noResponseRetries = typeof config.noResponseRetries === 'number' ? config.noResponseRetries : 2; config.checkRetryAfter = typeof config.checkRetryAfter === 'boolean' ? config.checkRetryAfter : true; config.maxRetryAfter = typeof config.maxRetryAfter === 'number' ? config.maxRetryAfter : 60000 * 5; // If this wasn't in the list of status codes where we want // to automatically retry, return. const retryRanges = [ // https://en.wikipedia.org/wiki/List_of_HTTP_status_codes // 1xx - Retry (Informational, request still processing) // 2xx - Do not retry (Success) // 3xx - Do not retry (Redirect) // 4xx - Do not retry (Client errors) // 429 - Retry ("Too Many Requests") // 5xx - Retry (Server errors) [100, 199], [429, 429], [500, 599], ]; config.statusCodesToRetry = normalizeArray(config.statusCodesToRetry) || retryRanges; // Put the config back into the err const axiosError = error; axiosError.config = axiosError.config || {}; // Allow for wider range of errors axiosError.config.raxConfig = { ...config }; // Determine if we should retry the request const shouldRetryFn = config.shouldRetry || shouldRetryRequest; if (!shouldRetryFn(axiosError)) { throw axiosError; } // Create a promise that invokes the retry after the backOffDelay const onBackoffPromise = new Promise((resolve, reject) => { let delay = 0; // If enabled, check for 'Retry-After' header in response to use as delay if (config.checkRetryAfter && axiosError.response?.headers['retry-after']) { const retryAfter = parseRetryAfter(axiosError.response.headers['retry-after']); if (retryAfter && retryAfter > 0 && retryAfter <= config.maxRetryAfter) { delay = retryAfter; } else { reject(axiosError); return; } } // Now it's certain that a retry is supposed to happen. Incremenent the // counter, critical for linear and exp backoff delay calc. Note that // `config.currentRetryAttempt` is local to this function whereas // `(err.config as RaxConfig).raxConfig` is state that is tranferred across // retries. That is, we want to mutate `(err.config as // RaxConfig).raxConfig`. Another important note is about the definition of // `currentRetryAttempt`: When we are here becasue the first and actual // HTTP request attempt failed then `currentRetryAttempt` is still zero. We // have found that a retry is indeed required. Since that is (will be) // indeed the first retry it makes sense to now increase // `currentRetryAttempt` by 1. So that it is in fact 1 for the first retry // (as opposed to 0 or 2); an intuitive convention to use for the math // below. axiosError.config.raxConfig.currentRetryAttempt += 1; // Store with shorter and more expressive variable name. const retrycount = axiosError.config.raxConfig .currentRetryAttempt; // Calculate delay according to chosen strategy // Default to exponential backoff - formula: ((2^c - 1) / 2) * 1000 if (delay === 0) { // Was not set by Retry-After logic if (config.backoffType === 'linear') { // The delay between the first (actual) attempt and the first retry // should be non-zero. Rely on the convention that `retrycount` is // equal to 1 for the first retry when we are in here (was once 0, // which was a bug -- see #122). delay = retrycount * 1000; } else if (config.backoffType === 'static') { delay = config.retryDelay; } else { delay = ((2 ** retrycount - 1) / 2) * 1000; } if (typeof config.maxRetryDelay === 'number') { delay = Math.min(delay, config.maxRetryDelay); } } setTimeout(resolve, delay); }); // Notify the user if they added an `onRetryAttempt` handler if (config.onRetryAttempt) { config.onRetryAttempt(axiosError); } const onRetryAttemptPromise = Promise.resolve(); // Return the promise in which recalls axios to retry the request return Promise.resolve() .then(async () => onBackoffPromise) .then(async () => onRetryAttemptPromise) .then(async () => config.instance.request(axiosError.config)); } /** * Determine based on config if we should retry the request. * @param err The AxiosError passed to the interceptor. */ export function shouldRetryRequest(error) { const config = error.config.raxConfig; // If there's no config, or retries are disabled, return. if (!config || config.retry === 0) { return false; } // Check if this error has no response (ETIMEDOUT, ENOTFOUND, etc) if (!error.response && (config.currentRetryAttempt || 0) >= config.noResponseRetries) { return false; } // Only retry with configured HttpMethods. if (!error.config?.method || !config.httpMethodsToRetry.includes(error.config.method.toUpperCase())) { return false; } // If this wasn't in the list of status codes where we want // to automatically retry, return. if (error.response?.status) { let isInRange = false; for (const [min, max] of config.statusCodesToRetry) { const { status } = error.response; if (status >= min && status <= max) { isInRange = true; break; } } if (!isInRange) { return false; } } // If we are out of retry attempts, return config.currentRetryAttempt = config.currentRetryAttempt || 0; if (config.currentRetryAttempt >= config.retry) { return false; } return true; } /** * Acquire the raxConfig object from an AxiosError if available. * @param err The Axios error with a config object. */ export function getConfig(error) { if (error?.config) { return error.config.raxConfig; } } //# sourceMappingURL=index.js.map