Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[业务产出] axios management 最佳实践 #44

Open
PeterChen1997 opened this issue Jul 18, 2022 · 0 comments
Open

[业务产出] axios management 最佳实践 #44

PeterChen1997 opened this issue Jul 18, 2022 · 0 comments

Comments

@PeterChen1997
Copy link
Owner

PeterChen1997 commented Jul 18, 2022

// 常量
const TRANSACTION_ID_IN_HEADER = 'X-Transaction-ID';
const REQUEST_START_AT_IN_HEADER = 'X-Request-Start-At';
const RETRY_COUNT = 2;

// 不记录请求列表(此处使用白名单过滤请求,添加后请确认是否生效)
const ignoreUrlList = ['/test/not/record'];

export const isRequestNeedRecord = (config: AxiosRequestConfig) => {
  if (!config.url) {
    return false;
  }

  for (const url of ignoreUrlList) {
    if (config.url.startsWith(url)) {
      return false;
    }
  }

  return true;
};

// 注册 axios retry 逻辑 - 这个库的逻辑编写的一般,可以自行编写替换
const registerAxiosRetry = (newInstance: AxiosInstance, isExternalRequest?: boolean) => {
  axiosRetry(newInstance, {
    retries: RETRY_COUNT,
    shouldResetTimeout: true,
    retryDelay: (retryCount) => retryCount * 1000,
    retryCondition: async (error: Error | AxiosError) => {
      if (!axios.isAxiosError(error)) {
        return false;
      }

      const check = async () => {
        // 默认只 retry 5xx 和 network error
        if (isNetworkOrIdempotentRequestError(error)) {
          return true;
        }

        // retry ECONNABORTED 类型的错误
        if (isConnectAbortedError(error)) {
          return true;
        }

        // 认证失败后,先尝试 renew token,再进行 retry
        if (!isExternalRequest && isAuthExpired(error)) {
          return await renewAuthToken();
        }

        return false;
      };

      /*
       * 此处需要包装函数用于兼容 axiosRetry 的逻辑,在 promise 类型的回调中,只有 throw error 才能终止 retry
       * 详见 https://github.com/softonic/axios-retry/pull/196
       */
      const needRetry = await check();

      if (!needRetry) {
        throw new Error('should not retry');
      }

      return true;
    },
  });
};

const registerRequestInterceptor = (newInstance: AxiosInstance, isExternalRequest?: boolean) => {
  newInstance.interceptors.request.use(
    (config: AxiosRequestConfig) => {
      const transactionId = v4();
      const startAt = dayjs().format('YYYY-MM-DD HH:mm:ss.SSS');
			const authContent = 'xxx'

      return {
        ...config,
        headers: {
          ...config.headers,
          // add authorization info
          ...(!isExternalRequest
            ? { Authorization: `Bearer ${authContent}` }
            : undefined),
          // add transaction id
          [TRANSACTION_ID_IN_HEADER]: transactionId,
          [REQUEST_START_AT_IN_HEADER]: startAt,
        },
      };
    },
    (error: Error | AxiosError) => {
      // log request error
      if (axios.isAxiosError(error)) {
        const transactionId = getTargetKeyFromConfig(error.config, TRANSACTION_ID_IN_HEADER);
        const startAt = getTargetKeyFromConfig(error.config, REQUEST_START_AT_IN_HEADER);

		    // 各类日志软件选其一即可
        logEvent('Send Request Error (request)', {
          ...error.config,
          error: safeStringify(error.toJSON()),
          transactionId,
          startAt,
        });
      } else {
        logEvent('Send Request Error (native)', {
          ...error,
        });
      }

      return Promise.reject(error);
    },
  );
};

const registerResponseInterceptor = (newInstance: AxiosInstance, isExternalRequest?: boolean) => {
  newInstance.interceptors.response.use(
    (response: AxiosResponse) => {
      // 2xx 范围内的状态码都会触发该函数

      // 请求成功时,无有效信息的请求不上报日志
      if (isRequestNeedRecord(response.config)) {
        // log response end
        logEvent('Receive Response', {
          ...response.config,
          transactionId: getTargetKeyFromConfig(response.config, TRANSACTION_ID_IN_HEADER),
          startAt: getTargetKeyFromConfig(response.config, REQUEST_START_AT_IN_HEADER),
        });
      }

      return Promise.resolve(response);
    },
    (error: Error | AxiosError) => {
      // log response error
      if (axios.isAxiosError(error)) {
        const transactionId = getTargetKeyFromConfig(error.config, TRANSACTION_ID_IN_HEADER);
        const startAt = getTargetKeyFromConfig(error.config, REQUEST_START_AT_IN_HEADER);

        logEvent('Receive Response Error (request)', {
          ...error.config,
          error: safeStringify(error.toJSON()),
          transactionId,
          startAt,
        });
      } else {
        logEvent('Receive Response Error (native)', {
          ...error,
        });
      }

      return Promise.reject(error);
    },
  );
};

