{"record":{"id":"fe84b7c38b802181","repo":"jackwener/OpenCLI","slug":"label-returned-http-429-rate-limited-fe84b7","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/juejin/utils.js","lineNumber":105,"sourceCode":"    let resp;\n    try {\n        const init = {\n            method,\n            headers: { 'user-agent': UA, accept: 'application/json' },\n        };\n        if (method === 'POST') {\n            init.headers['content-type'] = 'application/json';\n            init.body = JSON.stringify(body ?? {});\n        }\n        resp = await fetch(url, init);\n    } catch (err) {\n        throw new CommandExecutionError(\n            `${label} request failed: ${err?.message ?? err}`,\n            'Check that api.juejin.cn is reachable from this network.',\n        );\n    }\n    if (resp.status === 429) {\n        throw new CommandExecutionError(\n            `${label} returned HTTP 429 (rate limited)`,\n            'Juejin throttles bursty 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 payload;\n    try {\n        payload = await resp.json();\n    } catch (err) {\n        throw new CommandExecutionError(`${label} returned malformed JSON: ${err?.message ?? err}`);\n    }\n    if (!payload || typeof payload !== 'object' || Array.isArray(payload) || !Object.hasOwn(payload, 'err_no')) {\n        throw new CommandExecutionError(`${label} returned a malformed API envelope`);\n    }\n    if (payload.err_no !== 0) {\n        throw new CommandExecutionError(`${label} returned err_no ${payload.err_no}: ${payload.err_msg ?? ''}`);","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/juejin/utils.js#L87-L123","documentation":"juejinFetch special-cases HTTP 429 from api.juejin.cn and raises a dedicated CommandExecutionError with rate-limit guidance. Juejin throttles bursty or high-frequency traffic on its public endpoints; the library does not auto-retry, it surfaces the condition so callers can back off.","triggerScenarios":"The server responded with status 429 to a POST made by juejinFetch — i.e. the request succeeded at the transport level but Juejin's rate limiter rejected it. Happens when calling the adapter in a tight loop (pagination sweeps, scripts hitting recommend/hot endpoints repeatedly).","commonSituations":"A CI job or script iterating many pages without delay; multiple users sharing one NAT'd IP hitting Juejin; re-running a failed batch immediately instead of backing off; monitoring scripts polling every few seconds.","solutions":["Wait several seconds (or longer) and retry — Juejin's throttle window is typically short.","Add exponential backoff with jitter around calls that hit 429 instead of failing the whole run.","Reduce request frequency: batch pagination more aggressively (larger limits) and add delays between calls.","Avoid running parallel workers against Juejin from the same IP.","Check whether a shared network/VPN egress IP is causing other tenants' traffic to count against your limit."],"exampleFix":"// before (tight loop, no delay)\nfor (const cursor of cursors) {\n  await juejinFetch('/recommend_api/feed/v1', { cursor }, 'juejin recommend');\n}\n\n// after (sleep between pages)\nconst sleep = ms => new Promise(r => setTimeout(r, ms));\nfor (const cursor of cursors) {\n  await juejinFetch('/recommend_api/feed/v1', { cursor }, 'juejin recommend');\n  await sleep(1500);\n}","handlingStrategy":"retry","validationCode":"null","typeGuard":"function isRateLimited(err) {\n  return err instanceof CommandExecutionError && /HTTP 429 \\(rate limited\\)/.test(err.message);\n}","tryCatchPattern":"const sleep = ms => new Promise(r => setTimeout(r, ms));\nasync function withBackoff(fn, attempts = 4) {\n  for (let i = 0; ; i++) {\n    try { return await fn(); }\n    catch (err) {\n      if (!isRateLimited(err) || i >= attempts - 1) throw err;\n      await sleep(1000 * 2 ** i + Math.random() * 500);\n    }\n  }\n}\nconst payload = await withBackoff(() => juejinFetch(path, body, label));","preventionTips":["Add sleep/delay between paginated calls; never loop juejinFetch back-to-back.","Implement exponential backoff with jitter for any adapter-driven retry loop.","Serialize Juejin calls from one process — avoid parallel workers sharing the same IP budget.","Prefer larger page limits over many small pages to cut total request count."],"tags":["rate-limit","http-429","throttling","retry"],"backgroundTag":"http-429-rate-limited","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}