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
+24 -30

No files matched your search

+23 -29
View File
@@ -223,24 +223,15 @@ app.get('/api/health', (req, res) => {
}); });
}); });
app.post('/api/spotify/resolve', async (req, res) => { async function _resolveSpotify(type, id) {
const { url } = req.body;
if (!url) return res.status(400).json({ error: 'url required' });
try {
const { type, id } = parseSpotifyUrl(url);
const token = await fetchSpotifyToken(); const token = await fetchSpotifyToken();
if (type === 'track') { if (type === 'track') {
const t = await _spApi(`/tracks/${id}`, token); const t = await _spApi(`/tracks/${id}`, token);
const artist = _spArtistName(t.artists); const artist = _spArtistName(t.artists);
const artUrl = _spArtwork(t); const artUrl = _spArtwork(t);
return res.json({ return { type: 'track', name: t.name || '', artist, artworkUrl: artUrl,
type: 'track', tracks: [_spTrackItem(t, t.album?.name, artUrl, artist)] };
name: t.name || '',
artist,
artworkUrl: artUrl,
tracks: [_spTrackItem(t, t.album?.name, artUrl, artist)],
});
} }
if (type === 'album') { if (type === 'album') {
@@ -248,13 +239,8 @@ app.post('/api/spotify/resolve', async (req, res) => {
const artist = _spArtistName(album.artists); const artist = _spArtistName(album.artists);
const artUrl = _spArtwork(album); const artUrl = _spArtwork(album);
const trackItems = await _spAllAlbumTracks(id, token); const trackItems = await _spAllAlbumTracks(id, token);
return res.json({ return { type: 'album', name: album.name || '', artist, artworkUrl: artUrl,
type: 'album', tracks: trackItems.map(t => _spTrackItem(t, album.name, artUrl, artist)) };
name: album.name || '',
artist,
artworkUrl: artUrl,
tracks: trackItems.map(t => _spTrackItem(t, album.name, artUrl, artist)),
});
} }
if (type === 'playlist') { if (type === 'playlist') {
@@ -262,18 +248,26 @@ app.post('/api/spotify/resolve', async (req, res) => {
const owner = pl.owner?.display_name || ''; const owner = pl.owner?.display_name || '';
const artUrl = _spArtwork(pl); const artUrl = _spArtwork(pl);
const { tracks: trackItems, skippedLocal } = await _spAllPlaylistTracks(id, token); const { tracks: trackItems, skippedLocal } = await _spAllPlaylistTracks(id, token);
return res.json({ return { type: 'playlist', name: pl.name || '', artist: owner, artworkUrl: artUrl,
type: 'playlist', description: pl.description || '', skippedLocal,
name: pl.name || '', tracks: trackItems.map(t => _spTrackItem(t, null, null, null)) };
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}` }); 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);
console.log(`[spotify/resolve] ${type} ${id}`);
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) { } catch(e) {
console.error('[spotify/resolve]', e.message); console.error('[spotify/resolve]', e.message);
if (e.message.includes('403') && e.message.includes('/playlists/')) { if (e.message.includes('403') && e.message.includes('/playlists/')) {
+1 -1
View File
@@ -63,7 +63,7 @@ export async function resolveSpotifyLink() {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url }), body: JSON.stringify({ url }),
signal: AbortSignal.timeout(90000), signal: AbortSignal.timeout(30000),
}); });
if (!res.ok) { if (!res.ok) {
const err = await res.json().catch(() => ({})); const err = await res.json().catch(() => ({}));