{"record":{"id":"0b7da042855b6eeb","repo":"jackwener/OpenCLI","slug":"label-returned-http-429-rate-limited-0b7da0","errorCode":null,"errorMessage":"${label} returned HTTP 429 (rate limited)","messagePattern":"(.+?) returned HTTP 429 \\(rate limited\\)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":429,"severity":"error","filePath":"clis/semanticscholar/utils.js","lineNumber":116,"sourceCode":"        } catch (err) {\n            throw new CommandExecutionError(\n                `${label} request failed: ${err?.message ?? err}`,\n                'Check that api.semanticscholar.org is reachable from this network.',\n            );\n        }\n        if (resp.status === 429 && attempt === 0 && !apiKey) {\n            attempt += 1;\n            await new Promise(resolve => setTimeout(resolve, 1500));\n            continue;\n        }\n        break;\n    }\n\n    if (resp.status === 404) {\n        throw new EmptyResultError(label, `Semantic Scholar returned 404 for ${url}.`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(\n            `${label} returned HTTP 429 (rate limited)`,\n            'Semantic Scholar throttles anonymous traffic; set SEMANTIC_SCHOLAR_API_KEY (free at https://www.semanticscholar.org/product/api) or wait a minute 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    } catch (err) {\n        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);\n    }\n    if (body && typeof body === 'object' && body.error) {\n        throw new CommandExecutionError(`${label} returned an error: ${body.error}`);\n    }\n    return body;\n}","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/semanticscholar/utils.js#L98-L134","documentation":"s2Fetch retries once automatically on HTTP 429 (only for anonymous, key-less traffic, after a 1.5s pause); if the second response is still 429 — or the first when an API key is set — it throws CommandExecutionError '... returned HTTP 429 (rate limited)'. Semantic Scholar caps anonymous traffic at ~100 requests/5 minutes, so this signals the client is exceeding the rate limit.","triggerScenarios":"Looping over many `paper`/`citations` calls without delay and exhausting the anonymous quota; parallel workers hammering the API; a shared IP (CI runner, university NAT) already throttled; requests made with an over-quota or invalid SEMANTIC_SCHOLAR_API_KEY (keyed requests are not retried).","commonSituations":"Batch scripts fetching hundreds of papers; CI pipelines running concurrently on shared egress IPs; expired/exceeded API key tier; retry loops elsewhere re-triggering the limit.","solutions":["Request a free API key and set it: export SEMANTIC_SCHOLAR_API_KEY=<key> (raised limits).","Wait about a minute for the 5-minute window to reset, then retry.","Throttle your loop: add sleep/delay between calls and cap concurrency to 1-2.","Add exponential backoff with jitter around the CLI in scripts, and cache results to avoid repeat fetches."],"exampleFix":"// before\nfor (const id of ids) await run(`opencli semanticscholar paper ${id}`);\n// after\nfor (const id of ids) {\n    await run(`opencli semanticscholar paper ${id}`);\n    await new Promise(r => setTimeout(r, 3500)); // stay under ~100 req/5min\n}","handlingStrategy":"retry","validationCode":"// client-side throttle: max ~90 requests per 5 minutes\nconst MIN_INTERVAL_MS = 3400;\nlet last = 0;\nasync function throttle() {\n    const wait = last + MIN_INTERVAL_MS - Date.now();\n    if (wait > 0) await new Promise(r => setTimeout(r, wait));\n    last = Date.now();\n}","typeGuard":null,"tryCatchPattern":"async function withRateLimitRetry(fn, attempts = 4) {\n    for (let i = 0; ; i++) {\n        try { return await fn(); }\n        catch (err) {\n            if (!/HTTP 429/.test(String(err.message)) || i >= attempts - 1) throw err;\n            await new Promise(r => setTimeout(r, 60000 * (i + 1)));\n        }\n    }\n}","preventionTips":["Set a free SEMANTIC_SCHOLAR_API_KEY to lift the anonymous ~100 req/5min cap.","Serialize batch requests with a >=3.4s delay and avoid parallel workers on one IP.","Cache paper lookups locally so reruns do not refetch the same ids.","Add exponential backoff with jitter around 429s; stop hammering once throttled."],"tags":["rate-limit","http-429","api"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}