diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index 6f1a13e..c7322b2 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -67,6 +67,14 @@ function parseSpotifyUrl(url) { return { type: m[1], id: m[2] }; } +// AbortSignal.timeout() is unreliable in Node/undici when TCP connect is blocked. +// Use AbortController + setTimeout instead — always fires from Node's event loop. +function _fetchTimeout(url, opts, ms = 10000) { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), ms); + return fetch(url, { ...opts, signal: ctrl.signal }).finally(() => clearTimeout(timer)); +} + let _spTokenCache = null; let _spTokenExpiry = 0; @@ -81,14 +89,13 @@ async function fetchSpotifyToken() { } const basic = Buffer.from(`${CONFIG.spotifyClientId}:${CONFIG.spotifyClientSecret}`).toString('base64'); - const r = await fetch('https://accounts.spotify.com/api/token', { + const r = await _fetchTimeout('https://accounts.spotify.com/api/token', { method: 'POST', headers: { 'Authorization': `Basic ${basic}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'grant_type=client_credentials', - signal: AbortSignal.timeout(10000), }); if (!r.ok) { @@ -101,16 +108,14 @@ async function fetchSpotifyToken() { _spTokenCache = d.access_token; _spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000; + console.log('[spotify] token ok'); return _spTokenCache; } 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 fetch(url, { - headers: { Authorization: `Bearer ${token}` }, - signal: AbortSignal.timeout(10000), - }); + const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } }); if (r.status === 429) { const wait = (parseInt(r.headers.get('Retry-After') || '5', 10) + 1) * 1000; await new Promise(res => setTimeout(res, wait));