Files
router-lists-ui/ERROR_HANDLING_GUIDE.md
T

673 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🛡️ Руководство по обработке ошибок
Комплексная система обработки ошибок для 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); // Проверить систему уведомлений
```