React中Axios拦截器在accessToken过期后无法重试请求问题排查
React Refresh Token流程问题排查与修复
问题描述
在React应用中实现accessToken自动刷新流程时,使用useRefreshToken和useAxiosPrivate两个自定义Hook,遇到以下问题:
- accessToken过期触发刷新后,新token未被正确设置到重试请求中;
- 重试请求的
Content-Type为null,且出现名为[Object Object]的异常请求头; prevRequest.sent始终为undefined,无法避免重复触发刷新逻辑。
手动调用接口时一切正常,仅自动重试时出现上述问题。
原始代码
useRefreshToken Hook
const useRefreshToken = ()=> { // @ts-ignore const {setAuth,auth} = useAuth(); const refresh =async () => { const {data} = await axios.post('/token',{ token:auth.refreshToken }); // @ts-ignore setAuth(prev=>({...prev,accessToken:data.accessToken})); return data.accessToken } return refresh; }; export default useRefreshToken;
useAxiosPrivate Hook
const useAxiosPrivate = () => { const refresh = useRefreshToken(); // @ts-ignore const { auth } = useAuth(); useEffect(() => { const requestIntercept = axiosPrivate.interceptors.request.use((config: AxiosRequestConfig) => { console.log(config.headers) // @ts-ignore if (!config.headers['Authorization']) { // @ts-ignore config.headers['Authorization'] = `Bearer ${auth?.accessToken}`; } return config; }, (error) => Promise.reject(error)) const responseintercept = axiosPrivate.interceptors.response.use(response => response, async (error) => { const prevRequest = error.config; console.log("prevRequest?.sent", prevRequest?.sent) if (error?.response?.status === 403 && !prevRequest?.sent) { prevRequest.sent = true; const newAccessToken = await refresh(); console.log("NEW ACCESS TOKEN SET", newAccessToken); prevRequest.headers['Authorization'] = `Bearer ${newAccessToken}`; return axiosPrivate(prevRequest) } return Promise.reject(error) }) return () => { axiosPrivate.interceptors.request.eject(requestIntercept) axiosPrivate.interceptors.response.eject(responseintercept) } }, [auth, refresh]) return axiosPrivate; } export default useAxiosPrivate;
问题分析与修复
核心问题根源
- React状态更新的异步性:
setAuth更新全局auth状态是异步的,重试请求触发时,useAxiosPrivate中的auth仍为旧值,导致请求拦截器覆盖新token; - Axios版本差异:Axios v1.x+中
config.headers是AxiosHeaders对象而非普通JS对象,直接赋值会导致格式混乱; - 请求标记逻辑缺陷:单个请求的
sent标记无法跨请求共享,并发请求会重复触发刷新,且重试请求的新config无sent属性。
修复后代码
useRefreshToken Hook(添加异常处理)
const useRefreshToken = ()=> { const { setAuth, auth } = useAuth(); const refresh = async () => { try { const { data } = await axios.post('/token', { token: auth.refreshToken }); setAuth(prev => ({ ...prev, accessToken: data.accessToken })); return data.accessToken; } catch (err) { // 处理refreshToken过期,清空状态并跳转登录 setAuth({}); window.location.href = '/login'; throw err; } }; return refresh; }; export default useRefreshToken;
useAxiosPrivate Hook(核心逻辑修复)
import { AxiosHeaders, AxiosRequestConfig } from 'axios'; const useAxiosPrivate = () => { const refresh = useRefreshToken(); const { auth } = useAuth(); useEffect(() => { // 请求拦截器:优先保留已设置的Authorization,避免覆盖重试请求的新token const requestIntercept = axiosPrivate.interceptors.request.use((config: AxiosRequestConfig) => { const headers = AxiosHeaders.from(config.headers); if (!headers.has('Authorization') && auth?.accessToken) { headers.set('Authorization', `Bearer ${auth.accessToken}`); config.headers = headers; } return config; }, (error) => Promise.reject(error)); // 全局标记:控制是否正在刷新token,避免并发请求重复触发 let isRefreshing = false; // 存储等待重试的请求队列 let failedQueue: { resolve: (token: string) => void; reject: (err: any) => void }[] = []; const responseIntercept = axiosPrivate.interceptors.response.use( response => response, async (error) => { const prevRequest = error.config as AxiosRequestConfig & { sent?: boolean }; if (error?.response?.status === 403 && !prevRequest?.sent) { prevRequest.sent = true; if (!isRefreshing) { isRefreshing = true; try { const newAccessToken = await refresh(); // 重试所有等待的请求 failedQueue.forEach(promise => promise.resolve(newAccessToken)); failedQueue = []; // 更新当前请求的headers const headers = AxiosHeaders.from(prevRequest.headers); headers.set('Authorization', `Bearer ${newAccessToken}`); prevRequest.headers = headers; return axiosPrivate(prevRequest); } catch (err) { failedQueue.forEach(promise => promise.reject(err)); failedQueue = []; throw err; } finally { isRefreshing = false; } } else { // 等待刷新完成后再重试当前请求 return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then((token) => { const headers = AxiosHeaders.from(prevRequest.headers); headers.set('Authorization', `Bearer ${token}`); prevRequest.headers = headers; return axiosPrivate(prevRequest); }).catch(err => { throw err; }); } } return Promise.reject(error); } ); // 组件卸载时移除拦截器,避免内存泄漏和重复拦截 return () => { axiosPrivate.interceptors.request.eject(requestIntercept); axiosPrivate.interceptors.response.eject(responseIntercept); }; }, [auth, refresh]); return axiosPrivate; }; export default useAxiosPrivate;
关键修复点说明
- 新token生效问题:
- 直接使用
refresh返回的新token设置请求headers,不依赖异步更新的全局auth状态; - 添加请求队列,确保并发请求都能拿到刷新后的新token,避免重复刷新。
- 直接使用
- 请求头格式问题:
- 使用Axios官方的
AxiosHeadersAPI(from/has/set)操作请求头,避免普通对象操作导致的格式混乱; - 确保headers始终保持合法结构,不会丢失
Content-Type等原始配置。
- 使用Axios官方的
- 重复刷新问题:
- 用全局
isRefreshing标记替代单个请求的sent,更可靠地控制刷新流程; - 给
prevRequest添加类型断言,确保sent属性能被正确识别和赋值。
- 用全局
内容的提问来源于stack exchange,提问作者margherita pizza
相关产品推荐
相关产品推荐

