jackwener/OpenCLI · error · CommandExecutionError

${label} returned HTTP 429 (rate limited)

Error message

${label} returned HTTP 429 (rate limited)

What it means

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.

Source

Thrown at clis/semanticscholar/utils.js:116

        } catch (err) {
            throw new CommandExecutionError(
                `${label} request failed: ${err?.message ?? err}`,
                'Check that api.semanticscholar.org is reachable from this network.',
            );
        }
        if (resp.status === 429 && attempt === 0 && !apiKey) {
            attempt += 1;
            await new Promise(resolve => setTimeout(resolve, 1500));
            continue;
        }
        break;
    }

    if (resp.status === 404) {
        throw new EmptyResultError(label, `Semantic Scholar returned 404 for ${url}.`);
    }
    if (resp.status === 429) {
        throw new CommandExecutionError(
            `${label} returned HTTP 429 (rate limited)`,
            'Semantic Scholar throttles anonymous traffic; set SEMANTIC_SCHOLAR_API_KEY (free at https://www.semanticscholar.org/product/api) or wait a minute 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}`);
    }
    if (body && typeof body === 'object' && body.error) {
        throw new CommandExecutionError(`${label} returned an error: ${body.error}`);
    }
    return body;
}

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Request a free API key and set it: export SEMANTIC_SCHOLAR_API_KEY=<key> (raised limits).
  2. Wait about a minute for the 5-minute window to reset, then retry.
  3. Throttle your loop: add sleep/delay between calls and cap concurrency to 1-2.
  4. Add exponential backoff with jitter around the CLI in scripts, and cache results to avoid repeat fetches.

Example fix

// before
for (const id of ids) await run(`opencli semanticscholar paper ${id}`);
// after
for (const id of ids) {
    await run(`opencli semanticscholar paper ${id}`);
    await new Promise(r => setTimeout(r, 3500)); // stay under ~100 req/5min
}
Defensive patterns

Strategy: retry

Validate before calling

// client-side throttle: max ~90 requests per 5 minutes
const MIN_INTERVAL_MS = 3400;
let last = 0;
async function throttle() {
    const wait = last + MIN_INTERVAL_MS - Date.now();
    if (wait > 0) await new Promise(r => setTimeout(r, wait));
    last = Date.now();
}

Try / catch

async function withRateLimitRetry(fn, attempts = 4) {
    for (let i = 0; ; i++) {
        try { return await fn(); }
        catch (err) {
            if (!/HTTP 429/.test(String(err.message)) || i >= attempts - 1) throw err;
            await new Promise(r => setTimeout(r, 60000 * (i + 1)));
        }
    }
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Related errors


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