feat: Добавление компонентов ErrorBoundary и NetworkErrorHandler для улучшения обработки ошибок в приложении. Увеличение таймаута запросов до 30 секунд и улучшение логики повторных попыток с детализированными уведомлениями об ошибках.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s

This commit is contained in:
2025-10-03 15:49:28 +07:00
parent 8954c58744
commit cd6a971991
12 changed files with 2694 additions and 33 deletions
+248
View File
@@ -0,0 +1,248 @@
/**
* apiErrorHandler - утилиты для обработки ошибок API
*/
/**
* Типы ошибок API
*/
export const ErrorType = {
NETWORK: 'NETWORK', // Проблемы с сетью
TIMEOUT: 'TIMEOUT', // Таймаут запроса
SERVER: 'SERVER', // Ошибка сервера (5xx)
CLIENT: 'CLIENT', // Ошибка клиента (4xx)
VALIDATION: 'VALIDATION', // Ошибка валидации
AUTH: 'AUTH', // Ошибка аутентификации
PERMISSION: 'PERMISSION', // Нет прав доступа
NOT_FOUND: 'NOT_FOUND', // Ресурс не найден
CONFLICT: 'CONFLICT', // Конфликт данных
RATE_LIMIT: 'RATE_LIMIT', // Превышен лимит запросов
UNKNOWN: 'UNKNOWN' // Неизвестная ошибка
};
/**
* Определяет тип ошибки по axios error
*/
export function getErrorType(error) {
if (!error) return ErrorType.UNKNOWN;
// Проверяем таймаут
if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
return ErrorType.TIMEOUT;
}
// Проверяем сетевые ошибки
if (!error.response) {
return ErrorType.NETWORK;
}
const status = error.response.status;
// Классифицируем по статус-коду
switch (status) {
case 400:
return ErrorType.VALIDATION;
case 401:
return ErrorType.AUTH;
case 403:
return ErrorType.PERMISSION;
case 404:
return ErrorType.NOT_FOUND;
case 409:
return ErrorType.CONFLICT;
case 429:
return ErrorType.RATE_LIMIT;
default:
if (status >= 400 && status < 500) {
return ErrorType.CLIENT;
}
if (status >= 500) {
return ErrorType.SERVER;
}
return ErrorType.UNKNOWN;
}
}
/**
* Определяет, можно ли повторить запрос после этой ошибки
*/
export function isRetriableError(error, method = 'GET') {
const errorType = getErrorType(error);
const safeMethod = method.toUpperCase();
// GET запросы - можно повторять почти всегда
if (safeMethod === 'GET') {
return [
ErrorType.NETWORK,
ErrorType.TIMEOUT,
ErrorType.SERVER,
].includes(errorType);
}
// POST/PUT/PATCH/DELETE - только сетевые и таймауты
// Не повторяем 5xx чтобы не создать дубликаты
return [
ErrorType.NETWORK,
ErrorType.TIMEOUT,
].includes(errorType);
}
/**
* Получает задержку для retry с exponential backoff
*/
export function getRetryDelay(attemptNumber, baseDelay = 300) {
// Exponential backoff: 300ms, 600ms, 1200ms, 2400ms, 4800ms
const delay = baseDelay * Math.pow(2, attemptNumber);
// Добавляем jitter ±20% для избежания thundering herd
const jitter = delay * 0.2 * (Math.random() - 0.5);
return Math.min(delay + jitter, 10000); // Максимум 10 секунд
}
/**
* Получает максимальное количество попыток для типа ошибки
*/
export function getMaxRetries(errorType, method = 'GET') {
const safeMethod = method.toUpperCase();
// Идемпотентные методы - больше попыток
if (safeMethod === 'GET' || safeMethod === 'HEAD') {
switch (errorType) {
case ErrorType.NETWORK:
return 3;
case ErrorType.TIMEOUT:
return 2;
case ErrorType.SERVER:
return 2;
default:
return 0;
}
}
// Неидемпотентные методы - меньше попыток
switch (errorType) {
case ErrorType.NETWORK:
return 1;
case ErrorType.TIMEOUT:
return 1;
default:
return 0;
}
}
/**
* Форматирует сообщение об ошибке для пользователя
*/
export function formatErrorMessage(error) {
const errorType = getErrorType(error);
const status = error.response?.status;
const data = error.response?.data;
// Используем сообщение с сервера если есть
if (data?.message) {
return data.message;
}
// Иначе генерируем понятное сообщение
switch (errorType) {
case ErrorType.NETWORK:
return 'Не удалось подключиться к серверу. Проверьте соединение с интернетом.';
case ErrorType.TIMEOUT:
return 'Сервер не отвечает. Попробуйте повторить запрос позже.';
case ErrorType.AUTH:
return 'Требуется авторизация. Пожалуйста, войдите в систему.';
case ErrorType.PERMISSION:
return 'У вас нет прав для выполнения этой операции.';
case ErrorType.NOT_FOUND:
return 'Запрашиваемый ресурс не найден.';
case ErrorType.VALIDATION:
return 'Ошибка валидации данных. Проверьте введённые значения.';
case ErrorType.CONFLICT:
return 'Конфликт данных. Возможно, ресурс был изменён другим пользователем.';
case ErrorType.RATE_LIMIT:
return 'Превышен лимит запросов. Пожалуйста, подождите немного.';
case ErrorType.SERVER:
return `Ошибка сервера (${status}). Пожалуйста, попробуйте позже.`;
case ErrorType.CLIENT:
return `Ошибка запроса (${status}). ${error.message}`;
default:
return error.message || 'Произошла неизвестная ошибка.';
}
}
/**
* Извлекает детали ошибки для отладки
*/
export function getErrorDetails(error) {
const data = error.response?.data;
return {
type: getErrorType(error),
status: error.response?.status,
code: data?.code || error.code,
message: formatErrorMessage(error),
requestId: data?.requestId || error.response?.headers?.['x-request-id'],
details: data?.details,
url: error.config?.url,
method: error.config?.method?.toUpperCase(),
timestamp: new Date().toISOString()
};
}
/**
* Проверяет, является ли ошибка критичной (требует немедленного внимания)
*/
export function isCriticalError(error) {
const errorType = getErrorType(error);
return [
ErrorType.SERVER,
ErrorType.AUTH,
].includes(errorType);
}
/**
* Логирует ошибку (для отправки в систему мониторинга)
*/
export function logError(error, context = {}) {
const details = getErrorDetails(error);
console.error('API Error:', {
...details,
...context,
stackTrace: error.stack
});
// Здесь можно добавить отправку в Sentry, LogRocket и т.д.
// if (window.Sentry) {
// window.Sentry.captureException(error, {
// contexts: { api: details, custom: context }
// });
// }
}
/**
* Создает user-friendly сообщение с рекомендациями по устранению
*/
export function getErrorAction(error) {
const errorType = getErrorType(error);
switch (errorType) {
case ErrorType.NETWORK:
return 'Проверьте подключение к интернету и повторите попытку.';
case ErrorType.TIMEOUT:
return 'Сервер долго отвечает. Попробуйте обновить страницу.';
case ErrorType.AUTH:
return 'Пожалуйста, войдите в систему снова.';
case ErrorType.PERMISSION:
return 'Обратитесь к администратору для получения доступа.';
case ErrorType.VALIDATION:
return 'Проверьте правильность введённых данных.';
case ErrorType.CONFLICT:
return 'Обновите страницу и попробуйте снова.';
case ErrorType.RATE_LIMIT:
return 'Подождите несколько секунд и повторите попытку.';
case ErrorType.SERVER:
return 'Попробуйте повторить операцию через несколько минут.';
default:
return 'Попробуйте обновить страницу или обратитесь в поддержку.';
}
}