{"record":{"id":"d54f8713b50d2873","repo":"jackwener/OpenCLI","slug":"following","errorCode":null,"errorMessage":"Following","messagePattern":"Following","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/following.js","lineNumber":223,"sourceCode":"            throw new CommandExecutionError(`Could not find user @${targetUser}`);\n\n        const allUsers = [];\n        const seen = new Set();\n        let cursor = null;\n        let lastRawResponse = null;\n\n        // Runaway guard only; --limit and cursor exhaustion control normal pagination.\n        for (let i = 0; i < MAX_PAGINATION_PAGES && allUsers.length < limit; i++) {\n            const fetchCount = Math.min(50, limit - allUsers.length + 10);\n            const apiUrl = buildFollowingUrl(followingQueryId, userId, fetchCount, cursor);\n            const data = unwrapBrowserResult(await page.evaluate(async (url, headers) => {\n                const r = await fetch(url, { headers, credentials: 'include' });\n                return r.ok ? await r.json() : { error: r.status };\n            }, apiUrl, headers));\n            if (data?.error) {\n                if (data.error === 401 || data.error === 403)\n                    throw new AuthRequiredError('x.com', `Twitter following request failed (HTTP ${data.error})`);\n                throw new CommandExecutionError(describeTwitterApiError('Following', data.error));\n            }\n            lastRawResponse = data;\n            const { users, nextCursor } = parseFollowing(data);\n            for (const u of users) {\n                if (!seen.has(u.screen_name)) {\n                    seen.add(u.screen_name);\n                    allUsers.push(u);\n                }\n            }\n            if (!nextCursor || nextCursor === cursor)\n                break;\n            cursor = nextCursor;\n        }\n\n        if (allUsers.length === 0) {\n            if (looksLikePrivateTwitterTimeline(lastRawResponse)) {\n                throw new EmptyResultError('twitter following', `No following data returned for @${targetUser} (the target account may have set their following list to private)`);\n            }","sourceCodeStart":205,"sourceCodeEnd":241,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/following.js#L205-L241","documentation":"CommandExecutionError with the message built by describeTwitterApiError('Following', status). Thrown when the Following GraphQL request fails with a status other than 401/403; the helper turns the raw numeric status into a human-readable explanation for the Following endpoint.","triggerScenarios":"The in-page fetch of the Following API URL returns e.g. 400, 404, 429, or 5xx; data.error is truthy and not 401/403, so describeTwitterApiError('Following', data.error) is thrown, typically during pagination.","commonSituations":"Rate limiting (429) after aggressive pagination; stale FOLLOWING_QUERY_ID producing 400 Bad Request; Twitter 5xx outage; account restrictions returning 404 for the following tab.","solutions":["Read the described status: 429 means slow down — wait before retrying","Update opencli so resolveTwitterQueryId fetches the current Following query ID","Retry later for 5xx (Twitter-side incident)","Lower --limit and add delays between runs to stay under rate limits"],"exampleFix":"// before: hammering the API -> 429\nError: Twitter Following request failed: HTTP 429\n// after: wait, then limit results\n$ sleep 300 && opencli twitter following @elonmusk --limit 50","handlingStrategy":"retry","validationCode":"// Throttle calls up front to avoid 429s\nconst sleep = ms => new Promise(r => setTimeout(r, ms));\nfor (const u of targets) {\n  await cli.following(u, { limit: 100 });\n  await sleep(15000); // stay under Twitter rate limits\n}","typeGuard":"function isRetryableApiError(err) {\n  const code = (/(\\d{3})/.exec(String(err && err.message)) || [])[1];\n  return code === '429' || (code && code.startsWith('5'));\n}","tryCatchPattern":"try {\n  await cli.following(target);\n} catch (err) {\n  if (isRetryableApiError(err)) {\n    const code = (/(\\d{3})/.exec(err.message))[1];\n    await sleep(code === '429' ? 300000 : 5000);\n    return cli.following(target);\n  }\n  throw err;\n}","preventionTips":["Add delays between commands to avoid 429 rate limits","Update opencli regularly — stale query IDs cause 400s","Watch Twitter status for 5xx incidents before bulk runs","Keep --limit modest; smaller pages are less likely to trip limits"],"tags":["http","rate-limit","twitter-api"],"backgroundTag":"http-request-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}