Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s
186 lines
7.1 KiB
JavaScript
186 lines
7.1 KiB
JavaScript
import axios from 'axios';
|
|
import { formatDateTime } from './datetime.js';
|
|
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: formatDateTime(new Date())
|
|
};
|
|
|
|
// Выбираем тип уведомления
|
|
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;
|
|
|
|
|