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

为什么我用于请求Spotify API的useEffect会返回429错误?

问题根因

  1. 无依赖数组的useEffect死循环:useEffect不传入依赖数组时,每次组件渲染完成都会重复执行,而每个useEffect内部的setState操作会触发组件重新渲染,形成无限调用链路,短时间内发起大量请求触发Spotify接口的429限流。
  2. 加依赖后无token报错:
  • 组件挂载时token还未完成初始化(从localStorage读取、写入SpotifyApi实例是异步的),直接发起请求时api实例还没有绑定有效accessToken
  • Context中定义的refreshableCall没有用useCallback包裹,每次Provider重渲染都会生成新的函数实例,如果将其加入useEffect依赖会触发不必要的重复执行,同时没有在请求前判断token是否已准备完成。

修复方案

第一步:修复SpotifyContext.js的逻辑

  1. 用useCallback包裹refreshableCall和refreshAccessToken,稳定函数引用
  2. 添加刷新锁避免多个并发请求重复触发token刷新
    修改后的核心代码:
// SpotifyContext.js 内部Provider部分
import { useRef, useCallback } from 'react'

export function SpotifyAuthContextProvider({ children }) {
    const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
    const [isRefreshing, setIsRefreshing] = useState(false)
    const pendingRequests = useRef([])
    const hasToken = tokenInfo !== null

    useEffect(() => {
        if (tokenInfo === null) return;
        spotifyApi.setCredentials({
            accessToken: tokenInfo.accessToken,
            refreshToken: tokenInfo.refreshToken,
        })
        setStoredJSON('myApp:spotify', tokenInfo)
    }, [tokenInfo])

    // 用useCallback包裹refreshAccessToken
    const refreshAccessToken = useCallback(async () => {
        if (isRefreshing) {
            // 已经在刷新中,把请求加入队列等待
            return new Promise(resolve => {
                pendingRequests.current.push(resolve)
            })
        }
        setIsRefreshing(true)
        try {
            const refreshToken = tokenInfo.refreshToken;
            const res = await axios.post("http://localhost:3001/refresh", { refreshToken })
            const refreshedTokenInfo = {
                accessToken: res.data.accessToken,
                refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
                expiresAt: Date.now() + (res.data.expiresIn * 1000)
            }
            setTokenInfo(refreshedTokenInfo)
            spotifyApi.setCredentials({
                accessToken: refreshedTokenInfo.accessToken,
                refreshToken: refreshedTokenInfo.refreshToken,
            })
            // 执行所有等待中的请求
            pendingRequests.current.forEach(resolve => resolve(refreshedTokenInfo))
            pendingRequests.current = []
            return refreshedTokenInfo
        } finally {
            setIsRefreshing(false)
        }
    }, [tokenInfo, isRefreshing])

    // 用useCallback包裹refreshableCall
    const refreshableCall = useCallback(async (callApiFunc) => {
        if (!tokenInfo) throw new Error("No token available")
        if (Date.now() > tokenInfo.expiresAt)
            await refreshAccessToken();

        try {
            return await callApiFunc()
        } catch (err) {
            if (err.name !== "WebapiAuthenticationError")
                throw err;
        }
        return refreshAccessToken().then(callApiFunc)
    }, [tokenInfo, refreshAccessToken])

    // 其余exchangeCode逻辑不变
    const exchangeCode = useCallback((code) => {
        return axios
            .post("http://localhost:3001/login", { code })
            .then(res => {
                const { accessToken, refreshToken, expiresIn } = res.data;
                setTokenInfo({
                    accessToken,
                    refreshToken,
                    expiresAt: Date.now() + (expiresIn * 1000)
                });
            })
    }, [])

    return (
        <SpotifyAuthContext.Provider value={{
            api: spotifyApi,
            exchangeCode,
            hasToken,
            refreshableCall,
            refreshAccessToken
        }}>
            {children}
        </SpotifyAuthContext.Provider>
    )
}

第二步:修复Profile.js的useEffect逻辑

  1. 给useEffect添加依赖数组,并且只在hasToken为true时发起请求
  2. 合并多个请求到同一个useEffect减少冗余代码
    修改后的核心代码:
// Profile.js
const { api, refreshableCall, hasToken } = useSpotify()
const [error, setError] = useState(null)
const [userName, setUserName] = useState("")
const [userFollowers, setUserFollowers] = useState("")
const [userImage, setUserImage] = useState([])
const [userLink, setUserLink] = useState("")
const [userId, setUserId] = useState("")
const [userFollowing, setUserFollowing] = useState("")
const [userTopArtists, setUserTopArtists] = useState([])
const [userTopSongs, setUserTopSongs] = useState([])

useEffect(() => {
    if (!hasToken) return // token未准备好直接返回
    let disposed = false
    // 并行发起所有请求
    Promise.all([
        refreshableCall(() => api.getMyTopTracks({ limit: 10, time_range: "long_term" })),
        refreshableCall(() => api.getMe()),
        refreshableCall(() => api.getFollowedArtists()),
        refreshableCall(() => api.getMyTopArtists({ limit: 10, time_range: "long_term" }))
    ]).then(([topTracksRes, meRes, followedRes, topArtistsRes]) => {
        if (disposed) return
        setUserTopSongs(topTracksRes.body.items)
        const meData = meRes.body
        setUserName(meData.display_name)
        setUserImage(meData.images)
        setUserFollowers(meData.followers["total"])
        setUserLink(meData.external_urls.spotify)
        setUserId(meData.id)
        setUserFollowing(followedRes.body.artists.total)
        setUserTopArtists(topArtistsRes.body.items)
        setError(null)
    }).catch(err => {
        if (disposed) return
        setError(err)
        // 重置状态
        setUserTopSongs([])
        setUserName("")
        setUserImage([])
        setUserFollowers("")
        setUserLink("")
        setUserId("")
        setUserFollowing([])
        setUserTopArtists([])
    })
    return () => disposed = true
}, [refreshableCall, hasToken]) // 只在refreshableCall或者hasToken变化时执行

额外优化建议

如果后续还遇到429错误,可以给Spotify请求添加简单的限流逻辑,或者对重复的请求做缓存,避免相同接口重复调用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.06 23:00:03