Nuxt3自定义useFetch异常:401刷新token后请求未执行
问题:Nuxt3自定义useFetch的token刷新与请求队列失效问题
需求:当请求返回401且Token过期时,自动刷新token,同时将新发起的请求存入队列等待刷新完成;刷新成功后,执行队列中的请求并重试原本失败的请求。
现状:token刷新能正常执行,但原请求和队列中的请求均未正确执行,请求直接失败。
原代码
import type { FetchResponse, SearchParameters } from 'ofetch' import type { UseFetchOptions } from '#app' import { cloneDeep } from 'lodash' import { useUserStore } from '../stores/userStore' import { refreshToken } from '../apis/auth' import { defu } from 'defu' let isRefreshing = false const requestQueue: Array<{ resolve: Function, reject: Function }> = [] export interface ResOptions<T> { data?: T code?: number message?: string } type UrlType = string | Request | Ref<string | Request> | (() => string | Request) export type HttpOption<T> = UseFetchOptions<ResOptions<T>> export const myFetch = async <T>(url: UrlType, httpOptions: HttpOption<T> = {}) => { const userStore = useUserStore() const config = useRuntimeConfig() const defaults: HttpOption<T> = { baseURL: `${config.public.apiBase}/api`, async onRequest({ request, options }) { options.headers = options.headers || {} if (userStore.isLogin) { (options.headers as { [key: string]: string }).authorization = `Bearer ${userStore.token}` } }, async onResponse({ request, response, options }) { if (response._data.code !== 200) { console.log(response._data.message); } // return response._data }, async onResponseError({ request, response }) { if (response._data.code === 401 && response.headers.get('Token-Expired') === 'true') { if (!isRefreshing) { const refreshPromise = new Promise((resolve, reject) => { requestQueue.push({ resolve, reject }) }) isRefreshing = true const refreshSuccess = await refreshToken() isRefreshing = false if (refreshSuccess) { for (const { resolve, reject } of requestQueue) { try { const newResponse = await useFetch(url, httpOptions) resolve(newResponse) } catch (error) { reject(error) } } requestQueue.length = 0 } else { for (const { reject } of requestQueue) { reject('Refresh Fail') } requestQueue.length = 0 } debugger const response = await new Promise(async (resolve, reject) => { try { const initialResponse = await myFetch(url, httpOptions) resolve(initialResponse) } catch (error) { reject(error) } }) return response } else { const promise = new Promise(async (resolve, reject) => { requestQueue.push({ resolve, reject }) }) return promise } } else { console.log(response._data.message); return response._data; } }, } const params = defu(httpOptions, defaults) return await useFetch(url, params) } export const useCustomFetch = { get: async <T>(url: UrlType, params?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'get', params, ...option }) }, post: async <T>(url: UrlType, body?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'post', body, ...option }) }, put: async <T>(url: UrlType, body?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'put', body, ...option }) }, delete: async <T>(url: UrlType, body?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'delete', body, ...option }) }, }
问题分析与修复方案
原代码的核心问题在于请求队列的处理逻辑混乱:直接使用原生useFetch执行队列请求(未携带自定义配置)、冗余的重试逻辑、队列Promise未绑定实际执行逻辑。以下是修复后的代码:
import type { FetchResponse, SearchParameters } from 'ofetch' import type { UseFetchOptions } from '#app' import { useUserStore } from '../stores/userStore' import { refreshToken } from '../apis/auth' import { defu } from 'defu' let isRefreshing = false // 改为存储待执行的请求函数,逻辑更清晰 const requestQueue: Array<() => Promise<any>> = [] export interface ResOptions<T> { data?: T code?: number message?: string } type UrlType = string | Request | Ref<string | Request> | (() => string | Request) export type HttpOption<T> = UseFetchOptions<ResOptions<T>> export const myFetch = async <T>(url: UrlType, httpOptions: HttpOption<T> = {}) => { const userStore = useUserStore() const config = useRuntimeConfig() const defaults: HttpOption<T> = { baseURL: `${config.public.apiBase}/api`, async onRequest({ request, options }) { options.headers = options.headers || {} if (userStore.isLogin) { (options.headers as { [key: string]: string }).authorization = `Bearer ${userStore.token}` } }, async onResponse({ request, response, options }) { if (response._data.code !== 200) { console.log(response._data.message); } }, async onResponseError({ request, response, options }) { if (response._data.code === 401 && response.headers.get('Token-Expired') === 'true') { // 封装当前请求的重试逻辑 const retryRequest = () => myFetch(url, httpOptions) if (!isRefreshing) { isRefreshing = true try { const refreshSuccess = await refreshToken() // 确认refreshToken内部已更新userStore.token,若未更新需手动赋值 // userStore.token = 新获取的token; // 批量执行队列中所有等待的请求 await Promise.all(requestQueue.map(fn => fn())) requestQueue.length = 0 // 重试当前失败的请求 return await retryRequest() } catch (error) { // 刷新失败,清空队列并抛出错误 requestQueue.length = 0 throw new Error('Token刷新失败') } finally { isRefreshing = false } } else { // 当前正在刷新token,将请求加入队列等待 return new Promise((resolve, reject) => { requestQueue.push(async () => { try { const res = await retryRequest() resolve(res) } catch (err) { reject(err) } }) }) } } else { console.log(response._data.message); throw new Error(response._data.message || '请求失败') } }, } const params = defu(httpOptions, defaults) return await useFetch(url, params) } export const useCustomFetch = { get: async <T>(url: UrlType, params?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'get', params, ...option }) }, post: async <T>(url: UrlType, body?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'post', body, ...option }) }, put: async <T>(url: UrlType, body?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'put', body, ...option }) }, delete: async <T>(url: UrlType, body?: any, option?: HttpOption<T>) => { return await myFetch<T>(url, { method: 'delete', body, ...option }) }, }
关键修改点
- 队列结构优化:将队列改为存储请求执行函数,避免直接操作Promise的resolve/reject,逻辑更直观
- 统一请求执行入口:所有重试和队列请求均通过
myFetch执行,确保携带最新token和自定义配置 - 批量处理队列:token刷新成功后,用
Promise.all批量执行队列请求,提高效率 - 错误处理强化:刷新失败时直接抛出错误,避免静默失败
- 移除冗余逻辑:删除原代码中重复的重试Promise包装,统一通过队列管理所有等待请求
内容的提问来源于stack exchange,提问作者Jello
相关产品推荐
相关产品推荐

