index.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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 || (error.response.status >= 500 && error.response.status <= 599)));
  23. }
  24. export function isSafeRequestError(error) {
  25. if (!error.config?.method) {
  26. // Cannot determine if the request can be retried
  27. return false;
  28. }
  29. return isRetryableError(error) && SAFE_HTTP_METHODS.indexOf(error.config.method) !== -1;
  30. }
  31. export function isIdempotentRequestError(error) {
  32. if (!error.config?.method) {
  33. // Cannot determine if the request can be retried
  34. return false;
  35. }
  36. return isRetryableError(error) && IDEMPOTENT_HTTP_METHODS.indexOf(error.config.method) !== -1;
  37. }
  38. export function isNetworkOrIdempotentRequestError(error) {
  39. return isNetworkError(error) || isIdempotentRequestError(error);
  40. }
  41. function noDelay() {
  42. return 0;
  43. }
  44. export function exponentialDelay(retryNumber = 0, _error = undefined, delayFactor = 100) {
  45. const delay = 2 ** retryNumber * delayFactor;
  46. const randomSum = delay * 0.2 * Math.random(); // 0-20% of the delay
  47. return delay + randomSum;
  48. }
  49. export const DEFAULT_OPTIONS = {
  50. retries: 3,
  51. retryCondition: isNetworkOrIdempotentRequestError,
  52. retryDelay: noDelay,
  53. shouldResetTimeout: false,
  54. onRetry: () => { }
  55. };
  56. function getRequestOptions(config, defaultOptions) {
  57. return { ...DEFAULT_OPTIONS, ...defaultOptions, ...config[namespace] };
  58. }
  59. function setCurrentState(config, defaultOptions) {
  60. const currentState = getRequestOptions(config, defaultOptions || {});
  61. currentState.retryCount = currentState.retryCount || 0;
  62. currentState.lastRequestTime = currentState.lastRequestTime || Date.now();
  63. config[namespace] = currentState;
  64. return currentState;
  65. }
  66. function fixConfig(axiosInstance, config) {
  67. // @ts-ignore
  68. if (axiosInstance.defaults.agent === config.agent) {
  69. // @ts-ignore
  70. delete config.agent;
  71. }
  72. if (axiosInstance.defaults.httpAgent === config.httpAgent) {
  73. delete config.httpAgent;
  74. }
  75. if (axiosInstance.defaults.httpsAgent === config.httpsAgent) {
  76. delete config.httpsAgent;
  77. }
  78. }
  79. async function shouldRetry(currentState, error) {
  80. const { retries, retryCondition } = currentState;
  81. const shouldRetryOrPromise = (currentState.retryCount || 0) < retries && retryCondition(error);
  82. // This could be a promise
  83. if (typeof shouldRetryOrPromise === 'object') {
  84. try {
  85. const shouldRetryPromiseResult = await shouldRetryOrPromise;
  86. // keep return true unless shouldRetryPromiseResult return false for compatibility
  87. return shouldRetryPromiseResult !== false;
  88. }
  89. catch (_err) {
  90. return false;
  91. }
  92. }
  93. return shouldRetryOrPromise;
  94. }
  95. const axiosRetry = (axiosInstance, defaultOptions) => {
  96. const requestInterceptorId = axiosInstance.interceptors.request.use((config) => {
  97. setCurrentState(config, defaultOptions);
  98. return config;
  99. });
  100. const responseInterceptorId = axiosInstance.interceptors.response.use(null, async (error) => {
  101. const { config } = error;
  102. // If we have no information to retry the request
  103. if (!config) {
  104. return Promise.reject(error);
  105. }
  106. const currentState = setCurrentState(config, defaultOptions);
  107. if (await shouldRetry(currentState, error)) {
  108. currentState.retryCount += 1;
  109. const { retryDelay, shouldResetTimeout, onRetry } = currentState;
  110. const delay = retryDelay(currentState.retryCount, error);
  111. // Axios fails merging this configuration to the default configuration because it has an issue
  112. // with circular structures: https://github.com/mzabriskie/axios/issues/370
  113. fixConfig(axiosInstance, config);
  114. if (!shouldResetTimeout && config.timeout && currentState.lastRequestTime) {
  115. const lastRequestDuration = Date.now() - currentState.lastRequestTime;
  116. const timeout = config.timeout - lastRequestDuration - delay;
  117. if (timeout <= 0) {
  118. return Promise.reject(error);
  119. }
  120. config.timeout = timeout;
  121. }
  122. config.transformRequest = [(data) => data];
  123. await onRetry(currentState.retryCount, error, config);
  124. return new Promise((resolve) => {
  125. setTimeout(() => resolve(axiosInstance(config)), delay);
  126. });
  127. }
  128. return Promise.reject(error);
  129. });
  130. return { requestInterceptorId, responseInterceptorId };
  131. };
  132. // Compatibility with CommonJS
  133. axiosRetry.isNetworkError = isNetworkError;
  134. axiosRetry.isSafeRequestError = isSafeRequestError;
  135. axiosRetry.isIdempotentRequestError = isIdempotentRequestError;
  136. axiosRetry.isNetworkOrIdempotentRequestError = isNetworkOrIdempotentRequestError;
  137. axiosRetry.exponentialDelay = exponentialDelay;
  138. axiosRetry.isRetryableError = isRetryableError;
  139. export default axiosRetry;