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
+18 -13
View File
@@ -35,6 +35,8 @@ import { NotifyProvider } from './components/NotifyProvider.jsx';
import SettingsModal from './components/SettingsModal.jsx';
import ToastContainer from './components/ToastContainer.jsx';
import CommandPalette, { KeyboardShortcutsButton } from './components/CommandPalette.jsx';
import ErrorBoundary from './components/ErrorBoundary.jsx';
import NetworkErrorHandler from './components/NetworkErrorHandler.jsx';
// --- Simple i18n (RU/EN) ---
const LanguageContext = createContext({ lang: 'ru', setLang: () => {}, t: (k) => k });
@@ -91,19 +93,22 @@ const queryClient = new QueryClient({
function App() {
return (
<Router>
<QueryClientProvider client={queryClient}>
<LanguageProvider>
<ThemeProvider>
<ToastContainer>
<NotifyProvider>
<MainLayout />
</NotifyProvider>
</ToastContainer>
</ThemeProvider>
</LanguageProvider>
</QueryClientProvider>
</Router>
<ErrorBoundary>
<Router>
<QueryClientProvider client={queryClient}>
<LanguageProvider>
<ThemeProvider>
<ToastContainer>
<NotifyProvider>
<NetworkErrorHandler />
<MainLayout />
</NotifyProvider>
</ToastContainer>
</ThemeProvider>
</LanguageProvider>
</QueryClientProvider>
</Router>
</ErrorBoundary>
);
}
+159
View File
@@ -0,0 +1,159 @@
import { Component } from 'react';
import { IconAlertTriangle, IconRefresh, IconHome } from '@tabler/icons-react';
/**
* ErrorBoundary - глобальный обработчик ошибок React
* Ловит ошибки рендеринга и показывает fallback UI
*/
class ErrorBoundary extends Component {
constructor(props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
errorCount: 0
};
}
static getDerivedStateFromError(error) {
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// Логируем ошибку
console.error('ErrorBoundary caught an error:', error, errorInfo);
// Отправляем в мониторинг (если настроен)
this.logErrorToService(error, errorInfo);
this.setState(prevState => ({
error,
errorInfo,
errorCount: prevState.errorCount + 1
}));
}
logErrorToService(error, errorInfo) {
// Можно интегрировать с Sentry, LogRocket и т.д.
try {
if (typeof window !== 'undefined' && window.notify?.error) {
window.notify.error('Произошла критическая ошибка приложения', {
error: error.toString(),
componentStack: errorInfo?.componentStack
});
}
} catch (e) {
console.error('Failed to log error:', e);
}
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null
});
// Очищаем localStorage если ошибка повторяется
if (this.state.errorCount > 2) {
try {
localStorage.clear();
sessionStorage.clear();
} catch (e) {
console.error('Failed to clear storage:', e);
}
}
// Перезагружаем страницу если много ошибок
if (this.state.errorCount > 3) {
window.location.href = '/';
}
};
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
return (
<div className="page page-center">
<div className="container-tight py-4">
<div className="empty">
<div className="empty-icon">
<IconAlertTriangle size={64} className="text-danger" />
</div>
<p className="empty-title">Произошла ошибка</p>
<p className="empty-subtitle text-muted">
{this.state.error?.message || 'Что-то пошло не так. Попробуйте обновить страницу.'}
</p>
{process.env.NODE_ENV === 'development' && this.state.errorInfo && (
<div className="card mt-3">
<div className="card-body">
<h3 className="card-title">Детали ошибки (только в dev режиме)</h3>
<pre className="text-start" style={{
fontSize: '0.75rem',
maxHeight: '300px',
overflow: 'auto',
whiteSpace: 'pre-wrap'
}}>
{this.state.error?.toString()}
{'\n\n'}
{this.state.errorInfo?.componentStack}
</pre>
</div>
</div>
)}
<div className="empty-action">
<div className="btn-list justify-content-center">
<button
className="btn btn-primary"
onClick={this.handleReset}
>
<IconRefresh className="icon" />
Попробовать снова
</button>
<button
className="btn btn-outline-primary"
onClick={this.handleReload}
>
Перезагрузить страницу
</button>
<button
className="btn btn-outline-secondary"
onClick={this.handleGoHome}
>
<IconHome className="icon" />
На главную
</button>
</div>
</div>
{this.state.errorCount > 1 && (
<div className="alert alert-warning mt-3">
<p className="mb-0">
Ошибка повторяется ({this.state.errorCount} раз).
{this.state.errorCount > 2 && ' При следующей попытке кэш будет очищен.'}
{this.state.errorCount > 3 && ' Следующая попытка приведёт к полной перезагрузке.'}
</p>
</div>
)}
</div>
</div>
</div>
);
}
return this.props.children;
}
}
export default ErrorBoundary;
@@ -0,0 +1,81 @@
import { useState, useEffect } from 'react';
import { IconWifi, IconWifiOff } from '@tabler/icons-react';
/**
* NetworkErrorHandler - компонент для мониторинга состояния сети
* Показывает уведомление при потере соединения
*/
function NetworkErrorHandler() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const [wasOffline, setWasOffline] = useState(false);
const [showReconnected, setShowReconnected] = useState(false);
useEffect(() => {
const handleOnline = () => {
setIsOnline(true);
if (wasOffline) {
setShowReconnected(true);
// Показываем уведомление о восстановлении на 3 секунды
setTimeout(() => {
setShowReconnected(false);
setWasOffline(false);
}, 3000);
// Уведомляем через глобальную систему
if (window.notify?.success) {
window.notify.success('Соединение восстановлено');
}
}
};
const handleOffline = () => {
setIsOnline(false);
setWasOffline(true);
// Уведомляем через глобальную систему
if (window.notify?.warning) {
window.notify.warning('Нет соединения с интернетом');
}
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, [wasOffline]);
// Не показываем ничего если онлайн и не было офлайна
if (isOnline && !showReconnected) {
return null;
}
return (
<div
className="position-fixed top-0 start-0 end-0"
style={{ zIndex: 1090 }}
>
{!isOnline ? (
<div className="alert alert-danger mb-0 rounded-0 border-0" role="alert">
<div className="d-flex align-items-center justify-content-center">
<IconWifiOff className="icon me-2" />
<strong>Нет соединения с интернетом</strong>
<span className="ms-2 text-muted">Ожидание восстановления...</span>
</div>
</div>
) : showReconnected ? (
<div className="alert alert-success mb-0 rounded-0 border-0" role="alert">
<div className="d-flex align-items-center justify-content-center">
<IconWifi className="icon me-2" />
<strong>Соединение восстановлено</strong>
</div>
</div>
) : null}
</div>
);
}
export default NetworkErrorHandler;
+45
View File
@@ -0,0 +1,45 @@
import { useState } from 'react';
import { IconRefresh } from '@tabler/icons-react';
/**
* RetryButton - кнопка для повторной попытки с индикацией загрузки
*/
function RetryButton({
onRetry,
loading: externalLoading,
disabled,
className = 'btn btn-primary',
children = 'Повторить',
showIcon = true,
...props
}) {
const [internalLoading, setInternalLoading] = useState(false);
const loading = externalLoading !== undefined ? externalLoading : internalLoading;
const handleClick = async () => {
if (loading || disabled) return;
try {
setInternalLoading(true);
await onRetry();
} finally {
setInternalLoading(false);
}
};
return (
<button
type="button"
className={`${className}${loading ? ' btn-loading' : ''}`}
disabled={disabled || loading}
onClick={handleClick}
{...props}
>
{showIcon && !loading && <IconRefresh className="icon" />}
{children}
</button>
);
}
export default RetryButton;
@@ -0,0 +1,218 @@
/**
* Примеры использования новой системы обработки ошибок
* Этот файл демонстрирует различные сценарии использования
*/
import { useState } from 'react';
import { useErrorHandler } from '../hooks/useErrorHandler';
import RetryButton from '../components/RetryButton';
import api from '../lib/api';
/**
* Пример 1: Простая обработка ошибок в компоненте
*/
function SimpleErrorHandlingExample() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const { handleError, handleSuccess, withErrorHandler } = useErrorHandler();
// Вариант 1: Ручная обработка ошибок
const fetchDataManual = async () => {
setLoading(true);
try {
const response = await api.get('/domains-new');
setData(response.data);
handleSuccess('Данные успешно загружены');
} catch (error) {
handleError(error, { component: 'SimpleErrorHandlingExample' });
} finally {
setLoading(false);
}
};
// Вариант 2: Автоматическая обработка с withErrorHandler
const fetchDataAuto = withErrorHandler(
async () => {
setLoading(true);
const response = await api.get('/domains-new');
setData(response.data);
setLoading(false);
},
{
successMessage: 'Данные успешно загружены',
context: { component: 'SimpleErrorHandlingExample' }
}
);
return (
<div className="card">
<div className="card-header">
<h3 className="card-title">Пример простой обработки ошибок</h3>
</div>
<div className="card-body">
<div className="btn-list">
<button
className="btn btn-primary"
onClick={fetchDataManual}
disabled={loading}
>
Загрузить (ручная обработка)
</button>
<button
className="btn btn-primary"
onClick={fetchDataAuto}
disabled={loading}
>
Загрузить (автоматическая обработка)
</button>
</div>
{data && (
<div className="mt-3">
<p>Загружено записей: {data.items?.length || 0}</p>
</div>
)}
</div>
</div>
);
}
/**
* Пример 2: Использование RetryButton
*/
function RetryButtonExample() {
const [attempts, setAttempts] = useState(0);
const { handleError, handleSuccess } = useErrorHandler();
const unreliableOperation = async () => {
setAttempts(prev => prev + 1);
// Симуляция операции которая иногда падает
if (Math.random() < 0.5) {
throw new Error('Случайная ошибка для демонстрации');
}
handleSuccess('Операция выполнена успешно!');
};
return (
<div className="card">
<div className="card-header">
<h3 className="card-title">Пример использования RetryButton</h3>
</div>
<div className="card-body">
<p>Попыток: {attempts}</p>
<RetryButton
onRetry={unreliableOperation}
className="btn btn-primary"
>
Выполнить ненадежную операцию
</RetryButton>
</div>
</div>
);
}
/**
* Пример 3: Обработка различных типов ошибок
*/
function ErrorTypesExample() {
const { handleError } = useErrorHandler();
const triggerNetworkError = () => {
api.get('/nonexistent-endpoint')
.catch(error => handleError(error));
};
const triggerValidationError = () => {
api.post('/domains-new', { invalid: 'data' })
.catch(error => handleError(error));
};
const triggerTimeoutError = () => {
// Создаем запрос который точно превысит таймаут
const slowApi = api.create({ timeout: 100 });
slowApi.get('/domains-new')
.catch(error => handleError(error));
};
return (
<div className="card">
<div className="card-header">
<h3 className="card-title">Типы ошибок</h3>
</div>
<div className="card-body">
<div className="btn-list">
<button
className="btn btn-danger"
onClick={triggerNetworkError}
>
Сетевая ошибка
</button>
<button
className="btn btn-warning"
onClick={triggerValidationError}
>
Ошибка валидации
</button>
<button
className="btn btn-info"
onClick={triggerTimeoutError}
>
Таймаут
</button>
</div>
</div>
</div>
);
}
/**
* Главный компонент с примерами
*/
function ErrorHandlingExample() {
return (
<div className="container-xl">
<div className="page-header">
<h1 className="page-title">Примеры обработки ошибок</h1>
<p className="text-muted">
Демонстрация возможностей новой системы обработки ошибок
</p>
</div>
<div className="row row-cards">
<div className="col-12">
<SimpleErrorHandlingExample />
</div>
<div className="col-12">
<RetryButtonExample />
</div>
<div className="col-12">
<ErrorTypesExample />
</div>
</div>
<div className="card mt-3">
<div className="card-header">
<h3 className="card-title">Особенности новой системы</h3>
</div>
<div className="card-body">
<ul>
<li> <strong>Автоматический retry</strong> для GET запросов (до 3 попыток)</li>
<li> <strong>Умный retry</strong> для POST/PUT/DELETE при сетевых ошибках</li>
<li> <strong>Exponential backoff</strong> с jitter для избежания thundering herd</li>
<li> <strong>User-friendly сообщения</strong> вместо технических ошибок</li>
<li> <strong>Детальная информация</strong> для отладки (requestId, timestamp, детали)</li>
<li> <strong>Offline/Online detection</strong> с уведомлениями</li>
<li> <strong>Error Boundary</strong> для ловли React ошибок рендеринга</li>
<li> <strong>Типизация ошибок</strong> (Network, Timeout, Server, Client, etc.)</li>
<li> <strong>Интеграция с системой уведомлений</strong></li>
<li> <strong>Логирование критичных ошибок</strong></li>
</ul>
</div>
</div>
</div>
);
}
export default ErrorHandlingExample;
+82
View File
@@ -0,0 +1,82 @@
import { useCallback } from 'react';
import { useNotify } from '../components/NotifyProvider';
import { formatErrorMessage, getErrorAction, getErrorDetails } from '../lib/api';
/**
* useErrorHandler - хук для обработки ошибок в компонентах
* Предоставляет единообразный способ обработки ошибок
*/
export function useErrorHandler() {
const notify = useNotify();
const handleError = useCallback((error, context = {}) => {
console.error('Error in component:', error, context);
const message = formatErrorMessage(error);
const action = getErrorAction(error);
const details = getErrorDetails(error);
notify.error(message, {
...details,
action,
context
});
}, [notify]);
const handleSuccess = useCallback((message = 'Операция выполнена успешно') => {
notify.success(message);
}, [notify]);
const handleWarning = useCallback((message, details) => {
notify.warning(message, details);
}, [notify]);
const handleInfo = useCallback((message, details) => {
notify.info(message, details);
}, [notify]);
// Обёртка для async функций с автоматической обработкой ошибок
const withErrorHandler = useCallback((asyncFn, options = {}) => {
return async (...args) => {
try {
const result = await asyncFn(...args);
if (options.successMessage) {
handleSuccess(options.successMessage);
}
return result;
} catch (error) {
if (!options.silent) {
handleError(error, options.context);
}
if (options.rethrow) {
throw error;
}
return options.defaultValue;
}
};
}, [handleError, handleSuccess]);
return {
handleError,
handleSuccess,
handleWarning,
handleInfo,
withErrorHandler
};
}
/**
* useAsyncError - хук для обработки async ошибок в useEffect
*/
export function useAsyncError() {
const { handleError } = useErrorHandler();
return useCallback((promise, context) => {
promise.catch(error => handleError(error, context));
}, [handleError]);
}
+74 -20
View File
@@ -1,9 +1,20 @@
import axios from 'axios';
import {
getErrorType,
isRetriableError,
getRetryDelay,
getMaxRetries,
formatErrorMessage,
getErrorDetails,
logError,
isCriticalError,
getErrorAction
} from './apiErrorHandler';
// Базовый axios-клиент для всего приложения
const api = axios.create({
baseURL: '/api',
timeout: 10000,
timeout: 30000, // Увеличен до 30 секунд для больших запросов
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
@@ -24,7 +35,7 @@ function buildCacheKey(config) {
}
}
// Авто-ретрай для идемпотентных GET: до 2 попыток с экспоненциальной задержкой
// Улучшенный retry interceptor с поддержкой всех методов и типов ошибок
api.interceptors.response.use(
(response) => {
try {
@@ -38,9 +49,6 @@ api.interceptors.response.use(
if (cached) {
return { ...response, status: 200, data: cached.data, headers: { ...cached.headers, 'x-from-cache': '1' } };
}
// нет кеша — вернём пустые семантически корректные данные (чтобы не падали .map)
// вызывающий код должен ожидать типы, поэтому лучше не подменять тип неожиданно.
// Просто пропустим дальше как есть — до второго интерсептора и обработчиков.
return response;
}
// Не 304: обновляем кеш, но только если есть валидный etag
@@ -53,35 +61,72 @@ api.interceptors.response.use(
},
async (error) => {
const config = error?.config || {};
const isGet = String(config.method || 'get').toLowerCase() === 'get';
const status = error?.response?.status;
const retriable = !error.response || (status >= 500 && status !== 501);
const method = String(config.method || 'get').toUpperCase();
// Инициализируем счетчик попыток
config.__retryCount = config.__retryCount || 0;
if (isGet && retriable && config.__retryCount < 2) {
// Определяем тип ошибки и возможность повтора
const errorType = getErrorType(error);
const canRetry = isRetriableError(error, method);
const maxRetries = getMaxRetries(errorType, method);
// Логируем ошибку если это критичная ошибка или последняя попытка
if (isCriticalError(error) || config.__retryCount >= maxRetries) {
logError(error, {
retryAttempt: config.__retryCount,
maxRetries,
errorType,
canRetry
});
}
// Проверяем возможность повтора
if (canRetry && config.__retryCount < maxRetries) {
config.__retryCount += 1;
const delay = 300 * Math.pow(2, config.__retryCount - 1);
const delay = getRetryDelay(config.__retryCount - 1);
// Уведомляем о повторной попытке (только для пользовательских действий)
if (config.__retryCount === 1 && method !== 'GET' && typeof window !== 'undefined') {
console.log(`Повторная попытка ${config.__retryCount}/${maxRetries} для ${method} ${config.url}`);
}
await new Promise((r) => setTimeout(r, delay));
return api(config);
}
return Promise.reject(error);
}
);
// Нормализация ошибок и уведомления по умолчанию
// Улучшенная нормализация ошибок с user-friendly сообщениями
api.interceptors.response.use(
(res) => res,
(err) => {
try {
const status = err?.response?.status;
const data = err?.response?.data || {};
const message = data?.message || err?.message || 'Ошибка запроса';
const code = data?.code;
const details = data?.details;
const requestId = data?.requestId || err?.response?.headers?.['x-request-id'] || err?.config?.headers?.['X-Request-Id'];
if (status >= 400 && typeof window !== 'undefined' && window.notify?.error) {
window.notify.add('error', `${message}${status ? ` (${status})` : ''}${requestId ? ` • reqId=${requestId}` : ''}`, details ? { code, requestId, details } : undefined);
// Не показываем уведомления если это повторная попытка
const isRetrying = err?.config?.__retryCount > 0;
if (!isRetrying && typeof window !== 'undefined' && window.notify) {
const errorDetails = getErrorDetails(err);
const userMessage = formatErrorMessage(err);
const actionMessage = getErrorAction(err);
// Формируем детальное сообщение
const fullMessage = `${userMessage}${errorDetails.status ? ` (${errorDetails.status})` : ''}`;
const extraDetails = {
...errorDetails,
action: actionMessage,
timestamp: new Date().toLocaleString('ru-RU')
};
// Выбираем тип уведомления
const notifyType = isCriticalError(err) ? 'error' : 'warning';
window.notify.add(notifyType, fullMessage, extraDetails);
}
} catch {}
} catch (notifyError) {
console.error('Failed to show error notification:', notifyError);
}
return Promise.reject(err);
}
);
@@ -125,6 +170,15 @@ export function unwrapStd(res) {
return data;
}
// Экспортируем утилиты обработки ошибок для использования в компонентах
export {
getErrorType,
formatErrorMessage,
getErrorDetails,
getErrorAction,
isCriticalError
} from './apiErrorHandler';
export default api;
+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 'Попробуйте обновить страницу или обратитесь в поддержку.';
}
}