Skip to content

Axios retry 请求重试插件

axios-retry 是一个用于axios的请求重试插件,用于在请求失败时自动重试请求。支持自定义重试规则。

实现源码
ts
import type {
  AxiosError,
  AxiosResponse,
  InternalAxiosRequestConfig
} from 'axios';

export interface IOptions<
  T extends AxiosError = AxiosError,
  U extends AxiosResponse = AxiosResponse
> {
  maxRetryCount: number;
  retryDelay: number;
  request: (config: InternalAxiosRequestConfig) => Promise<AxiosResponse>;
  isRetry: (error?: T, res?: U) => boolean;
  beforeRetry?: (retryCount: number, error?: T, res?: U) => void;
  failed?: (retryCount: number, error?: T, res?: U) => void;
}

export type IConfig = Partial<IOptions> & Pick<IOptions, 'request'>;

export class AxiosRetryPlugin {
  private options: IOptions;
  private histories: Map<string, number>;

  constructor(options: IConfig) {
    this.options = {
      request: options.request,
      maxRetryCount: options.maxRetryCount ?? 3,
      retryDelay: options.retryDelay ?? 1000,
      isRetry:
        options.isRetry ??
        ((err) => {
          if (err) {
            return true;
          }

          return false;
        })
    };

    this.histories = new Map();
  }

  static getDataType(obj: any) {
    let res = Object.prototype.toString.call(obj).split(' ')[1];
    res = res.substring(0, res.length - 1).toLowerCase();
    return res;
  }

  static generateRequestKey(config: InternalAxiosRequestConfig): string {
    const { method, url, data, params } = config;
    let key = `${method}-${url}`;

    try {
      switch (AxiosRetryPlugin.getDataType(data)) {
        case 'object':
          key += `-${JSON.stringify(data)}`;
          break;
        case 'formdata':
          for (const [k, v] of data.entries()) {
            if (v instanceof Blob) {
              continue;
            }
            key += `-${k}-${v}`;
          }
          break;
        default:
          break;
      }

      if (AxiosRetryPlugin.getDataType(params) === 'object') {
        key += `-${JSON.stringify(params)}`;
      }
    } catch (e) {
      /* empty */
    }

    return key;
  }

  private fetch(config: InternalAxiosRequestConfig, beforeRetry?: () => void) {
    return new Promise<AxiosResponse>((resolve, reject) => {
      if (!this.options.retryDelay || this.options.retryDelay < 0) {
        beforeRetry && beforeRetry();
        this.options.request(config).then(resolve).catch(reject);
        return;
      }

      setTimeout(() => {
        beforeRetry && beforeRetry();
        this.options.request(config).then(resolve).catch(reject);
      }, this.options.retryDelay);
    });
  }

  private retryRequest(error: AxiosError): Promise<AxiosResponse>;
  private retryRequest(error: undefined, res: AxiosResponse): AxiosResponse;
  private retryRequest(error?: AxiosError, res?: AxiosResponse) {
    if (!this.options.isRetry(error, res)) {
      if (error) {
        return Promise.reject(error);
      }

      return res!;
    }

    let config: InternalAxiosRequestConfig;
    if (error) {
      if (!error.config) {
        return Promise.reject(error);
      }
      config = error.config;
    } else {
      config = res!.config;
    }

    const key = AxiosRetryPlugin.generateRequestKey(config);
    const retryCount = this.histories.get(key) || 0;

    if (retryCount >= this.options.maxRetryCount) {
      this.options.failed?.(retryCount, error, res);
      this.histories.delete(key);

      if (error) {
        return Promise.reject(error);
      }
      return res!;
    }

    const newCount = retryCount + 1;
    this.histories.set(key, newCount);

    return this.fetch(config, () => {
      this.options.beforeRetry?.(newCount, error, res);
    });
  }

  responseInterceptorFulfilled(response: AxiosResponse) {
    return this.retryRequest(undefined, response);
  }

  responseInterceptorRejected(error: AxiosError) {
    return this.retryRequest(error);
  }
}

export default function createAxiosRetryPlugin(config: IConfig) {
  const instance = new AxiosRetryPlugin(config);

  return {
    responseInterceptorFulfilled:
      instance.responseInterceptorFulfilled.bind(instance),
    responseInterceptorRejected:
      instance.responseInterceptorRejected.bind(instance)
  };
}

基本用法

简单用法

先创建插件实例,然后将插件实例的拦截器方法注册到 axios 拦截器中即可。

ts
import axios from 'axios';
import createAxiosRetryPlugin from '...';

const instance = axios.create();

const retry = createAxiosRetryPlugin({ request: instance.request });
instance.interceptors.response.use(
  undefined,
  retry.responseInterceptorRejected
);

高级用法

可以通过传入配置项来定制插件的行为。

ts
import axios, type { AxiosError, AxiosResponse } from 'axios'
import createAxiosRetryPlugin from '...'

const instance = axios.create()

const retry = createAxiosRetryPlugin({
  request: instance.request,
  maxRetryCount: 3,
  retryDelay: 1000,
  isRetry: (error?: AxiosError, res?: AxiosResponse) => {
    return error?.response?.status !== 401 && res?.data.statusCode !== 401
  }
})
instance.interceptors.response.use(
  retry.responseInterceptorFulfilled,
  retry.responseInterceptorRejected
)

配置项

request

  • 类型(config: AxiosRequestConfig) => Promise<AxiosResponse>
  • 默认值undefined
  • 说明:重试请求方法,建议传入axios 实例的 request 方法
  • 必填:是

maxRetryCount

  • 类型number
  • 默认值3
  • 说明:最大重试次数
  • 必填:否

retryDelay

  • 类型number
  • 默认值1000
  • 说明:重试延迟时间(毫秒)
  • 必填:否

isRetry

  • 类型(error?: AxiosError, res?: AxiosResponse) => boolean
  • 默认值(err) => { if (err) { return true; } return false; }
  • 说明:是否重试的判断函数
  • 必填:否

beforeRetry

  • 类型(retryCount: number, error?: AxiosError, res?: AxiosResponse) => void
  • 默认值undefined
  • 说明:重试前的回调函数,每次重试前都会调用
  • 必填:否

failed

  • 类型(retryCount: number, error?: AxiosError, res?: AxiosResponse) => void
  • 默认值undefined
  • 说明:重试失败的回调函数,当重试次数达到最大重试次数时调用
  • 必填:否

Released under the MIT License.