Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
249 lines
8.6 KiB
JavaScript
249 lines
8.6 KiB
JavaScript
/**
|
||
* 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 'Попробуйте обновить страницу или обратитесь в поддержку.';
|
||
}
|
||
}
|
||
|