export const axiosErrorHandler = async <T>({
  error,
  captureErrorInSentry = true,
  silenceError,
  config,
  isExternalRequest,
}: {
  config: AxiosRequestConfig;
  error: Error | AxiosError;
  silenceError?: boolean;
  captureErrorInSentry?: boolean;
  isExternalRequest?: boolean;
}): Promise<RequestResult<T>> => {
  // cancel 的请求直接返回
  const cancelled = axios.isCancel(error);

  if (cancelled) {
    return [undefined, { cancelled }];
  }

  // native 错误
  if (!axios.isAxiosError(error)) {
    return handleNativeError(error, config);
  }

  // axios 错误
  const isAuthFail = isAuthExpired(error);
  const isTimeout = isTimeoutError(error);
  const status = error.response?.status;
  const displayMessage = error.response?.data?.message;
  const transactionId = getTargetKeyFromConfig(error.config, TRANSACTION_ID_IN_HEADER);
  const requestStartTime = getTargetKeyFromConfig(error.config, REQUEST_START_AT_IN_HEADER);
  const networkStatusAtRequestStart = !!error.config[NETWORK_STATUS_AT_REQUEST_START];

  const intervalBetweenRequestAndResponse = dayjs().diff(dayjs(requestStartTime));

  const errorContext = {
    requestStartTime,
    method: config.method ?? 'N/A',
    endpoint: config.url ?? 'N/A',
    responseCode: status,
    message: displayMessage ?? error.message,
    networkStatusAtRequestStart,
  };

  // auth 失败的请求直接返回
  if (isAuthFail) {
    return [undefined, { cancelled }];
  }

  // 处理错误提示
  if (!silenceError) {
    if (isNetworkError(error)) {
      message.error('network_error');
    } else if (status === 404) {
      message.error(displayMessage ?? 'error_404');
    } else if (status === 403) {
      console.warn(displayMessage);
    } else {
      message.error(displayMessage);
    }
  }

  // 处理错误上报
  if (captureErrorInSentry) {
    if (error.response) {
      // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
      if (!isExternalRequest && status === 400) {
        sendWarningLog(error, transactionId);
      } else {
        captureError({
          error,
          context: errorContext,
          transactionId,
        });
      }
    } else if (error.request) {
      // 请求已经成功发起,但没有收到响应

      logEvent(isTimeout ? 'Receive Response Timeout' : 'Receive Request Error', {
        ...errorContext,
        error: safeStringify(error.toJSON()),
      });

      // 发起请求时网络正常 && 发起请求时间和报错时间间隔不超过两倍的 timeout,才上报至 Sentry,避免无效问题误报
      if (
        networkStatusAtRequestStart &&
        intervalBetweenRequestAndResponse < 2 * DEFAULT_REQUEST_TIMEOUT
      ) {
        captureError({
          error,
          context: errorContext,
          transactionId,
        });
      }
    } else {
      // 发送请求过程中出现问题
      captureError({
        context: errorContext,
        transactionId,
      });
    }
  }

  return [
    undefined,
    {
      error,
      cancelled,
    },
  ];
};

export const createAxios = (axiosConfig?: AxiosRequestConfig, isExternalRequest?: boolean) => {
  const newInstance = axios.create(axiosConfig);

  // register plugin
  registerAxiosRetry(newInstance, isExternalRequest);

  // register request interceptor
  registerRequestInterceptor(newInstance, isExternalRequest);

  // register response interceptor
  registerResponseInterceptor(newInstance, isExternalRequest);

  return newInstance;
};
@PeterChen1997 PeterChen1997 changed the title [业务产出] axios management 最佳实践 - doing [业务产出] axios management 最佳实践 Jul 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant