Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
673 lines
19 KiB
Markdown
673 lines
19 KiB
Markdown
# 🛡️ Руководство по обработке ошибок
|
||
|
||
Комплексная система обработки ошибок для 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); // Проверить систему уведомлений
|
||
```
|
||
|