import axios from 'axios'; // Базовый axios-клиент для всего приложения const api = axios.create({ baseURL: '/api', timeout: 10000, headers: { 'X-Requested-With': 'XMLHttpRequest', }, }); // Авто-ретрай для идемпотентных GET: до 2 попыток с экспоненциальной задержкой api.interceptors.response.use( (response) => response, 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); config.__retryCount = config.__retryCount || 0; if (isGet && retriable && config.__retryCount < 2) { config.__retryCount += 1; const delay = 300 * Math.pow(2, config.__retryCount - 1); await new Promise((r) => setTimeout(r, delay)); return api(config); } return Promise.reject(error); } ); // Нормализация ошибок и уведомления по умолчанию api.interceptors.response.use( (res) => res, (err) => { try { const status = err?.response?.status; const message = err?.response?.data?.message || err?.message || 'Ошибка запроса'; if (status >= 400 && typeof window !== 'undefined' && window.notify?.error) { window.notify.error(`${message} (${status ?? '—'})`); } } catch {} return Promise.reject(err); } ); // Request интерсептор: If-Match из etag (если не задан явный заголовок) api.interceptors.request.use((config) => { try { // Если заголовок не указан, но в теле есть 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 default api;