/** * 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 'Попробуйте обновить страницу или обратитесь в поддержку.'; } }