fix: 25s backend timeout + 30s frontend timeout on Spotify resolve

Extracts resolve logic into _resolveSpotify() and wraps it in
Promise.race with a 25s timer so the backend always sends a response
even if undici's AbortSignal.timeout silently fails on blocked sockets.
Also reduces frontend timeout from 90s to 30s.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
HoneyandClaude Sonnet 4.6 committed 2026-05-09 23:39:07 +02:00
1 parent 7d38fdf601
commit 76271c1e54
2 files changed
+40 -46

No files matched your search

+39 -45
View File
@@ -223,57 +223,51 @@ app.get('/api/health', (req, res) => {
});
});
async function _resolveSpotify(type, id) {
const token = await fetchSpotifyToken();
if (type === 'track') {
const t = await _spApi(`/tracks/${id}`, token);
const artist = _spArtistName(t.artists);
const artUrl = _spArtwork(t);
return { type: 'track', name: t.name || '', artist, artworkUrl: artUrl,
tracks: [_spTrackItem(t, t.album?.name, artUrl, artist)] };
}
if (type === 'album') {
const album = await _spApi(`/albums/${id}`, token);
const artist = _spArtistName(album.artists);
const artUrl = _spArtwork(album);
const trackItems = await _spAllAlbumTracks(id, token);
return { type: 'album', name: album.name || '', artist, artworkUrl: artUrl,
tracks: trackItems.map(t => _spTrackItem(t, album.name, artUrl, artist)) };
}
if (type === 'playlist') {
const pl = await _spApi(`/playlists/${id}?fields=name,description,owner,images`, token);
const owner = pl.owner?.display_name || '';
const artUrl = _spArtwork(pl);
const { tracks: trackItems, skippedLocal } = await _spAllPlaylistTracks(id, token);
return { type: 'playlist', name: pl.name || '', artist: owner, artworkUrl: artUrl,
description: pl.description || '', skippedLocal,
tracks: trackItems.map(t => _spTrackItem(t, null, null, null)) };
}
throw new Error(`Unbekannter Typ: ${type}`);
}
app.post('/api/spotify/resolve', async (req, res) => {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'url required' });
try {
const { type, id } = parseSpotifyUrl(url);
const token = await fetchSpotifyToken();
console.log(`[spotify/resolve] ${type} ${id}`);
if (type === 'track') {
const t = await _spApi(`/tracks/${id}`, token);
const artist = _spArtistName(t.artists);
const artUrl = _spArtwork(t);
return res.json({
type: 'track',
name: t.name || '',
artist,
artworkUrl: artUrl,
tracks: [_spTrackItem(t, t.album?.name, artUrl, artist)],
});
}
if (type === 'album') {
const album = await _spApi(`/albums/${id}`, token);
const artist = _spArtistName(album.artists);
const artUrl = _spArtwork(album);
const trackItems = await _spAllAlbumTracks(id, token);
return res.json({
type: 'album',
name: album.name || '',
artist,
artworkUrl: artUrl,
tracks: trackItems.map(t => _spTrackItem(t, album.name, artUrl, artist)),
});
}
if (type === 'playlist') {
const pl = await _spApi(`/playlists/${id}?fields=name,description,owner,images`, token);
const owner = pl.owner?.display_name || '';
const artUrl = _spArtwork(pl);
const { tracks: trackItems, skippedLocal } = await _spAllPlaylistTracks(id, token);
return res.json({
type: 'playlist',
name: pl.name || '',
artist: owner,
artworkUrl: artUrl,
description: pl.description || '',
skippedLocal,
tracks: trackItems.map(t => _spTrackItem(t, null, null, null)),
});
}
res.status(400).json({ error: `Unbekannter Typ: ${type}` });
const timeout = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Zeitüberschreitung — Spotify API antwortet nicht (>25s)')), 25000)
);
const data = await Promise.race([_resolveSpotify(type, id), timeout]);
res.json(data);
} catch(e) {
console.error('[spotify/resolve]', e.message);
if (e.message.includes('403') && e.message.includes('/playlists/')) {
+1 -1
View File
@@ -63,7 +63,7 @@ export async function resolveSpotifyLink() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }),
signal: AbortSignal.timeout(90000),
signal: AbortSignal.timeout(30000),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));