React Query请求遇401时如何全局实现刷新Token自动重试逻辑
React Query 全局处理401自动刷新Token方案
完全不需要逐个给上千个请求加onError逻辑,React Query提供了全局配置入口,一处配置即可对所有query、mutation生效,具体实现如下:
核心实现逻辑
- 基于React Query全局的QueryCache、MutationCache捕获所有请求错误,无需单独给单个请求配置
- 增加刷新Token的并发锁,避免多请求同时401时重复发起刷新请求
- 刷新Token成功后自动重发原请求,刷新失败则清空登录态跳转登录页
具体实现步骤
1. 封装带并发锁的Token刷新方法
锁机制是核心,避免并发重复刷新导致的Token混乱、接口压力问题:
// 全局刷新锁,存储正在执行的刷新请求Promise let refreshTokenPromise = null async function getFreshAccessToken() { // 已有正在执行的刷新请求时,直接返回该Promise,避免重复请求 if (refreshTokenPromise) return refreshTokenPromise const storedRefreshToken = localStorage.getItem('refreshToken') if (!storedRefreshToken) { throw new Error('refresh token not exist') } // 加锁 refreshTokenPromise = (async () => { try { const resp = await fetch('/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken: storedRefreshToken }) }) if (!resp.ok) throw new Error('refresh failed') const { accessToken } = await resp.json() // 更新本地存储的新Access Token localStorage.setItem('accessToken', accessToken) return accessToken } finally { // 无论刷新成功/失败,执行完立即释放锁 refreshTokenPromise = null } })() return refreshTokenPromise }
2. 初始化QueryClient时添加全局错误处理
不要在defaultOptions.queries里写onError,直接用QueryCache、MutationCache的全局错误回调,优先级更高,不会被单个请求自定义的onError覆盖:
import { QueryClient, QueryCache, MutationCache } from '@tanstack/react-query' const queryClient = new QueryClient({ defaultOptions: { queries: { // 全局重试规则:仅401错误最多重试1次,避免死循环 retry: (failureCount, error) => { if (failureCount > 1) return false return error?.response?.status === 401 } }, mutations: { // mutation和query保持一致的重试规则 retry: (failureCount, error) => { if (failureCount > 1) return false return error?.response?.status === 401 } } }, // 全局捕获所有query错误 queryCache: new QueryCache({ onError: async (error, failedQuery) => { if (error?.response?.status !== 401) return try { await getFreshAccessToken() // 刷新成功后,重发失败的query queryClient.invalidateQueries({ queryKey: failedQuery.queryKey }) } catch (refreshErr) { // 刷新失败,清空登录态跳登录页 localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') window.location.replace('/login') } } }), // 全局捕获所有mutation错误 mutationCache: new MutationCache({ onError: async (error, variables, _, failedMutation) => { if (error?.response?.status !== 401) return try { await getFreshAccessToken() // 刷新成功后,用原参数重发失败的mutation failedMutation.execute(variables) } catch (refreshErr) { localStorage.removeItem('accessToken') localStorage.removeItem('refreshToken') window.location.replace('/login') } } }) }) // 正常用QueryClientProvider包裹根组件即可,所有子组件里的useQuery、useMutation自动继承该配置
3. 统一请求封装注意事项
所有请求发出去前,必须实时从localStorage读取最新的accessToken,不要在封装时缓存初始token值,否则刷新token后还是会携带旧token发起请求:
// 基础请求封装示例 async function request(url, options = {}) { const accessToken = localStorage.getItem('accessToken') const resp = await fetch(url, { ...options, headers: { 'Content-Type': 'application/json', ...(options.headers || {}), ...(accessToken ? { Authorization: `Bearer ${accessToken}` } : {}) } }) if (!resp.ok) { const err = new Error(`Request failed: ${resp.status}`) // 把状态码挂载到错误对象上,供React Query识别 err.response = { status: resp.status } throw err } return resp.json() }
补充说明
- 如果你项目里用了axios、fetch wrapper之类的请求库,也可以把401拦截+刷token的逻辑写在请求库的全局拦截器里,和上述React Query全局配置方案二选一即可,两种都不需要修改现有业务请求代码
- 重试次数必须限制为1次,否则刷新接口本身返回401时会触发无限重试
- 并发锁逻辑必须加,否则页面同时触发多个401请求时,会瞬间发起多次刷新Token请求,极易导致Token覆盖混乱、刷新接口被打挂
内容的提问来源于stack exchange,提问作者Dmitriy_kzn
相关产品推荐
相关产品推荐

