Привет, Хабр, меня зовут Алёна, я senior фронтент-разработчик в отдела разработки ПО для розничного бизнеса в Райффайзенбанке. Недавно наша команда решила улучшить пользовательский опыт обработки ошибок запроса к бекенду и я решила комплексно исследовать эту тему и собрать воедино все лучшие практики.
Начтем с того, что при обработке ошибок Axios запросов существуют 4 ситуации, которые необходимо по-разному интерпретировать:
Запрос был обработан сервером и статус ответа сервера вне диапазона 2xx
Запрос был сделан, но ответ не был получен.
Ошибка возникла из-за неправильных настроек Axios.
Ошибка не является инстансом Axios.
В документации Axios, почему-то указаны только первые 3 случая и не сказано, как именно их обрабатывать. На этом этапе наш код на TypeScript будет выглядеть так. Для удобства опишем его в статическом классе. Т.к. ошибок может быть несколько, то вернем promise c массивом ошибок. Это может быть список строк или, например, объектов.
export class ErrorUtils {
static async getErrors(error: AxiosError): Promise<string[]> {
let errorMessages: string[];
// проверим, что ошибка является инстансом Axios
if (axios.isAxiosError(error)) {
if (error?.response) {
/* обработаем 1ый случай, когда запрос был обработан сервером и
статус ответа сервера вне диапазона 2xx */
errorMessages = await this._getResponseErrors(/*...*/);
} else if (error?.request) {
/* обработаем 2ой случай, когда запрос был сделан,
но ответ не был получен */
errorMessages = this._getRequestErrors(/*...*/);
} else {
/* 3ий случай, когда шшибка возникла из-за неправильных настроек Axios */
errorMessages = [/*...*/];
}
} else {
/* 4ый случай. Ошибка не является инстансом Axios. Например,
при отправке данных, например, если вы пытаетесь отправить объект,
который не может быть сериализован */
errorMessages = [/*...*/];
}
return errorMessages;
}
}
1. Запрос был обработан сервером и статус ответа сервера вне диапазона 2xx
Первым этапом необходимо попробовать распознать модель ошибки, которая пришла с бека. В нашем случае мы используем общий обработчик ошибок, прописанный в QueryClient из библиотеки react-query и при необходимости обрабатываем ошибки на уровне самого запроса. Например, если требуется показать ошибки определенным способом. Наш бекенд использует микро сервисную архитектуру, а так же взаимодействует с другими сервисами банка и поэтому модели ошибок могут быть разными и есть шанс, что придет еще не знакомая фронту структура данных.
Если фронту не получилось распознать модель ошибки, которую бекенд прислал в response.data, то нужно поискать подходящее сообщение в списке статусов.
Т.к. у нас описаны только самые часто встречающие статусы, то может возникнуть ситуация, когда фронтенд покажет общее сообщение для данного типа ошибок. Если описать все статусы, то этот пункт можно пропустить.
Для удобства показа ошибок с учетом микро сервисной архитектуры, мы пытаемся определить имя сервиса на основе url и имя метода которые прописываем в каждом обработчике.
private static async _getResponseErrors(
response: AxiosResponse, // error.response
message: string, // error.message
serviceName: string, // получение имени сервиса будет описано ниже
) {
// пытаемся получить имя метода
const methodName = response.config?.description || '';
// пытаемся распознать модель ошибки, которую прислал бекенд
const errorMessages = await this._tryGetErrorFromData(response);
if (errorMessages) {
return errorMessages;
}
// ищем сообщение об ошибке в списке статусов
const getErrorMessageFn =
HTTP_ERROR_DESCRIPTIONS[response.status as HttpStatusCode];
if (getErrorMessageFn) {
return [getErrorMessageFn(serviceName, methodName)];
}
/* показываем общее сообщение, если не нашли расшифровку ошибки по статусу.
Если в списке HTTP_ERROR_DESCRIPTIONS описать все статусы, то этот шаг можно
пропустить
*/
return [
`При вызове метода ${methodName} произошла ошибка при получении ответа от сервиса ${serviceName}: ${message}`,
];
}
Вспомогательный метод для определения имени сервиса:
private static _getServiceName(
baseURL?: string, // error.config?.baseURL
url?: string // error.config?.url
): string {
if (!baseURL) {
return '';
}
const serviceUrl = url ? baseURL + url : baseURL;
for (const key in SERVICE_NAMES) {
if (serviceUrl.indexOf(key) !== -1) {
return SERVICE_NAMES[key];
}
}
return '';
}
Константа SERVICE_NAMES может выглядеть следующим образом:
const SERVICE_NAMES: Record<string, string> = {
'/customer-accounts/': 'получения данных по счетам',
/*...*/
};
В описании названий сервисов удобно пропустить слово "сервис", чтобы в описании ошибки можно было использовать его в разных падежах.
Константу HTTP_ERROR_DESCRIPTIONS, так же, можно сделать типа Record:
export const HTTP_ERROR_DESCRIPTIONS: Partial<
Record<HttpStatusCode, (serviceName: string, methodName: string) => string>
> = {
[HttpStatusCode.BadRequest]: (serviceName: string, methodName: string) =>
`Сервис ${serviceName} не смог понять запрос метода ${methodName} из-за некорректного синтаксиса`,
/*...*/
[HttpStatusCode.ServiceUnavailable]: (serviceName: string) =>
`Сервис ${serviceName} не доступен`,
/*...*/
};
Так можно указать название метода. Слово "метод", так же, можно пропустить, чтобы в сообщениях его можно было склонять по падежам.
export function getStatusHistory(id: number) {
return someAxiosServiceApi
.get<StatusHistory[]>(
`/some-url/${id}/status-history`,
{ description: 'получения истории статусов по ведомости' }
)
.then(({ data }) => data);
}
Чтобы в TypeScript можно было использовать дополнительное свойство AxiosRequestConfig, необходимо, в проект добавить *.d.ts.
// код файла axios.d.ts
import 'axios';
declare module 'axios' {
export interface AxiosRequestConfig {
/*** Описание должно быть без слова "метод". Например, 'получения списка счетов' ***/
description?: string;
}
}
А в tsconfig нужно прописать путь к нему:
{
"compilerOptions": {
/*...*/
/* может протребоваться добавить jest и node для корректной работы
сборшика модулей*/
"types": ["./src/shared/types", "jest", "node"],
},
/*...*/
}
2. Запрос был сделан, но ответ не был получен
В данном случае ошибку можно обработать по коду.
private static _getRequestErrors(
serviceName: string,
code?: string // error.code
) {
if (code) {
/* получаем сообщение об ошибке на основе кода ошибки */
const errorMessage = ERROR_DESCRIPTIONS[code](serviceName);
if (errorMessage) {
return [errorMessage];
}
}
/* если описать полный перечень кодов в ERROR_DESCRIPTIONS, то
этот шаг можно пропустить */
return [`Не удалось получить ответ от сервиса ${serviceName}`];
}
Константа ERROR_DESCRIPTIONS может быть описана следующим образом:
export const ERROR_DESCRIPTIONS: Record<
string,
(serviceName: string) => string
> = {
/*...*/
[AxiosError.ERR_CANCELED]: (serviceName) =>
`Запрос к сервису ${serviceName} отменен пользователем`,
};
Список кодов и их описание можно найти в документации Axios: https://github.com/axios/axios?tab=readme-ov-file#error-types.
3. Ошибка возникла из-за неправильных настроек Axios
В данном случае можно вернуть сообщение, наподобие этого:
`Не удалось отправить запрос к сервису ${serviceName}: ${error.message}`
4. Ошибка не является инстансом Axios
В этом случае можно вернуть следующее сообщение:
`Ошибка на стороне клиента: ${(error as Error).message}`
Постобработка сообщений об ошибках
Если в сообщениях об ошибках использовать названия сервисов и методов, то могут возникнуть ситуации, когда не удалось определить их и сообщения будут содержать лишние пробелы. Их можно удалить следующим образом:
private static _processErrorMessages(messages: string[]): string[] {
const resultMessages: string[] = [];
messages.forEach((message) =>
resultMessages.push(message.replace(' ', ' ').trim()),
);
return resultMessages;
}
Если вы не используете сообщения названия методов и сервисов, то можете пропустить этот пункт
Финальный вид метода обработки ошибок Axios
Если подытожить, то метод getErrors может иметь следующую реализацию:
static async getErrors(error: AxiosError): Promise<string[]> {
let errorMessages: string[];
if (axios.isAxiosError(error)) {
const serviceName = this._getServiceName(
error.config?.baseURL,
error.config?.url,
);
if (error?.response) {
errorMessages = await this._getResponseErrors(
error.response,
error.message,
serviceName,
);
} else if (error?.request) {
errorMessages = this._getRequestErrors(serviceName, error.code);
} else {
errorMessages = [
`Не удалось отправить запрос к сервису ${serviceName}: ${error.message}`,
];
}
} else {
errorMessages = [
`Ошибка на стороне клиента: ${(error as Error).message}`,
];
}
return this._processErrorMessages(errorMessages);
}
Однако, основываясь на том, что при обработке ошибок существуют 4 перечисленные выше случая, вы можете иначе обработать их.
Пишите в комментариях свои мысли по поводу обработки ошибок, задавайте вопросы. Буду рада на них ответить!
Изображение на обложке статьи "503 error service unavailable concept illustration" с сайта http://www.freepik.com.
Комментарии (2)
Vitaly_js
29.01.2025 11:41По моему рабочий подход. Я бы может подумал о другой форме. Не очень мне по душе вся эта "статическая" история. На вид тут работают 4 объекта. 3 обрабатывают ошибки axios и один общий. Можно было бы составить массив из таких объектов и просто пробежаться по нему. Тогда
getErrors
схлопывается раза в 3. Но это предложение по форме сути он не меняет.
somech
Почему в getError приходит аргумент с типом AxiosError, но у вас далее все равно происходит проверка утилитой самого axios? Логично ведь unknown?