jackwener/OpenCLI · error · CommandExecutionError

Chess.com API returned HTTP ${resp.status} for ${url}

Error message

Chess.com API returned HTTP ${resp.status} for ${url}

What it means

A CommandExecutionError thrown when the Chess.com API returns a non-2xx status other than 404 (resp.ok is false). Since the public pub API needs no auth, this typically indicates rate limiting or server-side trouble rather than credentials problems. 404 is intentionally handled separately as an empty result.

Source

Thrown at clis/chess/utils.js:55

            'Expected https://www.chess.com/game/live/<id> or https://www.chess.com/game/daily/<id>.',
        );
    }
    return { kind: m[1].toLowerCase(), id: m[2] };
}

export async function chessApi(path, fetchImpl = fetch) {
    const url = path.startsWith('http') ? path : `${API_BASE}${path}`;
    let resp;
    try {
        resp = await fetchImpl(url, { headers: { 'User-Agent': UA, accept: 'application/json' } });
    } catch (error) {
        throw new CommandExecutionError(`Failed to fetch Chess.com API ${url}: ${error?.message || error}`);
    }
    if (!resp || typeof resp !== 'object') {
        throw new CommandExecutionError(`Chess.com API returned an invalid response object for ${url}`);
    }
    if (resp.status === 404) throw new EmptyResultError(`Chess.com returned 404 for ${url}`);
    if (!resp.ok) throw new CommandExecutionError(`Chess.com API returned HTTP ${resp.status} for ${url}`);
    let payload;
    try {
        payload = await resp.json();
    } catch (error) {
        throw new CommandExecutionError(`Chess.com API returned malformed JSON for ${url}: ${error?.message || error}`);
    }
    if (!isPlainObject(payload)) {
        throw new CommandExecutionError(`Chess.com API returned an unexpected payload shape for ${url}`);
    }
    return payload;
}

/** Pull rating + record fields out of a stats sub-object (`chess_rapid` etc). */
export function summarizeStats(stats, kind) {
    const k = stats?.[kind];
    if (!k) return null;
    if (!isPlainObject(k)) {
        throw new CommandExecutionError(`Chess.com stats payload for ${kind} is not an object`);

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Retry with exponential backoff on 429/5xx and throttle requests (e.g. wait a few seconds between calls).
  2. Cache responses to reduce repeat hits on the same endpoint.
  3. Check https://status.chess.com for ongoing outages.
  4. If persistent 3xx, ensure your fetch follows redirects (default) and the API_BASE path is current.

Example fix

// before
for (const u of users) await chessApi(`/player/${u}/stats`); // 429s
// after
for (const u of users) {
  const stats = await withRetry(() => chessApi(`/player/${u}/stats`), { retries: 3, backoffMs: 1000 });
  await sleep(1500);
}
Defensive patterns

Strategy: retry

Validate before calling

// Throttle pre-emptively; Chess.com throttles unauthenticated pub API calls:
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
await sleep(1500); // between per-player requests in a batch

Type guard

function isHttpError(e) {
  return e instanceof Error && /returned HTTP \d+/.test(e.message);
}

Try / catch

async function withRetry(fn, { retries = 3, backoffMs = 2000 } = {}) {
  for (let i = 0; ; i++) {
    try { return await fn(); }
    catch (e) {
      if (!isHttpError(e) || i >= retries) throw e;
      await new Promise((r) => setTimeout(r, backoffMs * 2 ** i));
    }
  }
}

Prevention

When it happens

Trigger: HTTP 429 when polling /player/<username>/stats or monthly archives too aggressively (Chess.com throttles unauthenticated requests), 5xx during Chess.com outages, 301/3xx if a redirect is not followed by the fetch implementation.

Common situations: Loops over many usernames without delay hitting the rate limit; Chess.com maintenance windows; a custom fetchImpl with redirect: 'manual' turning a moved endpoint into a 3xx failure.

Related errors


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