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:
+74
-20
@@ -1,9 +1,20 @@
|
||||
import axios from 'axios';
|
||||
import {
|
||||
getErrorType,
|
||||
isRetriableError,
|
||||
getRetryDelay,
|
||||
getMaxRetries,
|
||||
formatErrorMessage,
|
||||
getErrorDetails,
|
||||
logError,
|
||||
isCriticalError,
|
||||
getErrorAction
|
||||
} from './apiErrorHandler';
|
||||
|
||||
// Базовый axios-клиент для всего приложения
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 10000,
|
||||
timeout: 30000, // Увеличен до 30 секунд для больших запросов
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
@@ -24,7 +35,7 @@ function buildCacheKey(config) {
|
||||
}
|
||||
}
|
||||
|
||||
// Авто-ретрай для идемпотентных GET: до 2 попыток с экспоненциальной задержкой
|
||||
// Улучшенный retry interceptor с поддержкой всех методов и типов ошибок
|
||||
api.interceptors.response.use(
|
||||
(response) => {
|
||||
try {
|
||||
@@ -38,9 +49,6 @@ api.interceptors.response.use(
|
||||
if (cached) {
|
||||
return { ...response, status: 200, data: cached.data, headers: { ...cached.headers, 'x-from-cache': '1' } };
|
||||
}
|
||||
// нет кеша — вернём пустые семантически корректные данные (чтобы не падали .map)
|
||||
// вызывающий код должен ожидать типы, поэтому лучше не подменять тип неожиданно.
|
||||
// Просто пропустим дальше как есть — до второго интерсептора и обработчиков.
|
||||
return response;
|
||||
}
|
||||
// Не 304: обновляем кеш, но только если есть валидный etag
|
||||
@@ -53,35 +61,72 @@ api.interceptors.response.use(
|
||||
},
|
||||
async (error) => {
|
||||
const config = error?.config || {};
|
||||
const isGet = String(config.method || 'get').toLowerCase() === 'get';
|
||||
const status = error?.response?.status;
|
||||
const retriable = !error.response || (status >= 500 && status !== 501);
|
||||
const method = String(config.method || 'get').toUpperCase();
|
||||
|
||||
// Инициализируем счетчик попыток
|
||||
config.__retryCount = config.__retryCount || 0;
|
||||
if (isGet && retriable && config.__retryCount < 2) {
|
||||
|
||||
// Определяем тип ошибки и возможность повтора
|
||||
const errorType = getErrorType(error);
|
||||
const canRetry = isRetriableError(error, method);
|
||||
const maxRetries = getMaxRetries(errorType, method);
|
||||
|
||||
// Логируем ошибку если это критичная ошибка или последняя попытка
|
||||
if (isCriticalError(error) || config.__retryCount >= maxRetries) {
|
||||
logError(error, {
|
||||
retryAttempt: config.__retryCount,
|
||||
maxRetries,
|
||||
errorType,
|
||||
canRetry
|
||||
});
|
||||
}
|
||||
|
||||
// Проверяем возможность повтора
|
||||
if (canRetry && config.__retryCount < maxRetries) {
|
||||
config.__retryCount += 1;
|
||||
const delay = 300 * Math.pow(2, config.__retryCount - 1);
|
||||
const delay = getRetryDelay(config.__retryCount - 1);
|
||||
|
||||
// Уведомляем о повторной попытке (только для пользовательских действий)
|
||||
if (config.__retryCount === 1 && method !== 'GET' && typeof window !== 'undefined') {
|
||||
console.log(`Повторная попытка ${config.__retryCount}/${maxRetries} для ${method} ${config.url}`);
|
||||
}
|
||||
|
||||
await new Promise((r) => setTimeout(r, delay));
|
||||
return api(config);
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Нормализация ошибок и уведомления по умолчанию
|
||||
// Улучшенная нормализация ошибок с user-friendly сообщениями
|
||||
api.interceptors.response.use(
|
||||
(res) => res,
|
||||
(err) => {
|
||||
try {
|
||||
const status = err?.response?.status;
|
||||
const data = err?.response?.data || {};
|
||||
const message = data?.message || err?.message || 'Ошибка запроса';
|
||||
const code = data?.code;
|
||||
const details = data?.details;
|
||||
const requestId = data?.requestId || err?.response?.headers?.['x-request-id'] || err?.config?.headers?.['X-Request-Id'];
|
||||
if (status >= 400 && typeof window !== 'undefined' && window.notify?.error) {
|
||||
window.notify.add('error', `${message}${status ? ` (${status})` : ''}${requestId ? ` • reqId=${requestId}` : ''}`, details ? { code, requestId, details } : undefined);
|
||||
// Не показываем уведомления если это повторная попытка
|
||||
const isRetrying = err?.config?.__retryCount > 0;
|
||||
|
||||
if (!isRetrying && typeof window !== 'undefined' && window.notify) {
|
||||
const errorDetails = getErrorDetails(err);
|
||||
const userMessage = formatErrorMessage(err);
|
||||
const actionMessage = getErrorAction(err);
|
||||
|
||||
// Формируем детальное сообщение
|
||||
const fullMessage = `${userMessage}${errorDetails.status ? ` (${errorDetails.status})` : ''}`;
|
||||
const extraDetails = {
|
||||
...errorDetails,
|
||||
action: actionMessage,
|
||||
timestamp: new Date().toLocaleString('ru-RU')
|
||||
};
|
||||
|
||||
// Выбираем тип уведомления
|
||||
const notifyType = isCriticalError(err) ? 'error' : 'warning';
|
||||
window.notify.add(notifyType, fullMessage, extraDetails);
|
||||
}
|
||||
} catch {}
|
||||
} catch (notifyError) {
|
||||
console.error('Failed to show error notification:', notifyError);
|
||||
}
|
||||
return Promise.reject(err);
|
||||
}
|
||||
);
|
||||
@@ -125,6 +170,15 @@ export function unwrapStd(res) {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Экспортируем утилиты обработки ошибок для использования в компонентах
|
||||
export {
|
||||
getErrorType,
|
||||
formatErrorMessage,
|
||||
getErrorDetails,
|
||||
getErrorAction,
|
||||
isCriticalError
|
||||
} from './apiErrorHandler';
|
||||
|
||||
export default api;
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user