index.js 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. import isRetryAllowed from 'is-retry-allowed';
  2. export const namespace = 'axios-retry';
  3. export function isNetworkError(error) {
  4. const CODE_EXCLUDE_LIST = ['ERR_CANCELED', 'ECONNABORTED'];
  5. if (error.response) {
  6. return false;
  7. }
  8. if (!error.code) {
  9. return false;
  10. }
  11. // Prevents retrying timed out & cancelled requests
  12. if (CODE_EXCLUDE_LIST.includes(error.code)) {
  13. return false;
  14. }
  15. // Prevents retrying unsafe errors
  16. return isRetryAllowed(error);
  17. }
  18. const SAFE_HTTP_METHODS = ['get', 'head', 'options'];
  19. const IDEMPOTENT_HTTP_METHODS = SAFE_HTTP_METHODS.concat(['put', 'delete']);
  20. export function isRetryableError(error) {
  21. return (error.code !== 'ECONNABORTED' &&
  22. (!error.response ||
  23. error.response.status === 429 ||
  24. (error.response.status >= 500 && error.response.status <= 599)));
  25. }
  26. export function isSafeRequestError(error) {
  27. if (!error.config?.method) {
  28. // Cannot determine if the request can be retried
  29. return false;
  30. }
  31. return isRetryableError(error) && SAFE_HTTP_METHODS.indexOf(error.config.method) !== -1;
  32. }
  33. export function isIdempotentRequestError(error) {
  34. if (!error.config?.method) {
  35. // Cannot determine if the request can be retried
  36. return false;
  37. }
  38. return isRetryableError(error) && IDEMPOTENT_HTTP_METHODS.indexOf(error.config.method) !== -1;
  39. }
  40. export function isNetworkOrIdempotentRequestError(error) {
  41. return isNetworkError(error) || isIdempotentRequestError(error);
  42. }
  43. export function retryAfter(error = undefined) {
  44. const retryAfterHeader = error?.response?.headers['retry-after'];
  45. if (!retryAfterHeader) {
  46. return 0;
  47. }
  48. // if the retry after header is a number, convert it to milliseconds
  49. let retryAfterMs = (Number(retryAfterHeader) || 0) * 1000;
  50. // If the retry after header is a date, get the number of milliseconds until that date
  51. if (retryAfterMs === 0) {
  52. retryAfterMs = (new Date(retryAfterHeader).valueOf() || 0) - Date.now();
  53. }
  54. return Math.max(0, retryAfterMs);
  55. }
  56. function noDelay(_retryNumber = 0, error = undefined) {
  57. return Math.max(0, retryAfter(error));
  58. }
  59. export function exponentialDelay(retryNumber = 0, error = undefined, delayFactor = 100) {
  60. const calculatedDelay = 2 ** retryNumber * delayFactor;
  61. const delay = Math.max(calculatedDelay, retryAfter(error));
  62. const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
  63. return delay + randomSum;
  64. }
  65. /**
  66. * Linear delay
  67. * @param {number | undefined} delayFactor - delay factor in milliseconds (default: 100)
  68. * @returns {function} (retryNumber: number, error: AxiosError | undefined) => number
  69. */
  70. export function linearDelay(delayFactor = 100) {
  71. return (retryNumber = 0, error = undefined) => {
  72. const delay = retryNumber * delayFactor;
  73. return Math.max(delay, retryAfter(error));
  74. };
  75. }
  76. export const DEFAULT_OPTIONS = {
  77. retries: 3,
  78. retryCondition: isNetworkOrIdempotentRequestError,
  79. retryDelay: noDelay,
  80. shouldResetTimeout: false,
  81. onRetry: () => { },
  82. onMaxRetryTimesExceeded: () => { },
  83. validateResponse: null
  84. };
  85. function getRequestOptions(config, defaultOptions) {
  86. return { ...DEFAULT_OPTIONS, ...defaultOptions, ...config[namespace] };
  87. }
  88. function setCurrentState(config, defaultOptions, resetLastRequestTime = false) {
  89. const currentState = getRequestOptions(config, defaultOptions || {});
  90. currentState.retryCount = currentState.retryCount || 0;
  91. if (!currentState.lastRequestTime || resetLastRequestTime) {
  92. currentState.lastRequestTime = Date.now();
  93. }
  94. config[namespace] = currentState;
  95. return currentState;
  96. }
  97. function fixConfig(axiosInstance, config) {
  98. // @ts-ignore
  99. if (axiosInstance.defaults.agent === config.agent) {
  100. // @ts-ignore
  101. delete config.agent;
  102. }
  103. if (axiosInstance.defaults.httpAgent === config.httpAgent) {
  104. delete config.httpAgent;
  105. }
  106. if (axiosInstance.defaults.httpsAgent === config.httpsAgent) {
  107. delete config.httpsAgent;
  108. }
  109. }
  110. async function shouldRetry(currentState, error) {
  111. const { retries, retryCondition } = currentState;
  112. const shouldRetryOrPromise = (currentState.retryCount || 0) < retries && retryCondition(error);
  113. // This could be a promise
  114. if (typeof shouldRetryOrPromise === 'object') {
  115. try {
  116. const shouldRetryPromiseResult = await shouldRetryOrPromise;
  117. // keep return true unless shouldRetryPromiseResult return false for compatibility
  118. return shouldRetryPromiseResult !== false;
  119. }
  120. catch (_err) {
  121. return false;
  122. }
  123. }
  124. return shouldRetryOrPromise;
  125. }
  126. async function handleRetry(axiosInstance, currentState, error, config) {
  127. currentState.retryCount += 1;
  128. const { retryDelay, shouldResetTimeout, onRetry } = currentState;
  129. const delay = retryDelay(currentState.retryCount, error);
  130. // Axios fails merging this configuration to the default configuration because it has an issue
  131. // with circular structures: https://github.com/mzabriskie/axios/issues/370
  132. fixConfig(axiosInstance, config);
  133. if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
  134. const lastRequestDuration = Date.now() - currentState.lastRequestTime;
  135. const timeout = config.timeout - lastRequestDuration - delay;
  136. if (timeout <= 0) {
  137. return Promise.reject(error);
  138. }
  139. config.timeout = timeout;
  140. }
  141. config.transformRequest = [(data) => data];
  142. await onRetry(currentState.retryCount, error, config);
  143. if (config.signal?.aborted) {
  144. return Promise.resolve(axiosInstance(config));
  145. }
  146. return new Promise((resolve) => {
  147. const abortListener = () => {
  148. clearTimeout(timeout);
  149. resolve(axiosInstance(config));
  150. };
  151. const timeout = setTimeout(() => {
  152. resolve(axiosInstance(config));
  153. if (config.signal?.removeEventListener) {
  154. config.signal.removeEventListener('abort', abortListener);
  155. }
  156. }, delay);
  157. if (config.signal?.addEventListener) {
  158. config.signal.addEventListener('abort', abortListener, { once: true });
  159. }
  160. });
  161. }
  162. async function handleMaxRetryTimesExceeded(currentState, error) {
  163. if (currentState.retryCount >= currentState.retries)
  164. await currentState.onMaxRetryTimesExceeded(error, currentState.retryCount);
  165. }
  166. const axiosRetry = (axiosInstance, defaultOptions) => {
  167. const requestInterceptorId = axiosInstance.interceptors.request.use((config) => {
  168. setCurrentState(config, defaultOptions, true);
  169. if (config[namespace]?.validateResponse) {
  170. // by setting this, all HTTP responses will be go through the error interceptor first
  171. config.validateStatus = () => false;
  172. }
  173. return config;
  174. });
  175. const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => {
  176. const { config } = error;
  177. // If we have no information to retry the request
  178. if (!config) {
  179. return Promise.reject(error);
  180. }
  181. const currentState = setCurrentState(config, defaultOptions);
  182. if (error.response && currentState.validateResponse?.(error.response)) {
  183. // no issue with response
  184. return error.response;
  185. }
  186. if (await shouldRetry(currentState, error)) {
  187. return handleRetry(axiosInstance, currentState, error, config);
  188. }
  189. await handleMaxRetryTimesExceeded(currentState, error);
  190. return Promise.reject(error);
  191. });
  192. return { requestInterceptorId, responseInterceptorId };
  193. };
  194. // Compatibility with CommonJS
  195. axiosRetry.isNetworkError = isNetworkError;
  196. axiosRetry.isSafeRequestError = isSafeRequestError;
  197. axiosRetry.isIdempotentRequestError = isIdempotentRequestError;
  198. axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
  199. axiosRetry.exponentialDelay = exponentialDelay;
  200. axiosRetry.linearDelay = linearDelay;
  201. axiosRetry.isRetryableError = isRetryableError;
  202. export default axiosRetry;