{"record":{"id":"239510ab0b800b6a","repo":"jackwener/OpenCLI","slug":"label-returned-http-429-rate-limited-239510","errorCode":null,"errorMessage":"${label} returned HTTP 429 (rate limited)","messagePattern":"(.+?) returned HTTP 429 \\(rate limited\\)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":429,"severity":"warning","filePath":"clis/openalex/utils.js","lineNumber":96,"sourceCode":"    );\n}\n\nexport async function openalexFetch(url, label) {\n    let resp;\n    try {\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 api.openalex.org is reachable from this network.',\n        );\n    }\n    if (resp.status === 404) {\n        throw new EmptyResultError(label, `OpenAlex returned 404 for ${url}.`);\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(\n            `${label} returned HTTP 429 (rate limited)`,\n            'OpenAlex throttles unauthenticated traffic; wait a few seconds and retry, or set OPENALEX_MAILTO.',\n        );\n    }\n    if (!resp.ok) {\n        let detail = '';\n        try {\n            const text = await resp.text();\n            const match = text.match(/\"message\"\\s*:\\s*\"([^\"]+)\"/);\n            if (match) detail = ` (${match[1]})`;\n        }\n        catch { /* ignore */ }\n        throw new CommandExecutionError(`${label} returned HTTP ${resp.status}${detail}`);\n    }\n    let body;\n    try {\n        body = await resp.json();\n    }","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/openalex/utils.js#L78-L114","documentation":"openalexFetch detects HTTP 429 from api.openalex.org and throws CommandExecutionError indicating the request was rate limited, with a hint to wait and retry or set OPENALEX_MAILTO. OpenAlex throttles unauthenticated traffic to its common pool; providing a mailto= parameter moves requests into the faster 'polite pool'.","triggerScenarios":"Issuing many openalex requests in a short window without mailto, exceeding OpenAlex's unauthenticated rate limit (roughly 10 req/s), causing the API to respond 429.","commonSituations":"Bulk scripts looping over hundreds of IDs with no delay; CI jobs hammering the API in parallel; shared-IP environments (offices, CI runners) exhausting the collective unauthenticated quota.","solutions":["Set OPENALEX_MAILTO (or append ?mailto=you@example.com) to join the polite pool","Add throttling: sleep ~150-200ms between requests, or use a concurrency limit of 1-5","Back off and retry on 429 honoring any Retry-After header","Batch queries where possible (e.g. filter=ids:W1|W2|… in one request) to cut request count"],"exampleFix":"// before\nfor (const id of ids) await work(id); // bursts -> 429\n// after\nawait Promise.all(ids.map(id =>\n  withBackoff(() => work(id))\n));\nasync function withBackoff(fn, tries = 5) {\n  try { return await fn(); }\n  catch (e) {\n    if (tries && String(e).includes('429')) {\n      await new Promise(r => setTimeout(r, 2 ** (5 - tries) * 500));\n      return withBackoff(fn, tries - 1);\n    }\n    throw e;\n  }\n}","handlingStrategy":"retry","validationCode":"// ensure mailto is configured before bulk runs\nif (!process.env.OPENALEX_MAILTO) {\n  console.warn('Set OPENALEX_MAILTO to join the OpenAlex polite pool and avoid 429s');\n}","typeGuard":null,"tryCatchPattern":"async function fetchWithBackoff(fn, maxTries = 5) {\n  for (let i = 0; i < maxTries; i++) {\n    try { return await fn(); }\n    catch (e) {\n      const is429 = e.name === 'CommandExecutionError' && e.message.includes('429');\n      if (!is429 || i === maxTries - 1) throw e;\n      await new Promise(r => setTimeout(r, 2 ** i * 500));\n    }\n  }\n}","preventionTips":["Set OPENALEX_MAILTO to opt into the polite pool","Throttle to ~5-10 requests/sec max in bulk scripts","Honor Retry-After headers when retrying","Batch related lookups into single filter=… requests to reduce call count"],"tags":["openalex","rate-limit","http-429"],"backgroundTag":"rate-limit-exceeded","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}