diff --git a/backend/novasonic-server.js b/backend/novasonic-server.js index c7322b2..7b11403 100644 --- a/backend/novasonic-server.js +++ b/backend/novasonic-server.js @@ -67,12 +67,36 @@ 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)); +// undici (native fetch) cannot abort a pending TCP connect even with AbortController. +// Use Node's https module with socket.setTimeout() — fires at OS level during connect. +function _fetchTimeout(url, opts = {}, ms = 10000) { + return new Promise((resolve, reject) => { + const u = new URL(url); + const req = https.request({ + hostname: u.hostname, port: 443, + path: u.pathname + u.search, + method: opts.method || 'GET', + headers: opts.headers || {}, + }, res => { + const chunks = []; + res.on('data', c => chunks.push(c)); + res.on('end', () => { + const body = Buffer.concat(chunks).toString(); + resolve({ + ok: res.statusCode >= 200 && res.statusCode < 300, + status: res.statusCode, + headers: { get: h => res.headers[h.toLowerCase()] ?? null }, + json: () => { try { return Promise.resolve(JSON.parse(body)); } catch(e) { return Promise.reject(e); } }, + text: () => Promise.resolve(body), + }); + }); + res.on('error', reject); + }); + req.on('socket', s => s.setTimeout(ms, () => req.destroy(new Error(`Spotify nicht erreichbar (Timeout ${ms}ms) — api.spotify.com blockiert?`)))); + req.on('error', reject); + if (opts.body) req.write(opts.body); + req.end(); + }); } let _spTokenCache = null;