jackwener/OpenCLI · error · CliError

REFRESH_FAILED

REFRESH_FAILED

Error message

${err?.error_description || `Token refresh failed (${res.status})`}

What it means

refreshAccessToken exchanges the stored Spotify refresh token for a new access token using the client-credentials Basic auth header. When the token endpoint responds with a non-OK status, the CLI reads error_description from the JSON body (falling back to a generic message with the HTTP status) and throws a CliError with code REFRESH_FAILED. This means the stored credentials can no longer produce a valid access token.

Source

Thrown at clis/spotify/spotify.js:56

        return null;
    }
}
function saveTokens(tokens) {
    mkdirSync(join(homedir(), '.opencli'), { recursive: true });
    writeFileSync(TOKEN_FILE, JSON.stringify(tokens, null, 2));
}
async function refreshAccessToken(refreshToken) {
    const res = await fetch('https://accounts.spotify.com/api/token', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            Authorization: 'Basic ' + Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64'),
        },
        body: new URLSearchParams({ grant_type: 'refresh_token', refresh_token: refreshToken }),
    });
    if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new CliError('REFRESH_FAILED', err?.error_description || `Token refresh failed (${res.status})`);
    }
    const data = await res.json();
    const tokens = {
        access_token: data.access_token,
        refresh_token: data.refresh_token || refreshToken,
        expires_at: Date.now() + data.expires_in * 1000,
    };
    saveTokens(tokens);
    return tokens.access_token;
}
async function getToken() {
    const tokens = loadTokens();
    if (!tokens)
        throw new CliError('AUTH_REQUIRED', 'Not authenticated. Run: opencli spotify auth');
    if (!tokens.access_token || !tokens.refresh_token || !(tokens.expires_at > 0)) {
        throw new CliError('AUTH_CORRUPTED', 'Token file is corrupted. Run: opencli spotify auth');
    }
    if (Date.now() > tokens.expires_at - 60_000)

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Re-authenticate to obtain fresh tokens: run opencli spotify auth (deletes/overwrites the stale token file).
  2. Verify CLIENT_ID and CLIENT_SECRET match the Spotify app that originally issued the refresh token.
  3. Check the error_description in the message for invalid_grant vs invalid_client to decide which fix applies.
  4. Check Spotify status / retry if the status is 5xx (transient server error).

Example fix

// before: keep retrying with stale token
const tokens = await getToken(); // keeps failing with REFRESH_FAILED
// after: detect code and re-auth
catch (e) { if (e.code === 'REFRESH_FAILED') { await runAuthFlow(); } }
Defensive patterns

Strategy: try-catch

Validate before calling

const tokens = loadTokens();
if (!tokens?.refresh_token) { console.error('No refresh token stored; run: opencli spotify auth'); }
if (!process.env.CLIENT_ID || !process.env.CLIENT_SECRET) { console.error('CLIENT_ID/CLIENT_SECRET not set'); }

Try / catch

try {
  const token = await getToken();
} catch (e) {
  if (e.code === 'REFRESH_FAILED') {
    if (/invalid_client/i.test(e.message)) console.error('Check CLIENT_ID/CLIENT_SECRET');
    else if (/invalid_grant/i.test(e.message)) await runAuthFlow(); // token revoked/rotated
    else if (e.message.includes('5')) retryWithBackoff();
  } else throw e;
}

Prevention

When it happens

Trigger: POST to Spotify's token endpoint with grant_type=refresh_token returns 400/401 — typically invalid_grant (refresh token revoked or expired), invalid_client (wrong CLIENT_ID/CLIENT_SECRET), or a 5xx from Spotify.

Common situations: User revoked the app in their Spotify account; refresh token rotated and the old one stored on disk is stale; CLIENT_ID/CLIENT_SECRET env vars don't match the app that issued the token; clock or network issues; Spotify outage.

Related errors


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/4582abde67a6e722. Report an issue: GitHub.