jackwener/OpenCLI · warning · CommandExecutionError

${label} returned HTTP 429 (rate limited)

Error message

${label} returned HTTP 429 (rate limited)

What it means

cratesFetch throws this CommandExecutionError when crates.io answers HTTP 429, meaning the request was rate limited. crates.io deliberately throttles unauthenticated clients, so the library surfaces 429 as its own case with a 'wait and retry' hint instead of the generic non-ok branch.

Source

Thrown at clis/crates/utils.js:56

}

export async function cratesFetch(url, label) {
    let resp;
    try {
        // crates.io requires a descriptive User-Agent per https://crates.io/data-access
        resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });
    }
    catch (err) {
        throw new CommandExecutionError(
            `${label} request failed: ${err?.message ?? err}`,
            'Check that crates.io is reachable from this network.',
        );
    }
    if (resp.status === 404) {
        throw new EmptyResultError(label, `crates.io returned 404 for ${url}.`);
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(
            `${label} returned HTTP 429 (rate limited)`,
            'crates.io rate-limits unauthenticated traffic; wait a few seconds and retry.',
        );
    }
    if (!resp.ok) {
        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);
    }
    let body;
    try {
        body = await resp.json();
    }
    catch (err) {
        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);
    }
    return body;
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Wait a few seconds and retry the command (crates.io suggests ≥1 req/sec, ideally 1 req/10s for bulk).
  2. Add throttling/backoff in your script: sleep between requests and retry on 429 with exponential backoff.
  3. Batch your lookups: query only the crates you need instead of enumerating.
  4. If behind a shared proxy/CI runner, retry later or run from a different network.

Example fix

// before
for (const name of names) { await fetchCrate(name); } // bursts -> 429
// after
for (const name of names) {
  await fetchCrate(name);
  await new Promise(r => setTimeout(r, 2000));
}
Defensive patterns

Strategy: retry

Try / catch

async function fetchWithBackoff(fn, retries = 4) {
  for (let i = 0; i < retries; i++) {
    try { return await fn(); }
    catch (err) {
      if (!/429/.test(err.message) || i === retries - 1) throw err;
      await new Promise(r => setTimeout(r, 2000 * 2 ** i));
    }
  }
}

Prevention

When it happens

Trigger: Calling cratesFetch (via any crates subcommand) more than ~1 request/second from an unauthenticated client, or running many queries back-to-back in a script from one IP.

Common situations: Batch scripts iterating dozens of crates; CI jobs making parallel crates.io calls; shared NAT/proxy IP already throttled; polling loops with no delay.

Understand the failure class

Related errors


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