Axios+React中JWT令牌刷新时setRequestHeader执行失败
场景说明
基于access token和refresh token实现JWT认证机制:请求拦截器确保请求头包含带Bearer令牌的Authorization字段;响应拦截器检查是否需要刷新access token,发送刷新请求并更新Axios实例配置。采用TypeScript编写,代码参考Dave Gray的视频教程。
问题现象
测试时将refresh token有效期设为长期,access token设为5秒。access token过期后请求受保护路由,后端日志显示两个请求均成功:(1)受保护路由返回401,(2)刷新令牌请求成功。但浏览器控制台(Chrome、Safari)抛出DOMException,提示setRequestHeader执行失败,原因是请求头值为无效的函数,涉及Axios库AxiosHeaders.js第119行。
代码示例
const axiosPrivate = axios.create({ baseURL: BASE_URL, headers: { "Content-Type": "application/json" }, withCredentials: true, }); interface IRequestConfig extends AxiosRequestConfig { sent?: boolean; } const useAxiosPrivate = () => { const { auth } = useAuth()!; const refresh = useRefreshToken(); React.useEffect(() => { const requestInterceptor = axiosPrivate.interceptors.request.use( (config: AxiosRequestConfig) => { config.headers = config.headers ?? {}; if (!config.headers["Authorization"]) { config.headers["Authorization"] = `Bearer ${auth?.token}`; } return config; }, async (error: AxiosError): Promise<AxiosError> => { return Promise.reject(error); } ); const responseInterceptor = axiosPrivate.interceptors.response.use( (response: AxiosResponse) => response, async (error: AxiosError): Promise<AxiosError> => { const prevRequestConfig = error.config as IRequestConfig; if (error?.response?.status === 401 && !prevRequestConfig?.sent) { const newAccessToken = await refresh(); prevRequestConfig.sent = true; prevRequestConfig.headers = prevRequestConfig.headers!; prevRequestConfig.headers[ "Authorization" ] = `Bearer ${newAccessToken}`; return axiosPrivate(prevRequestConfig); } return Promise.reject(error); } ); return () => { axiosPrivate.interceptors.request.eject(requestInterceptor); axiosPrivate.interceptors.response.eject(responseInterceptor); }; }, [auth, refresh]); return axiosPrivate; };
错误信息
DOMException: Failed to execute 'setRequestHeader' on 'XMLHttpRequest': 'function (header, parser) { header = normalizeHeader(header); if (!header) return undefined; const key = findKey(this, header); if (key) { const value = this[key]; if (!parser) { return value; } if (parser === true) { return parseTokens(value); } if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isFunction(parser)) { return parser.call(this, value, key); } if (_utils_js__WEBPACK_IMPORTED_MODULE_0__["default"].isRegExp(parser)) { return parser.exec(value); } throw new TypeError('parser must be boolean|regexp|function'); } }' is not a valid HTTP header field value.
调研情况
仅在GitHub找到类似问题,有PR提示问题可能与Axios读取实例配置的方式有关,但不确定是否为Axios本身问题,需解决思路。
解决思路
核心原因
Axios v1.x版本中,config.headers默认是AxiosHeaders实例(而非普通JavaScript对象),直接对其进行赋值或修改操作时,可能误将AxiosHeaders的内部方法当作请求头值传递,导致浏览器抛出无效头值的异常。
具体修复方案
将AxiosHeaders转为普通对象后更新
修改响应拦截器中更新headers的代码,通过扩展运算符将原headers转为普通对象,再添加新的Authorization头:// 替换原代码中prevRequestConfig.headers相关行 prevRequestConfig.sent = true; prevRequestConfig.headers = { ...prevRequestConfig.headers, Authorization: `Bearer ${newAccessToken}` };使用AxiosHeaders的官方API修改头信息
利用Axios提供的AxiosHeaders.isAxiosHeaders判断类型,调用set方法修改Authorization头,符合Axios的规范:import { AxiosHeaders } from 'axios'; // 在响应拦截器内 const prevRequestConfig = error.config as IRequestConfig; if (error?.response?.status === 401 && !prevRequestConfig?.sent) { const newAccessToken = await refresh(); prevRequestConfig.sent = true; if (AxiosHeaders.isAxiosHeaders(prevRequestConfig.headers)) { prevRequestConfig.headers.set('Authorization', `Bearer ${newAccessToken}`); } else { prevRequestConfig.headers = { ...prevRequestConfig.headers, Authorization: `Bearer ${newAccessToken}` }; } return axiosPrivate(prevRequestConfig); }优化请求拦截器的headers处理
请求拦截器中避免直接覆盖config.headers为普通对象,改用set方法操作:const requestInterceptor = axiosPrivate.interceptors.request.use( (config: AxiosRequestConfig) => { if (!config.headers?.has('Authorization')) { config.headers?.set('Authorization', `Bearer ${auth?.token}`); } return config; }, async (error: AxiosError): Promise<AxiosError> => { return Promise.reject(error); } );
内容的提问来源于stack exchange,提问作者Lev Pleshkov

