You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中Axios拦截器在accessToken过期后无法重试请求问题排查

React Refresh Token流程问题排查与修复

问题描述

在React应用中实现accessToken自动刷新流程时,使用useRefreshToken和useAxiosPrivate两个自定义Hook,遇到以下问题:

  1. accessToken过期触发刷新后,新token未被正确设置到重试请求中;
  2. 重试请求的Content-Type为null,且出现名为[Object Object]的异常请求头;
  3. 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;

问题分析与修复

核心问题根源

  1. React状态更新的异步性:setAuth更新全局auth状态是异步的,重试请求触发时,useAxiosPrivate中的auth仍为旧值,导致请求拦截器覆盖新token;
  2. Axios版本差异:Axios v1.x+中config.headers是AxiosHeaders对象而非普通JS对象,直接赋值会导致格式混乱;
  3. 请求标记逻辑缺陷:单个请求的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;

关键修复点说明

  1. 新token生效问题:
    • 直接使用refresh返回的新token设置请求headers,不依赖异步更新的全局auth状态;
    • 添加请求队列,确保并发请求都能拿到刷新后的新token,避免重复刷新。
  2. 请求头格式问题:
    • 使用Axios官方的AxiosHeadersAPI(from/has/set)操作请求头,避免普通对象操作导致的格式混乱;
    • 确保headers始终保持合法结构,不会丢失Content-Type等原始配置。
  3. 重复刷新问题:
    • 用全局isRefreshing标记替代单个请求的sent,更可靠地控制刷新流程;
    • 给prevRequest添加类型断言,确保sent属性能被正确识别和赋值。

内容的提问来源于stack exchange,提问作者margherita pizza

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 12:15:49