import axios from 'axios'; import { getErrorType, isRetriableError, getRetryDelay, getMaxRetries, formatErrorMessage, getErrorDetails, logError, isCriticalError, getErrorAction } from './apiErrorHandler'; // Базовый axios-клиент для всего приложения const api = axios.create({ baseURL: '/api', timeout: 30000, // Увеличен до 30 секунд для больших запросов headers: { 'X-Requested-With': 'XMLHttpRequest', }, validateStatus: (status) => (status >= 200 && status < 300) || status === 304, }); // Простое кеширование ответов GET по ключу запроса + ETag const responseCache = new Map(); // key -> { etag, data, headers } function buildCacheKey(config) { try { const url = config.baseURL ? new URL(config.url, 'http://x').toString().replace('http://x','') : config.url; const params = config.params || {}; const keys = Object.keys(params).sort(); const qs = keys.map(k => `${k}=${encodeURIComponent(params[k])}`).join('&'); return `${config.method || 'get'} ${url}${qs ? '?' + qs : ''}`; } catch { return `${config.method || 'get'} ${config.url}`; } } // Улучшенный retry interceptor с поддержкой всех методов и типов ошибок api.interceptors.response.use( (response) => { try { const method = String(response?.config?.method || 'get').toLowerCase(); if (method === 'get') { const key = buildCacheKey(response.config); const etag = response.headers?.etag; // Если 304 — всегда пробуем вернуть кеш, не трогая response.data if (response.status === 304) { const cached = responseCache.get(key); if (cached) { return { ...response, status: 200, data: cached.data, headers: { ...cached.headers, 'x-from-cache': '1' } }; } return response; } // Не 304: обновляем кеш, но только если есть валидный etag if (etag) { responseCache.set(key, { etag, data: response.data, headers: response.headers }); } } } catch {} return response; }, async (error) => { const config = error?.config || {}; const method = String(config.method || 'get').toUpperCase(); // Инициализируем счетчик попыток config.__retryCount = config.__retryCount || 0; // Определяем тип ошибки и возможность повтора 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 = 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 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 (notifyError) { console.error('Failed to show error notification:', notifyError); } return Promise.reject(err); } ); // Request интерсептор: If-Match из etag (если не задан явный заголовок) api.interceptors.request.use((config) => { try { // If-None-Match для GET на базе кеша const method = String(config.method || 'get').toLowerCase(); if (method === 'get') { // Исключение: для /communities всегда запрашиваем свежие данные (без условного GET) const rawUrl = String(config.url || ''); const pathOnly = rawUrl.split('?')[0]; const isCommunities = pathOnly === '/communities' || pathOnly.endsWith('/communities'); if (!isCommunities) { const key = buildCacheKey(config); const cached = responseCache.get(key); if (cached?.etag) { config.headers = config.headers || {}; if (!config.headers['If-None-Match']) config.headers['If-None-Match'] = String(cached.etag); } } else if (config.headers && config.headers['If-None-Match']) { // На всякий случай удалим, если был выставлен где-то ещё delete config.headers['If-None-Match']; } } // Если заголовок не указан, но в теле есть etag — пробуем проставить If-Match if (!config.headers?.['If-Match'] && config.data && typeof config.data === 'object' && config.data.etag) { config.headers = config.headers || {}; config.headers['If-Match'] = String(config.data.etag); } } catch {} return config; }); // Утилита: опциональная распаковка стандартного формата export function unwrapStd(res) { const data = res?.data; if (data && typeof data === 'object' && Array.isArray(data.items)) return data.items; return data; } // Экспортируем утилиты обработки ошибок для использования в компонентах export { getErrorType, formatErrorMessage, getErrorDetails, getErrorAction, isCriticalError } from './apiErrorHandler'; export default api;