Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 | 1x 1x 1x 1x 1x 1x 1x 6x 6x 2x 1x 1x 1x 1x 6x 6x 6x 1x 1x 1x 1x 1x 1x 1x 5x 5x 2x 2x 1x 1x 1x 1x 2x 5x 3x 3x 4x 4x 1x 1x 1x 10x 10x 10x 9x 9x 10x 8x 2x 2x 2x 2x 1x 1x 1x 1x 1x 2x 6x 6x 6x 6x 6x 6x 8x 1x 1x 8x 4x 4x 4x 4x 4x 4x 2x 2x 2x 2x 2x 2x 2x 10x 1x 1x | import axios from "axios";
import { useAuthStore } from "../stores/authStore";
import { refreshSession, isTokenExpiringSoon } from "./auth";
let isRefreshing = false;
let refreshPromise: Promise<string> | null = null;
let failedQueue: Array<{
resolve: (token: string) => void;
reject: (err: unknown) => void;
}> = [];
function processQueue(error: unknown, token: string | null = null) {
failedQueue.forEach((prom) => {
if (error) {
prom.reject(error);
} else if (token) {
prom.resolve(token);
}
});
failedQueue = [];
}
const apiClient = axios.create({
baseURL: "http://localhost:8000/api/v1",
headers: {
"Content-Type": "application/json",
},
});
apiClient.interceptors.request.use(async (config) => {
const token = localStorage.getItem("access_token");
// Proactive refresh: if token is expiring soon and this isn't the refresh call itself
if (token && isTokenExpiringSoon() && !config.url?.includes("/auth/")) {
try {
const newToken = await refreshSession();
config.headers.Authorization = `Bearer ${newToken}`;
return config;
} catch {
// Proactive refresh failed; let response interceptor handle 401
void 0;
}
}
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
apiClient.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
// Only handle 401s that haven't been retried yet
if (
error.response?.status === 401 &&
originalRequest &&
!originalRequest._retry
) {
// If already refreshing, queue this request
if (isRefreshing) {
try {
const token = await new Promise<string>((resolve, reject) => {
failedQueue.push({ resolve, reject });
});
originalRequest.headers.Authorization = `Bearer ${token}`;
return apiClient(originalRequest);
} catch (err) {
return Promise.reject(err);
}
}
originalRequest._retry = true;
isRefreshing = true;
let newToken: string;
// Only wrap the actual refresh operation in the try/catch
try {
refreshPromise = refreshSession();
newToken = await refreshPromise;
if (!newToken) {
throw new Error("Session refresh returned empty token");
}
} catch (refreshError) {
processQueue(refreshError, null);
// Graceful state wipe triggering React Router redirect
useAuthStore.getState().logout();
isRefreshing = false;
refreshPromise = null;
return Promise.reject(refreshError);
}
// Success path execution
isRefreshing = false;
refreshPromise = null;
processQueue(null, newToken);
originalRequest.headers.Authorization = `Bearer ${newToken}`;
// Returning the un-awaited Promise here avoids catching retry failures
// as "refresh failures" and safely sidesteps the V8 coverage glitch.
return apiClient(originalRequest);
}
return Promise.reject(error);
},
);
export default apiClient;
|