{"record":{"id":"b8b8646f65054f8d","repo":"jackwener/OpenCLI","slug":"label-returned-http-429-rate-limited-b8b864","errorCode":null,"errorMessage":"${label} returned HTTP 429 (rate limited)","messagePattern":"(.+?) returned HTTP 429 \\(rate limited\\)","errorType":"http","errorClass":"CommandExecutionError","httpStatus":429,"severity":"warning","filePath":"clis/crates/utils.js","lineNumber":56,"sourceCode":"}\n\nexport async function cratesFetch(url, label) {\n    let resp;\n    try {\n        // crates.io requires a descriptive User-Agent per https://crates.io/data-access\n        resp = await fetch(url, { headers: { 'user-agent': UA, accept: 'application/json' } });\n    }\n    catch (err) {\n        throw new CommandExecutionError(\n            `${label} request failed: ${err?.message ?? err}`,\n            'Check that crates.io is reachable from this network.',\n        );\n    }\n    if (resp.status === 404) {\n        throw new EmptyResultError(label, `crates.io returned 404 for ${url}.`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(\n            `${label} returned HTTP 429 (rate limited)`,\n            'crates.io rate-limits unauthenticated traffic; wait a few seconds and retry.',\n        );\n    }\n    if (!resp.ok) {\n        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}`);\n    }\n    let body;\n    try {\n        body = await resp.json();\n    }\n    catch (err) {\n        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);\n    }\n    return body;\n}\n","sourceCodeStart":38,"sourceCodeEnd":73,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/crates/utils.js#L38-L73","documentation":"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.","triggerScenarios":"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.","commonSituations":"Batch scripts iterating dozens of crates; CI jobs making parallel crates.io calls; shared NAT/proxy IP already throttled; polling loops with no delay.","solutions":["Wait a few seconds and retry the command (crates.io suggests ≥1 req/sec, ideally 1 req/10s for bulk).","Add throttling/backoff in your script: sleep between requests and retry on 429 with exponential backoff.","Batch your lookups: query only the crates you need instead of enumerating.","If behind a shared proxy/CI runner, retry later or run from a different network."],"exampleFix":"// before\nfor (const name of names) { await fetchCrate(name); } // bursts -> 429\n// after\nfor (const name of names) {\n  await fetchCrate(name);\n  await new Promise(r => setTimeout(r, 2000));\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"async function fetchWithBackoff(fn, retries = 4) {\n  for (let i = 0; i < retries; i++) {\n    try { return await fn(); }\n    catch (err) {\n      if (!/429/.test(err.message) || i === retries - 1) throw err;\n      await new Promise(r => setTimeout(r, 2000 * 2 ** i));\n    }\n  }\n}","preventionTips":["Keep crates.io traffic to <= ~1 request/second (ideally slower for bulk).","Add sleeps between iterations in batch scripts and CI jobs.","Avoid running parallel crates.io queries from one IP.","Cache results you've already fetched instead of re-querying."],"tags":["rate-limit","http-429","crates-io","retry"],"backgroundTag":"http-429-rate-limited","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}