feat: Добавление компонентов ErrorBoundary и NetworkErrorHandler для улучшения обработки ошибок в приложении. Увеличение таймаута запросов до 30 секунд и улучшение логики повторных попыток с детализированными уведомлениями об ошибках.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
This commit is contained in:
@@ -0,0 +1,672 @@
|
||||
# 🛡️ Руководство по обработке ошибок
|
||||
|
||||
Комплексная система обработки ошибок для Router Lists UI, включающая автоматический retry, user-friendly сообщения и мониторинг состояния сети.
|
||||
|
||||
## 📚 Оглавление
|
||||
|
||||
- [Компоненты системы](#компоненты-системы)
|
||||
- [Типы ошибок](#типы-ошибок)
|
||||
- [Автоматический Retry](#автоматический-retry)
|
||||
- [Использование в компонентах](#использование-в-компонентах)
|
||||
- [API Reference](#api-reference)
|
||||
- [Примеры](#примеры)
|
||||
- [Лучшие практики](#лучшие-практики)
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Компоненты системы
|
||||
|
||||
### 1. **ErrorBoundary**
|
||||
Глобальный обработчик ошибок React для ловли ошибок рендеринга.
|
||||
|
||||
**Возможности:**
|
||||
- ✅ Ловит ошибки рендеринга и показывает fallback UI
|
||||
- ✅ Логирует ошибки в консоль и систему мониторинга
|
||||
- ✅ Показывает детали ошибки в dev режиме
|
||||
- ✅ Автоматически очищает кэш при повторяющихся ошибках
|
||||
- ✅ Кнопки восстановления: "Попробовать снова", "Перезагрузить", "На главную"
|
||||
|
||||
**Использование:**
|
||||
```jsx
|
||||
// Уже встроен в App.jsx, оборачивает все приложение
|
||||
<ErrorBoundary>
|
||||
<App />
|
||||
</ErrorBoundary>
|
||||
```
|
||||
|
||||
### 2. **NetworkErrorHandler**
|
||||
Компонент для мониторинга состояния сети.
|
||||
|
||||
**Возможности:**
|
||||
- ✅ Детектирует offline/online события
|
||||
- ✅ Показывает баннер при потере соединения
|
||||
- ✅ Уведомляет о восстановлении соединения
|
||||
- ✅ Интегрируется с глобальной системой уведомлений
|
||||
|
||||
**Использование:**
|
||||
```jsx
|
||||
// Уже встроен в App.jsx
|
||||
<NetworkErrorHandler />
|
||||
```
|
||||
|
||||
### 3. **apiErrorHandler.js**
|
||||
Утилиты для классификации и обработки ошибок API.
|
||||
|
||||
**Функции:**
|
||||
- `getErrorType(error)` - определяет тип ошибки
|
||||
- `isRetriableError(error, method)` - проверяет возможность retry
|
||||
- `getRetryDelay(attemptNumber)` - вычисляет задержку для retry
|
||||
- `getMaxRetries(errorType, method)` - возвращает максимум попыток
|
||||
- `formatErrorMessage(error)` - форматирует user-friendly сообщение
|
||||
- `getErrorDetails(error)` - извлекает детали ошибки
|
||||
- `getErrorAction(error)` - возвращает рекомендации по устранению
|
||||
- `isCriticalError(error)` - проверяет критичность ошибки
|
||||
- `logError(error, context)` - логирует ошибку
|
||||
|
||||
### 4. **useErrorHandler**
|
||||
React хук для обработки ошибок в компонентах.
|
||||
|
||||
**API:**
|
||||
```javascript
|
||||
const {
|
||||
handleError, // Обработать ошибку
|
||||
handleSuccess, // Показать успех
|
||||
handleWarning, // Показать предупреждение
|
||||
handleInfo, // Показать инфо
|
||||
withErrorHandler // Обёртка для async функций
|
||||
} = useErrorHandler();
|
||||
```
|
||||
|
||||
### 5. **RetryButton**
|
||||
Кнопка для повторной попытки с индикацией загрузки.
|
||||
|
||||
**Props:**
|
||||
- `onRetry` - async функция для выполнения
|
||||
- `loading` - внешнее состояние загрузки
|
||||
- `disabled` - отключить кнопку
|
||||
- `className` - CSS классы
|
||||
- `showIcon` - показывать иконку
|
||||
- `children` - текст кнопки
|
||||
|
||||
---
|
||||
|
||||
## 🎭 Типы ошибок
|
||||
|
||||
Система классифицирует ошибки на следующие типы:
|
||||
|
||||
| Тип | Код | Описание | Retry |
|
||||
|-----|-----|----------|-------|
|
||||
| **NETWORK** | - | Проблемы с сетью (нет соединения) | ✅ Да (3x) |
|
||||
| **TIMEOUT** | ECONNABORTED | Превышено время ожидания | ✅ Да (2x) |
|
||||
| **SERVER** | 5xx | Ошибка на сервере | ✅ Да (2x GET) |
|
||||
| **CLIENT** | 4xx | Ошибка клиента (общая) | ❌ Нет |
|
||||
| **VALIDATION** | 400 | Ошибка валидации данных | ❌ Нет |
|
||||
| **AUTH** | 401 | Требуется аутентификация | ❌ Нет |
|
||||
| **PERMISSION** | 403 | Нет прав доступа | ❌ Нет |
|
||||
| **NOT_FOUND** | 404 | Ресурс не найден | ❌ Нет |
|
||||
| **CONFLICT** | 409 | Конфликт данных (ETag) | ❌ Нет |
|
||||
| **RATE_LIMIT** | 429 | Превышен лимит запросов | ❌ Нет |
|
||||
| **UNKNOWN** | - | Неизвестная ошибка | ❌ Нет |
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Автоматический Retry
|
||||
|
||||
### Стратегия Retry
|
||||
|
||||
**GET запросы:**
|
||||
- Network Error: до 3 попыток
|
||||
- Timeout: до 2 попыток
|
||||
- Server Error (5xx): до 2 попыток
|
||||
|
||||
**POST/PUT/PATCH/DELETE:**
|
||||
- Network Error: до 1 попытки
|
||||
- Timeout: до 1 попытки
|
||||
- Без retry для 5xx (чтобы не создать дубликаты)
|
||||
|
||||
### Exponential Backoff
|
||||
|
||||
Задержки между попытками растут экспоненциально:
|
||||
|
||||
```
|
||||
Попытка 1: 300ms (± 20% jitter)
|
||||
Попытка 2: 600ms (± 20% jitter)
|
||||
Попытка 3: 1200ms (± 20% jitter)
|
||||
```
|
||||
|
||||
**Jitter** (±20%) добавляется для избежания thundering herd problem.
|
||||
|
||||
### Максимальная задержка
|
||||
|
||||
Максимальная задержка ограничена **10 секундами** для предотвращения бесконечного ожидания.
|
||||
|
||||
---
|
||||
|
||||
## 💻 Использование в компонентах
|
||||
|
||||
### Вариант 1: Ручная обработка
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||
import api from '../lib/api';
|
||||
|
||||
function MyComponent() {
|
||||
const [data, setData] = useState(null);
|
||||
const { handleError, handleSuccess } = useErrorHandler();
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
const response = await api.get('/domains-new');
|
||||
setData(response.data);
|
||||
handleSuccess('Данные загружены');
|
||||
} catch (error) {
|
||||
handleError(error, { component: 'MyComponent' });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button onClick={fetchData}>
|
||||
Загрузить данные
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Вариант 2: Автоматическая обработка
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||
import api from '../lib/api';
|
||||
|
||||
function MyComponent() {
|
||||
const [data, setData] = useState(null);
|
||||
const { withErrorHandler } = useErrorHandler();
|
||||
|
||||
const fetchData = withErrorHandler(
|
||||
async () => {
|
||||
const response = await api.get('/domains-new');
|
||||
setData(response.data);
|
||||
},
|
||||
{
|
||||
successMessage: 'Данные загружены',
|
||||
context: { component: 'MyComponent' }
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<button onClick={fetchData}>
|
||||
Загрузить данные
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Вариант 3: С RetryButton
|
||||
|
||||
```jsx
|
||||
import RetryButton from '../components/RetryButton';
|
||||
import api from '../lib/api';
|
||||
|
||||
function MyComponent() {
|
||||
const saveData = async () => {
|
||||
await api.post('/domains-new', { domains: [...] });
|
||||
};
|
||||
|
||||
return (
|
||||
<RetryButton onRetry={saveData}>
|
||||
Сохранить
|
||||
</RetryButton>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Вариант 4: React Query интеграция
|
||||
|
||||
```jsx
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||
import api from '../lib/api';
|
||||
|
||||
function MyComponent() {
|
||||
const { handleError } = useErrorHandler();
|
||||
|
||||
const { data, isLoading, refetch } = useQuery({
|
||||
queryKey: ['domains'],
|
||||
queryFn: async () => {
|
||||
const response = await api.get('/domains-new');
|
||||
return response.data;
|
||||
},
|
||||
onError: (error) => {
|
||||
handleError(error, { component: 'MyComponent' });
|
||||
}
|
||||
});
|
||||
|
||||
// React Query автоматически делает retry,
|
||||
// но мы добавляем user-friendly уведомления
|
||||
|
||||
return <div>{/* ... */}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 API Reference
|
||||
|
||||
### useErrorHandler()
|
||||
|
||||
```typescript
|
||||
interface ErrorHandlerHook {
|
||||
// Обработать ошибку
|
||||
handleError: (error: Error, context?: object) => void;
|
||||
|
||||
// Показать успех
|
||||
handleSuccess: (message?: string) => void;
|
||||
|
||||
// Показать предупреждение
|
||||
handleWarning: (message: string, details?: object) => void;
|
||||
|
||||
// Показать информацию
|
||||
handleInfo: (message: string, details?: object) => void;
|
||||
|
||||
// Обёртка для async функций
|
||||
withErrorHandler: (
|
||||
asyncFn: Function,
|
||||
options?: {
|
||||
successMessage?: string;
|
||||
context?: object;
|
||||
silent?: boolean;
|
||||
rethrow?: boolean;
|
||||
defaultValue?: any;
|
||||
}
|
||||
) => Function;
|
||||
}
|
||||
```
|
||||
|
||||
### getErrorType()
|
||||
|
||||
```javascript
|
||||
import { getErrorType, ErrorType } from '../lib/api';
|
||||
|
||||
const error = new Error('Network error');
|
||||
const type = getErrorType(error);
|
||||
|
||||
if (type === ErrorType.NETWORK) {
|
||||
console.log('Проблемы с сетью');
|
||||
}
|
||||
```
|
||||
|
||||
### formatErrorMessage()
|
||||
|
||||
```javascript
|
||||
import { formatErrorMessage } from '../lib/api';
|
||||
|
||||
try {
|
||||
await api.get('/endpoint');
|
||||
} catch (error) {
|
||||
const message = formatErrorMessage(error);
|
||||
// "Не удалось подключиться к серверу. Проверьте соединение."
|
||||
alert(message);
|
||||
}
|
||||
```
|
||||
|
||||
### getErrorAction()
|
||||
|
||||
```javascript
|
||||
import { getErrorAction } from '../lib/api';
|
||||
|
||||
try {
|
||||
await api.post('/endpoint', data);
|
||||
} catch (error) {
|
||||
const action = getErrorAction(error);
|
||||
// "Проверьте правильность введённых данных."
|
||||
console.log('Рекомендация:', action);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Примеры
|
||||
|
||||
### Пример 1: Загрузка данных с обработкой ошибок
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||
import RetryButton from '../components/RetryButton';
|
||||
import api from '../lib/api';
|
||||
|
||||
function DataLoader() {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { withErrorHandler } = useErrorHandler();
|
||||
|
||||
const loadData = withErrorHandler(
|
||||
async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await api.get('/domains-new');
|
||||
setData(response.data);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
},
|
||||
{
|
||||
successMessage: 'Данные успешно загружены',
|
||||
context: { component: 'DataLoader' }
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-body">
|
||||
{!data ? (
|
||||
<RetryButton
|
||||
onRetry={loadData}
|
||||
loading={loading}
|
||||
>
|
||||
Загрузить данные
|
||||
</RetryButton>
|
||||
) : (
|
||||
<div>
|
||||
<p>Загружено: {data.items.length} записей</p>
|
||||
<button onClick={loadData}>Обновить</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Пример 2: Форма с валидацией
|
||||
|
||||
```jsx
|
||||
import { useState } from 'react';
|
||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||
import api from '../lib/api';
|
||||
|
||||
function DomainForm() {
|
||||
const [domain, setDomain] = useState('');
|
||||
const { handleError, handleSuccess } = useErrorHandler();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
try {
|
||||
await api.post('/domains-new', {
|
||||
domains: [{ domain, community: '65000:100' }]
|
||||
});
|
||||
handleSuccess('Домен успешно добавлен');
|
||||
setDomain('');
|
||||
} catch (error) {
|
||||
// Система автоматически покажет user-friendly сообщение
|
||||
// "Ошибка валидации данных. Проверьте введённые значения."
|
||||
handleError(error, { form: 'DomainForm', domain });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input
|
||||
type="text"
|
||||
value={domain}
|
||||
onChange={(e) => setDomain(e.target.value)}
|
||||
placeholder="example.com"
|
||||
/>
|
||||
<button type="submit">Добавить</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Пример 3: Обработка критичных ошибок
|
||||
|
||||
```jsx
|
||||
import { useEffect } from 'react';
|
||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||
import { isCriticalError } from '../lib/api';
|
||||
import api from '../lib/api';
|
||||
|
||||
function CriticalDataLoader() {
|
||||
const { handleError } = useErrorHandler();
|
||||
|
||||
useEffect(() => {
|
||||
const loadCriticalData = async () => {
|
||||
try {
|
||||
await api.get('/server-configs');
|
||||
} catch (error) {
|
||||
if (isCriticalError(error)) {
|
||||
// Критичная ошибка - показываем модальное окно
|
||||
handleError(error);
|
||||
// Можно также перенаправить на страницу ошибки
|
||||
// window.location.href = '/error';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadCriticalData();
|
||||
}, [handleError]);
|
||||
|
||||
return <div>...</div>;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Лучшие практики
|
||||
|
||||
### 1. Всегда используйте useErrorHandler
|
||||
|
||||
```jsx
|
||||
// ✅ Хорошо
|
||||
import { useErrorHandler } from '../hooks/useErrorHandler';
|
||||
|
||||
function MyComponent() {
|
||||
const { handleError } = useErrorHandler();
|
||||
// ...
|
||||
}
|
||||
|
||||
// ❌ Плохо
|
||||
function MyComponent() {
|
||||
const handleError = (error) => {
|
||||
alert(error.message); // Не user-friendly
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Передавайте контекст в handleError
|
||||
|
||||
```jsx
|
||||
// ✅ Хорошо
|
||||
handleError(error, {
|
||||
component: 'DomainsManager',
|
||||
action: 'delete',
|
||||
itemId: domain.id
|
||||
});
|
||||
|
||||
// ❌ Плохо
|
||||
handleError(error);
|
||||
```
|
||||
|
||||
### 3. Используйте withErrorHandler для простых случаев
|
||||
|
||||
```jsx
|
||||
// ✅ Хорошо - лаконично
|
||||
const loadData = withErrorHandler(
|
||||
async () => {
|
||||
const res = await api.get('/data');
|
||||
setData(res.data);
|
||||
},
|
||||
{ successMessage: 'Загружено' }
|
||||
);
|
||||
|
||||
// ❌ Избыточно
|
||||
const loadData = async () => {
|
||||
try {
|
||||
const res = await api.get('/data');
|
||||
setData(res.data);
|
||||
handleSuccess('Загружено');
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 4. Не дублируйте обработку ошибок
|
||||
|
||||
```jsx
|
||||
// ✅ Хорошо - API автоматически показывает уведомление
|
||||
try {
|
||||
await api.post('/data', payload);
|
||||
} catch (error) {
|
||||
// Ошибка уже показана пользователю
|
||||
// Здесь только локальная обработка
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// ❌ Плохо - двойное уведомление
|
||||
try {
|
||||
await api.post('/data', payload);
|
||||
} catch (error) {
|
||||
handleError(error); // Уведомление показано дважды!
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Используйте RetryButton для операций
|
||||
|
||||
```jsx
|
||||
// ✅ Хорошо
|
||||
<RetryButton onRetry={saveData}>
|
||||
Сохранить
|
||||
</RetryButton>
|
||||
|
||||
// ❌ Плохо - ручная реализация retry
|
||||
<button onClick={async () => {
|
||||
let retries = 0;
|
||||
while (retries < 3) {
|
||||
try {
|
||||
await saveData();
|
||||
break;
|
||||
} catch {
|
||||
retries++;
|
||||
}
|
||||
}
|
||||
}}>
|
||||
Сохранить
|
||||
</button>
|
||||
```
|
||||
|
||||
### 6. Логируйте критичные ошибки
|
||||
|
||||
```jsx
|
||||
// ✅ Хорошо
|
||||
import { isCriticalError, logError } from '../lib/api';
|
||||
|
||||
catch (error) {
|
||||
if (isCriticalError(error)) {
|
||||
logError(error, { critical: true, user: currentUser });
|
||||
}
|
||||
handleError(error);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Настройка
|
||||
|
||||
### Изменение таймаута
|
||||
|
||||
```javascript
|
||||
// frontend/src/lib/api.js
|
||||
const api = axios.create({
|
||||
timeout: 30000, // 30 секунд (по умолчанию)
|
||||
});
|
||||
```
|
||||
|
||||
### Настройка retry параметров
|
||||
|
||||
```javascript
|
||||
// frontend/src/lib/apiErrorHandler.js
|
||||
|
||||
// Изменить базовую задержку
|
||||
export function getRetryDelay(attemptNumber, baseDelay = 300) {
|
||||
// Меняем baseDelay для другой стратегии
|
||||
}
|
||||
|
||||
// Изменить количество попыток
|
||||
export function getMaxRetries(errorType, method = 'GET') {
|
||||
if (method === 'GET') {
|
||||
return errorType === ErrorType.NETWORK ? 5 : 3; // Больше попыток
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### Интеграция с Sentry
|
||||
|
||||
```javascript
|
||||
// frontend/src/lib/apiErrorHandler.js
|
||||
|
||||
export function logError(error, context = {}) {
|
||||
// ...
|
||||
|
||||
// Добавить отправку в Sentry
|
||||
if (window.Sentry && isCriticalError(error)) {
|
||||
window.Sentry.captureException(error, {
|
||||
contexts: {
|
||||
api: getErrorDetails(error),
|
||||
custom: context
|
||||
},
|
||||
level: 'error'
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 Дополнительные ресурсы
|
||||
|
||||
- [Примеры использования](./frontend/src/examples/ErrorHandlingExample.jsx)
|
||||
- [Axios документация](https://axios-http.com/docs/handling_errors)
|
||||
- [React Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)
|
||||
- [Exponential Backoff](https://en.wikipedia.org/wiki/Exponential_backoff)
|
||||
|
||||
---
|
||||
|
||||
## 📝 Changelog
|
||||
|
||||
### v1.0.0 (2025-10-03)
|
||||
- ✅ Добавлен ErrorBoundary для React ошибок
|
||||
- ✅ Добавлен NetworkErrorHandler для offline/online
|
||||
- ✅ Улучшен retry механизм с exponential backoff
|
||||
- ✅ Добавлена типизация ошибок (11 типов)
|
||||
- ✅ User-friendly сообщения вместо технических
|
||||
- ✅ Хук useErrorHandler для компонентов
|
||||
- ✅ Компонент RetryButton
|
||||
- ✅ Интеграция с системой уведомлений
|
||||
- ✅ Логирование критичных ошибок
|
||||
- ✅ Увеличен таймаут до 30 секунд
|
||||
|
||||
---
|
||||
|
||||
## 🤝 Поддержка
|
||||
|
||||
Если у вас возникли вопросы или проблемы с системой обработки ошибок:
|
||||
|
||||
1. Проверьте консоль браузера на наличие ошибок
|
||||
2. Проверьте вкладку Network в DevTools
|
||||
3. Убедитесь что backend сервер запущен
|
||||
4. Проверьте настройки CORS
|
||||
|
||||
**Полезные команды для отладки:**
|
||||
|
||||
```javascript
|
||||
// В консоли браузера
|
||||
localStorage.clear(); // Очистить кэш
|
||||
window.notify.clear(); // Очистить уведомления
|
||||
console.log(window.notify); // Проверить систему уведомлений
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user