index.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  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. export const DEFAULT_OPTIONS = {
  66. retries: 3,
  67. retryCondition: isNetworkOrIdempotentRequestError,
  68. retryDelay: noDelay,
  69. shouldResetTimeout: false,
  70. onRetry: () => { },
  71. onMaxRetryTimesExceeded: () => { },
  72. validateResponse: null
  73. };
  74. function getRequestOptions(config, defaultOptions) {
  75. return { ...DEFAULT_OPTIONS, ...defaultOptions, ...config[namespace] };
  76. }
  77. function setCurrentState(config, defaultOptions) {
  78. const currentState = getRequestOptions(config, defaultOptions || {});
  79. currentState.retryCount = currentState.retryCount || 0;
  80. currentState.lastRequestTime = currentState.lastRequestTime || Date.now();
  81. config[namespace] = currentState;
  82. return currentState;
  83. }
  84. function fixConfig(axiosInstance, config) {
  85. // @ts-ignore
  86. if (axiosInstance.defaults.agent === config.agent) {
  87. // @ts-ignore
  88. delete config.agent;
  89. }
  90. if (axiosInstance.defaults.httpAgent === config.httpAgent) {
  91. delete config.httpAgent;
  92. }
  93. if (axiosInstance.defaults.httpsAgent === config.httpsAgent) {
  94. delete config.httpsAgent;
  95. }
  96. }
  97. async function shouldRetry(currentState, error) {
  98. const { retries, retryCondition } = currentState;
  99. const shouldRetryOrPromise = (currentState.retryCount || 0) < retries && retryCondition(error);
  100. // This could be a promise
  101. if (typeof shouldRetryOrPromise === 'object') {
  102. try {
  103. const shouldRetryPromiseResult = await shouldRetryOrPromise;
  104. // keep return true unless shouldRetryPromiseResult return false for compatibility
  105. return shouldRetryPromiseResult !== false;
  106. }
  107. catch (_err) {
  108. return false;
  109. }
  110. }
  111. return shouldRetryOrPromise;
  112. }
  113. async function handleRetry(axiosInstance, currentState, error, config) {
  114. currentState.retryCount += 1;
  115. const { retryDelay, shouldResetTimeout, onRetry } = currentState;
  116. const delay = retryDelay(currentState.retryCount, error);
  117. // Axios fails merging this configuration to the default configuration because it has an issue
  118. // with circular structures: https://github.com/mzabriskie/axios/issues/370
  119. fixConfig(axiosInstance, config);
  120. if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
  121. const lastRequestDuration = Date.now() - currentState.lastRequestTime;
  122. const timeout = config.timeout - lastRequestDuration - delay;
  123. if (timeout <= 0) {
  124. return Promise.reject(error);
  125. }
  126. config.timeout = timeout;
  127. }
  128. config.transformRequest = [(data) => data];
  129. await onRetry(currentState.retryCount, error, config);
  130. if (config.signal?.aborted) {
  131. return Promise.resolve(axiosInstance(config));
  132. }
  133. return new Promise((resolve) => {
  134. const abortListener = () => {
  135. clearTimeout(timeout);
  136. resolve(axiosInstance(config));
  137. };
  138. const timeout = setTimeout(() => {
  139. resolve(axiosInstance(config));
  140. if (config.signal?.removeEventListener) {
  141. config.signal.removeEventListener('abort', abortListener);
  142. }
  143. }, delay);
  144. if (config.signal?.addEventListener) {
  145. config.signal.addEventListener('abort', abortListener, { once: true });
  146. }
  147. });
  148. }
  149. async function handleMaxRetryTimesExceeded(currentState, error) {
  150. if (currentState.retryCount >= currentState.retries)
  151. await currentState.onMaxRetryTimesExceeded(error, currentState.retryCount);
  152. }
  153. const axiosRetry = (axiosInstance, defaultOptions) => {
  154. const requestInterceptorId = axiosInstance.interceptors.request.use((config) => {
  155. setCurrentState(config, defaultOptions);
  156. if (config[namespace]?.validateResponse) {
  157. // by setting this, all HTTP responses will be go through the error interceptor first
  158. config.validateStatus = () => false;
  159. }
  160. return config;
  161. });
  162. const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => {
  163. const { config } = error;
  164. // If we have no information to retry the request
  165. if (!config) {
  166. return Promise.reject(error);
  167. }
  168. const currentState = setCurrentState(config, defaultOptions);
  169. if (error.response && currentState.validateResponse?.(error.response)) {
  170. // no issue with response
  171. return error.response;
  172. }
  173. if (await shouldRetry(currentState, error)) {
  174. return handleRetry(axiosInstance, currentState, error, config);
  175. }
  176. await handleMaxRetryTimesExceeded(currentState, error);
  177. return Promise.reject(error);
  178. });
  179. return { requestInterceptorId, responseInterceptorId };
  180. };
  181. // Compatibility with CommonJS
  182. axiosRetry.isNetworkError = isNetworkError;
  183. axiosRetry.isSafeRequestError = isSafeRequestError;
  184. axiosRetry.isIdempotentRequestError = isIdempotentRequestError;
  185. axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
  186. axiosRetry.exponentialDelay = exponentialDelay;
  187. axiosRetry.isRetryableError = isRetryableError;
  188. export default axiosRetry;