Files
router-lists-ui/ERROR_HANDLING_GUIDE.md
T

19 KiB
Raw Blame History

🛡️ Руководство по обработке ошибок

Комплексная система обработки ошибок для Router Lists UI, включающая автоматический retry, user-friendly сообщения и мониторинг состояния сети.

📚 Оглавление


🧩 Компоненты системы

1. ErrorBoundary

Глобальный обработчик ошибок React для ловли ошибок рендеринга.

Возможности:

  • ✅ Ловит ошибки рендеринга и показывает fallback UI
  • ✅ Логирует ошибки в консоль и систему мониторинга
  • ✅ Показывает детали ошибки в dev режиме
  • ✅ Автоматически очищает кэш при повторяющихся ошибках
  • ✅ Кнопки восстановления: "Попробовать снова", "Перезагрузить", "На главную"

Использование:

// Уже встроен в App.jsx, оборачивает все приложение
<ErrorBoundary>
  <App />
</ErrorBoundary>

2. NetworkErrorHandler

Компонент для мониторинга состояния сети.

Возможности:

  • ✅ Детектирует offline/online события
  • ✅ Показывает баннер при потере соединения
  • ✅ Уведомляет о восстановлении соединения
  • ✅ Интегрируется с глобальной системой уведомлений

Использование:

// Уже встроен в 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:

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: Ручная обработка

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: Автоматическая обработка

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

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 интеграция

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()

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()

import { getErrorType, ErrorType } from '../lib/api';

const error = new Error('Network error');
const type = getErrorType(error);

if (type === ErrorType.NETWORK) {
  console.log('Проблемы с сетью');
}

formatErrorMessage()

import { formatErrorMessage } from '../lib/api';

try {
  await api.get('/endpoint');
} catch (error) {
  const message = formatErrorMessage(error);
  // "Не удалось подключиться к серверу. Проверьте соединение."
  alert(message);
}

getErrorAction()

import { getErrorAction } from '../lib/api';

try {
  await api.post('/endpoint', data);
} catch (error) {
  const action = getErrorAction(error);
  // "Проверьте правильность введённых данных."
  console.log('Рекомендация:', action);
}

🎯 Примеры

Пример 1: Загрузка данных с обработкой ошибок

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: Форма с валидацией

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: Обработка критичных ошибок

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

// ✅ Хорошо
import { useErrorHandler } from '../hooks/useErrorHandler';

function MyComponent() {
  const { handleError } = useErrorHandler();
  // ...
}

// ❌ Плохо
function MyComponent() {
  const handleError = (error) => {
    alert(error.message); // Не user-friendly
  };
}

2. Передавайте контекст в handleError

// ✅ Хорошо
handleError(error, {
  component: 'DomainsManager',
  action: 'delete',
  itemId: domain.id
});

// ❌ Плохо
handleError(error);

3. Используйте withErrorHandler для простых случаев

// ✅ Хорошо - лаконично
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. Не дублируйте обработку ошибок

// ✅ Хорошо - API автоматически показывает уведомление
try {
  await api.post('/data', payload);
} catch (error) {
  // Ошибка уже показана пользователю
  // Здесь только локальная обработка
  setLoading(false);
}

// ❌ Плохо - двойное уведомление
try {
  await api.post('/data', payload);
} catch (error) {
  handleError(error); // Уведомление показано дважды!
}

5. Используйте RetryButton для операций

// ✅ Хорошо
<RetryButton onRetry={saveData}>
  Сохранить
</RetryButton>

// ❌ Плохо - ручная реализация retry
<button onClick={async () => {
  let retries = 0;
  while (retries < 3) {
    try {
      await saveData();
      break;
    } catch {
      retries++;
    }
  }
}}>
  Сохранить
</button>

6. Логируйте критичные ошибки

// ✅ Хорошо
import { isCriticalError, logError } from '../lib/api';

catch (error) {
  if (isCriticalError(error)) {
    logError(error, { critical: true, user: currentUser });
  }
  handleError(error);
}

🔧 Настройка

Изменение таймаута

// frontend/src/lib/api.js
const api = axios.create({
  timeout: 30000, // 30 секунд (по умолчанию)
});

Настройка retry параметров

// 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

// 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'
    });
  }
}

🎓 Дополнительные ресурсы


📝 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

Полезные команды для отладки:

// В консоли браузера
localStorage.clear();          // Очистить кэш
window.notify.clear();         // Очистить уведомления
console.log(window.notify);    // Проверить систему уведомлений