jackwener/OpenCLI · error · CliError

API_ERROR

API_ERROR

Error message

${err?.error?.message || `Spotify API error ${res.status}`}

What it means

api() is the low-level Spotify REST helper; for any non-OK response (other than 204/202) it parses the JSON body and throws a CliError with code API_ERROR carrying err.error.message from Spotify's error envelope, falling back to a generic message with the HTTP status. This surfaces upstream Spotify Web API failures (4xx/5xx) uniformly to callers.

Source

Thrown at clis/spotify/spotify.js:90

        throw new CliError('AUTH_CORRUPTED', 'Token file is corrupted. Run: opencli spotify auth');
    }
    if (Date.now() > tokens.expires_at - 60_000)
        return refreshAccessToken(tokens.refresh_token);
    return tokens.access_token;
}
// ── Spotify API helper ────────────────────────────────────────────────────────
async function api(method, path, body) {
    const token = await getToken();
    const res = await fetch(`https://api.spotify.com/v1${path}`, {
        method,
        headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
        body: body ? JSON.stringify(body) : undefined,
    });
    if (res.status === 204 || res.status === 202)
        return null;
    if (!res.ok) {
        const err = await res.json().catch(() => ({}));
        throw new CliError('API_ERROR', err?.error?.message || `Spotify API error ${res.status}`);
    }
    return res.json();
}
async function findTrackUri(query) {
    const data = await api('GET', `/search?q=${encodeURIComponent(query)}&type=track&limit=1`);
    const track = getFirstSpotifyTrack(data);
    if (!track)
        throw new CliError('EMPTY_RESULT', `No track found for: ${query}`);
    return track;
}
function openBrowser(url) {
    const cmd = process.platform === 'win32' ? `start "" "${url}"` : process.platform === 'darwin' ? `open "${url}"` : `xdg-open "${url}"`;
    exec(cmd);
}
// ── Commands ──────────────────────────────────────────────────────────────────
cli({
    site: 'spotify',
    name: 'auth',

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Read the embedded Spotify error.message to identify the specific cause (403 premium, 404 no device, 429 rate limit).
  2. For 401, re-authenticate with opencli spotify auth to refresh credentials.
  3. For 404, open a Spotify player/device first so playback endpoints have an active device.
  4. For 429, back off and retry later; reduce polling frequency.
  5. For 403, verify the account is Premium and the app has the required scopes.

Example fix

// before: fire and forget
await api('PUT', `/me/player/play`, { uris: [uri] }); // 404 if no device
// after: catch and surface
catch (e) { if (e.code === 'API_ERROR' && /404|device/i.test(e.message)) await transferToActiveDevice(); }
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-checks that avoid common upstream 4xx/403/404:
// Premium required for playback; ensure a device is active before play/pause/volume calls.
const devices = await api('GET', '/me/player/devices');
if (!devices?.devices?.some(d => d.is_active)) await api('PUT', '/me/player', { device_ids: [devices.devices[0].id] });

Try / catch

try {
  return await api('PUT', `/me/player/volume?volume_percent=${level}`);
} catch (e) {
  if (e.code === 'API_ERROR') {
    if (e.message.includes('401')) await reAuth();
    else if (/404|device/i.test(e.message)) console.error('Open a Spotify player first');
    else if (e.message.includes('429')) await backoff();
    else if (/403|premium/i.test(e.message)) console.error('Premium account required');
  }
  throw e;
}

Prevention

When it happens

Trigger: Any authenticated Spotify Web API request returning 400 (bad query/params), 401 (expired/invalid access token that wasn't refreshed), 403 (insufficient scope, e.g. playback control without premium), 404 (no active device), or 429 (rate limited) — the thrown message mirrors Spotify's error.message.

Common situations: Playing a track with a free account (403 PREMIUM_REQUIRED); no active Spotify device open (404); malformed search query (400); hitting rate limits after polling loops (429); expired token slipping through the 60s refresh window.

Related errors


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