fix: replace AbortSignal.timeout with AbortController+setTimeout for reliable fetch timeouts
undici (Node.js native fetch) silently ignores AbortSignal.timeout when TCP connect is blocked. Using AbortController driven by Node's setTimeout guarantees the abort fires and the hanging request is cancelled within 10s. Adds [spotify] token ok log to confirm when credentials work. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
76271c1e54
commit
d2620519aa
1 file changed
+11
-6
@@ -67,6 +67,14 @@ function parseSpotifyUrl(url) {
|
|||||||
return { type: m[1], id: m[2] };
|
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 _spTokenCache = null;
|
||||||
let _spTokenExpiry = 0;
|
let _spTokenExpiry = 0;
|
||||||
|
|
||||||
@@ -81,14 +89,13 @@ async function fetchSpotifyToken() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const basic = Buffer.from(`${CONFIG.spotifyClientId}:${CONFIG.spotifyClientSecret}`).toString('base64');
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Basic ${basic}`,
|
'Authorization': `Basic ${basic}`,
|
||||||
'Content-Type': 'application/x-www-form-urlencoded',
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
},
|
},
|
||||||
body: 'grant_type=client_credentials',
|
body: 'grant_type=client_credentials',
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
@@ -101,16 +108,14 @@ async function fetchSpotifyToken() {
|
|||||||
|
|
||||||
_spTokenCache = d.access_token;
|
_spTokenCache = d.access_token;
|
||||||
_spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000;
|
_spTokenExpiry = Date.now() + (d.expires_in * 1000) - 30000;
|
||||||
|
console.log('[spotify] token ok');
|
||||||
return _spTokenCache;
|
return _spTokenCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
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++) {
|
for (let attempt = 0; attempt < 4; attempt++) {
|
||||||
const r = await fetch(url, {
|
const r = await _fetchTimeout(url, { headers: { Authorization: `Bearer ${token}` } });
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
signal: AbortSignal.timeout(10000),
|
|
||||||
});
|
|
||||||
if (r.status === 429) {
|
if (r.status === 429) {
|
||||||
const wait = (parseInt(r.headers.get('Retry-After') || '5', 10) + 1) * 1000;
|
const wait = (parseInt(r.headers.get('Retry-After') || '5', 10) + 1) * 1000;
|
||||||
await new Promise(res => setTimeout(res, wait));
|
await new Promise(res => setTimeout(res, wait));
|
||||||
|
|||||||
Reference in new issue
Block a user