From 859f4a458ad62588f59526fa00b28b68a3362943 Mon Sep 17 00:00:00 2001 From: Honey Date: Mon, 11 May 2026 00:05:41 +0200 Subject: [PATCH] fix: fail fast on Spotify 429 instead of retrying into timeout Retrying with wait times (23s + 60s + 60s) exceeded the 120s global timeout and showed a generic timeout error. Now returns immediately with a RATE_LIMITED error and shows the user how long to wait. Co-Authored-By: Claude Sonnet 4.6 --- backend/novasonic-server.js | 31 +++++++++++++------------------ src/spotify.js | 12 ++++++++++++ 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 917a67c..f437f2f 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -251,25 +251,16 @@ async function fetchSpotifyToken() { async function _spApi(path, token) { const url = path.startsWith('http') ? path : `https://api.spotify.com/v1${path}`; - for (let attempt = 0; attempt < 4; attempt++) { - const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } }); - if (r.status === 429) { - const retryAfter = parseInt(r.headers.get('Retry-After') || '5', 10); - // Long Retry-After = token is banned — invalidate cache and fail immediately - if (retryAfter > 60) { - _spTokenCache = null; - _spTokenExpiry = 0; - throw new Error(`Spotify-Token für ${Math.round(retryAfter / 3600)}h gesperrt (Rate Limit). Token-Cache geleert — nächster Versuch holt neuen Token.`); - } - const wait = (retryAfter + 1) * 1000; - console.log(`[spotify] 429 waiting ${retryAfter + 1}s`); - await new Promise(res => setTimeout(res, wait)); - continue; - } - if (!r.ok) throw new Error(`Spotify API Fehler ${r.status}: ${path}`); - return r.json(); + const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } }); + if (r.status === 429) { + const retryAfter = parseInt(r.headers.get('Retry-After') || '10', 10); + _spTokenCache = null; + _spTokenExpiry = 0; + console.log(`[spotify] 429 Retry-After=${retryAfter}s — failing fast`); + throw new Error(`RATE_LIMITED:${retryAfter}`); } - throw new Error('Spotify API rate-limited — bitte kurz warten und erneut versuchen'); + if (!r.ok) throw new Error(`Spotify API Fehler ${r.status}: ${path}`); + return r.json(); } async function _spAllPlaylistTracks(id, token) { @@ -423,6 +414,10 @@ app.post('/api/spotify/resolve', async (req, res) => { if (e.message.includes('403') && e.message.includes('/playlists/')) { return res.status(403).json({ error: 'EDITORIAL_PLAYLIST' }); } + if (e.message.startsWith('RATE_LIMITED:')) { + const secs = parseInt(e.message.split(':')[1], 10) || 30; + return res.status(429).json({ error: 'RATE_LIMITED', retryAfter: secs }); + } res.status(500).json({ error: e.message }); } }); diff --git a/src/spotify.js b/src/spotify.js index 6e7e8d6..68ee4ac 100644 --- a/src/spotify.js +++ b/src/spotify.js @@ -68,6 +68,7 @@ export async function resolveSpotifyLink() { if (!res.ok) { const err = await res.json().catch(() => ({})); if (err.error === 'EDITORIAL_PLAYLIST') throw new Error('EDITORIAL_PLAYLIST'); + if (err.error === 'RATE_LIMITED') throw new Error(`RATE_LIMITED:${err.retryAfter || 30}`); throw new Error(err.error || err.message || `HTTP ${res.status}`); } const data = await res.json(); @@ -79,6 +80,17 @@ export async function resolveSpotifyLink() { inner.innerHTML = `
Zeitüberschreitung — der Server hat nicht rechtzeitig geantwortet. Läuft das Backend?
`; return; } + if (e.message.startsWith('RATE_LIMITED:')) { + const secs = parseInt(e.message.split(':')[1], 10) || 30; + const mins = secs >= 60 ? `${Math.ceil(secs / 60)} Min.` : `${secs}s`; + inner.innerHTML = `
+
⏳ Spotify Rate Limit
+
+ Spotify hat zu viele Anfragen erhalten. Bitte in ca. ${mins} erneut versuchen. +
+
`; + return; + } if (e.message === 'EDITORIAL_PLAYLIST') { inner.innerHTML = `
💡 Editorial Playlist erkannt