Конфигурация Axios — Полный код¶
Уровень: L3 (deep-dive) | ← Назад к API Layer
Файл: frontend/src/api/client.ts¶
import axios from 'axios';
import { useAuthStore } from '../stores/auth';
// Базовый URL из переменной окружения, fallback на /api для проксирования через Vite
const baseURL = import.meta.env.VITE_API_URL || '/api';
// Создание единственного экземпляра axios для всего приложения
export const apiClient = axios.create({
baseURL,
headers: { 'Content-Type': 'application/json' },
});
// ─── REQUEST INTERCEPTOR ─────────────────────────────────────────────────────
// Автоматически добавляет Bearer-токен к каждому запросу
apiClient.interceptors.request.use((config) => {
const { accessToken } = useAuthStore.getState();
if (accessToken) {
config.headers.Authorization = `Bearer ${accessToken}`;
}
return config;
});
// ─── RESPONSE INTERCEPTOR — AUTO REFRESH ─────────────────────────────────────
// Флаг: идёт ли сейчас процесс обновления токена
let isRefreshing = false;
// Очередь запросов, ожидающих завершения refresh
let failedQueue: Array<{
resolve: (value: unknown) => void;
reject: (reason: unknown) => void;
}> = [];
// Обработка очереди после завершения refresh (успех или ошибка)
function processQueue(error: unknown) {
failedQueue.forEach(({ resolve, reject }) => {
if (error) {
reject(error); // Refresh не удался — отклонить все ожидающие запросы
} else {
resolve(undefined); // Refresh успешен — повторить запросы
}
});
failedQueue = [];
}
apiClient.interceptors.response.use(
(response) => response, // Успешные ответы проходят без изменений
async (error) => {
const originalRequest = error.config;
// Пропустить, если не 401 или запрос уже повторялся
if (error.response?.status !== 401 || originalRequest._retry) {
return Promise.reject(error);
}
// Пропустить auth-эндпоинты (избежать бесконечного цикла)
if (originalRequest.url?.includes('/auth/refresh') || originalRequest.url?.includes('/auth/login')) {
return Promise.reject(error);
}
// Если refresh уже идёт — поставить запрос в очередь
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject });
}).then(() => apiClient(originalRequest)); // После refresh — повторить запрос
}
// Пометить запрос как повторяемый (защита от рекурсии)
originalRequest._retry = true;
isRefreshing = true;
const { refreshToken, setTokens, logout } = useAuthStore.getState();
// Нет refresh token — сразу logout
if (!refreshToken) {
await logout();
isRefreshing = false;
return Promise.reject(error);
}
try {
// Запрос нового access token
const { data } = await axios.post(`${baseURL}/auth/refresh`, {
refresh_token: refreshToken,
});
// Сохранить новые токены в store
setTokens(data.access_token, data.refresh_token);
// Обработать очередь ожидающих запросов (без ошибки = повторить)
processQueue(null);
// Повторить оригинальный запрос с новым токеном
return apiClient(originalRequest);
} catch (refreshError) {
// Refresh не удался — отклонить всю очередь и выйти
processQueue(refreshError);
await logout();
return Promise.reject(refreshError);
} finally {
isRefreshing = false;
}
},
);
Разбор логики refresh¶
Проблема¶
При истечении access token (обычно 15-30 мин) любой API-запрос вернёт 401. Если в этот момент выполняется несколько параллельных запросов — все получат 401 одновременно.
Решение: Queue-based refresh¶
- Первый 401 → устанавливает
isRefreshing = true, начинает refresh - Последующие 401 → запросы помещаются в
failedQueue(Promise) - Refresh успешен →
processQueue(null)разрешает все Promise в очереди - Refresh неудачен →
processQueue(error)отклоняет все Promise
Защита от бесконечного цикла¶
originalRequest._retry = true— запрос не будет повторяться дважды- Проверка URL:
/auth/refreshи/auth/loginне обрабатываются interceptor'ом isRefreshing— предотвращает параллельные refresh-запросы
Взаимодействие с Zustand Store¶
accessToken— вставляется в каждый запрос (request interceptor)refreshToken— используется для обновленияsetTokens()— обновляет оба токена после refreshlogout()— очищает данные при неудачном refresh