Redux Toolkit Query自动重授权调用/refresh接口报403错误
问题现象
基于Redux Toolkit Query实现自动重授权逻辑时,调用/refresh端点拉取新access token固定返回如下错误:
error: data: "Forbidden" error: "SyntaxError: Unexpected token F in JSON at position 0" originalStatus: 403 status: "PARSING_ERROR"
相同请求用axios发起、通过Insomnia接口客户端测试都能正常返回access token,问题实现代码如下:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' import { setCredentials, logOut } from '../component-slice' const baseQuery = fetchBaseQuery({ baseUrl: "http://localhost:5000/api", credentials: 'include', prepareHeaders: (headers, { getState }) => { const token = getState().componentSlice.token if (token) { headers.set('authorization', `Bearer ${token}`) } return headers } }) const baseQueryWithReauth = async (args, api, extraOptions) => { let result = await baseQuery(args, api, extraOptions) if (result?.error?.originalStatus === 403) { console.log('sending refresh token') // 发送refresh token换取新access token const refreshResult = await baseQuery('/refresh', api, extraOptions) console.log(refreshResult) if (refreshResult?.data) { const user = api.getState().componentSlice.user // 存储新token api.dispatch(setCredentials({ ...refreshResult.data, user })) // 用新token重试原请求 result = await baseQuery(args, api, extraOptions) } else { api.dispatch(logOut()) } } return result } export const userApi = createApi({ reducerPath: 'api', baseQuery: baseQueryWithReauth, tagTypes: ['Product', 'List', 'category'], endpoints: builder => ({ getProfile: builder.mutation({ query: () => ({ url: `/profile`, credentials: 'include', method: 'GET' }) }), googleLogout: builder.mutation({ query: () => ({ url: `/google/logout`, method: 'GET', credentials: 'include' }) }) }) })
根因分析
- 你配置的
prepareHeaders会给所有发出去的请求自动注入本地存储的access token,没有做任何路径/场景排除。 - 当原请求因为access token过期返回403时,本地存储的还是那个已经失效的旧token,此时调用
/refresh接口,prepareHeaders依然会把这个过期token塞到Authorization请求头里发给后端。 - 后端拿到带无效Authorization头的请求,直接拦截返回纯文本格式的
Forbidden(403状态码),根本不会走到读取cookie中refresh token、签发新access token的逻辑。 - RTKQ的
fetchBaseQuery默认按JSON格式解析响应体,解析纯文本Forbidden的第一个字符F时就抛出解析错误,也就是你看到的PARSING_ERROR。 - 用axios、Insomnia调用时能正常返回,是因为这两个场景下你没有主动给请求加上那个已经过期的Authorization头,后端会正常校验cookie里的refresh token。
修复方案
核心逻辑:调用/refresh接口时,不要注入已经失效的旧access token,两种实现方式选一种即可:
方式1:调用refresh接口时主动跳过鉴权头注入
修改重授权逻辑中调用/refresh的部分,显式标记该请求不需要携带access token,同时调整prepareHeaders逻辑识别该标记:
const baseQuery = fetchBaseQuery({ baseUrl: "http://localhost:5000/api", credentials: 'include', prepareHeaders: (headers, { getState }) => { const token = getState().componentSlice.token // 标记了skipAuth的请求不注入access token if (token && !headers.get('X-Skip-Auth')) { headers.set('authorization', `Bearer ${token}`) } // 移除自定义标记头,不要发给后端 headers.delete('X-Skip-Auth') return headers } }) const baseQueryWithReauth = async (args, api, extraOptions) => { let result = await baseQuery(args, api, extraOptions) if (result?.error?.originalStatus === 403) { console.log('sending refresh token') // 标记refresh请求跳过鉴权头注入 const refreshResult = await baseQuery( { url: '/refresh', headers: { 'X-Skip-Auth': 'true' } }, api, extraOptions ) console.log(refreshResult) if (refreshResult?.data) { const user = api.getState().componentSlice.user api.dispatch(setCredentials({ ...refreshResult.data, user })) result = await baseQuery(args, api, extraOptions) } else { api.dispatch(logOut()) } } return result }
方式2:直接在prepareHeaders中排除refresh路径
如果不想加自定义标记,也可以直接在头注入逻辑中判断请求路径,遇到/refresh时跳过token注入:
prepareHeaders: (headers, { getState, url }) => { const token = getState().componentSlice.token // refresh接口不注入access token if (token && url !== '/refresh') { headers.set('authorization', `Bearer ${token}`) } return headers }
补充说明:如果你的
/refresh接口本身返回的不是JSON格式(比如纯文本token),可以在调用时单独配置responseHandler: 'text',但根据你的描述接口正常返回JSON格式的token数据,不需要额外配置。
内容的提问来源于stack exchange,提问作者Chigalakingsley
相关产品推荐
相关产品推荐

