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

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'
      })
    })
  })
})
根因分析
  1. 你配置的prepareHeaders会给所有发出去的请求自动注入本地存储的access token,没有做任何路径/场景排除。
  2. 当原请求因为access token过期返回403时,本地存储的还是那个已经失效的旧token,此时调用/refresh接口,prepareHeaders依然会把这个过期token塞到Authorization请求头里发给后端。
  3. 后端拿到带无效Authorization头的请求,直接拦截返回纯文本格式的Forbidden(403状态码),根本不会走到读取cookie中refresh token、签发新access token的逻辑。
  4. RTKQ的fetchBaseQuery默认按JSON格式解析响应体,解析纯文本Forbidden的第一个字符F时就抛出解析错误,也就是你看到的PARSING_ERROR。
  5. 用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 10:21:23