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 <noreply@anthropic.com>
This commit is contained in:
1 parent
fbb00baa11
commit
859f4a458a
2 files changed
+19
-12
No files matched your search
@@ -251,25 +251,16 @@ async function fetchSpotifyToken() {
|
|||||||
|
|
||||||
async function _spApi(path, token) {
|
async function _spApi(path, token) {
|
||||||
const url = path.startsWith('http') ? path : `https://api.spotify.com/v1${path}`;
|
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}` } });
|
const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } });
|
||||||
if (r.status === 429) {
|
if (r.status === 429) {
|
||||||
const retryAfter = parseInt(r.headers.get('Retry-After') || '5', 10);
|
const retryAfter = parseInt(r.headers.get('Retry-After') || '10', 10);
|
||||||
// Long Retry-After = token is banned — invalidate cache and fail immediately
|
|
||||||
if (retryAfter > 60) {
|
|
||||||
_spTokenCache = null;
|
_spTokenCache = null;
|
||||||
_spTokenExpiry = 0;
|
_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.`);
|
console.log(`[spotify] 429 Retry-After=${retryAfter}s — failing fast`);
|
||||||
}
|
throw new Error(`RATE_LIMITED:${retryAfter}`);
|
||||||
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}`);
|
if (!r.ok) throw new Error(`Spotify API Fehler ${r.status}: ${path}`);
|
||||||
return r.json();
|
return r.json();
|
||||||
}
|
|
||||||
throw new Error('Spotify API rate-limited — bitte kurz warten und erneut versuchen');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _spAllPlaylistTracks(id, token) {
|
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/')) {
|
if (e.message.includes('403') && e.message.includes('/playlists/')) {
|
||||||
return res.status(403).json({ error: 'EDITORIAL_PLAYLIST' });
|
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 });
|
res.status(500).json({ error: e.message });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ export async function resolveSpotifyLink() {
|
|||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({}));
|
const err = await res.json().catch(() => ({}));
|
||||||
if (err.error === 'EDITORIAL_PLAYLIST') throw new Error('EDITORIAL_PLAYLIST');
|
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}`);
|
throw new Error(err.error || err.message || `HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@@ -79,6 +80,17 @@ export async function resolveSpotifyLink() {
|
|||||||
inner.innerHTML = `<div style="color:var(--danger);padding:16px;background:var(--surface);border-radius:10px">Zeitüberschreitung — der Server hat nicht rechtzeitig geantwortet. Läuft das Backend?</div>`;
|
inner.innerHTML = `<div style="color:var(--danger);padding:16px;background:var(--surface);border-radius:10px">Zeitüberschreitung — der Server hat nicht rechtzeitig geantwortet. Läuft das Backend?</div>`;
|
||||||
return;
|
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 = `<div style="color:var(--text);padding:16px;background:var(--surface);border-radius:10px;border-left:3px solid var(--warning,#f59e0b)">
|
||||||
|
<div style="font-weight:600;margin-bottom:6px">⏳ Spotify Rate Limit</div>
|
||||||
|
<div style="font-size:13px;line-height:1.5;color:var(--text2)">
|
||||||
|
Spotify hat zu viele Anfragen erhalten. Bitte in ca. <strong>${mins}</strong> erneut versuchen.
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (e.message === 'EDITORIAL_PLAYLIST') {
|
if (e.message === 'EDITORIAL_PLAYLIST') {
|
||||||
inner.innerHTML = `<div style="padding:16px;background:var(--surface);border-radius:10px;border-left:3px solid #1DB954">
|
inner.innerHTML = `<div style="padding:16px;background:var(--surface);border-radius:10px;border-left:3px solid #1DB954">
|
||||||
<div style="font-weight:600;margin-bottom:10px;color:#1DB954">💡 Editorial Playlist erkannt</div>
|
<div style="font-weight:600;margin-bottom:10px;color:#1DB954">💡 Editorial Playlist erkannt</div>
|
||||||
|
|||||||
Reference in new issue
Block a user