Files
router-lists-ui/ERROR_HANDLING_GUIDE.md

19 KiB
Raw Permalink 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);    // Проверить систему уведомлений