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

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 })
    },
}

关键修改点

  1. 队列结构优化:将队列改为存储请求执行函数,避免直接操作Promise的resolve/reject,逻辑更直观
  2. 统一请求执行入口:所有重试和队列请求均通过myFetch执行,确保携带最新token和自定义配置
  3. 批量处理队列:token刷新成功后,用Promise.all批量执行队列请求,提高效率
  4. 错误处理强化:刷新失败时直接抛出错误,避免静默失败
  5. 移除冗余逻辑:删除原代码中重复的重试Promise包装,统一通过队列管理所有等待请求

内容的提问来源于stack exchange,提问作者Jello

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 09:42:47