-
Notifications
You must be signed in to change notification settings - Fork 1
Open
Labels
Description
// 常量
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;
};