{"record":{"id":"bf7853559ea4059a","repo":"jackwener/OpenCLI","slug":"could-not-fetch-lists-listsdata-error","errorCode":null,"errorMessage":"Could not fetch lists: ${listsData.__error}","messagePattern":"Could not fetch lists: (.+?)","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/list-add-core.js","lineNumber":159,"sourceCode":"            throw new CommandExecutionError(`Could not resolve user @${username}`);\n        }\n\n        // ListsManagementPageTimeline — used for list existence check + before/after member_count.\n        const listsQueryId = await resolveTwitterQueryId(page, 'ListsManagementPageTimeline', LISTS_MANAGEMENT_QUERY_ID);\n        const listsUrl = `/i/api/graphql/${listsQueryId}/ListsManagementPageTimeline?features=${encodeURIComponent(JSON.stringify(LISTS_MANAGEMENT_FEATURES))}`;\n        const listsDataRaw = await page.evaluate(`async () => {\n            const r = await fetch(${JSON.stringify(listsUrl)}, { headers: ${headers}, credentials: 'include' });\n            if (!r.ok) return { __error: 'HTTP ' + r.status };\n            return await r.json();\n        }`);\n        // Don't unwrap listsData: opencli spreads GraphQL response to top-level + adds session;\n        // parseListsManagement reads `.data.viewer.*` from this shape directly.\n        const listsData = listsDataRaw;\n        const parsedLists = listsData && !listsData.__error\n            ? parseListsManagement(listsData, new Set())\n            : [];\n        if (listsData && listsData.__error) {\n            throw new CommandExecutionError(`Could not fetch lists: ${listsData.__error}`);\n        }\n        const targetList = parsedLists.find((l) => l.id === listId);\n        if (!targetList) {\n            throw new CommandExecutionError(`List ${listId} not found among your lists (${parsedLists.length} lists fetched).`);\n        }\n\n        // Direct GraphQL ListAddMember mutation.\n        //\n        // Previously this command opened the X profile, clicked \"…\" → \"Add/remove from Lists\",\n        // navigated the dialog and used nativeClick on the Save button. In 2026-05 X replaced\n        // the dialog with a full-page route (/i/lists/add_member), breaking that UI flow.\n        //\n        // The mutation is the same one the UI fires under the hood; calling it directly is\n        // both more reliable and ~10x faster (no goto-profile + scroll-dialog roundtrip).\n        const memberCountBefore = Number(targetList.members) || 0;\n        const listAddMemberQueryId = await resolveTwitterQueryId(page, 'ListAddMember', LIST_ADD_MEMBER_QUERY_ID);\n        const addUrl = `/i/api/graphql/${listAddMemberQueryId}/ListAddMember`;\n        const addBody = JSON.stringify({","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/list-add-core.js#L141-L177","documentation":"listAddUser fetches your lists via the ListsManagementPageTimeline GraphQL endpoint inside the browser page. When that fetch returns a non-ok HTTP status the evaluate returns {__error:'HTTP <status>'}; the command turns that into CommandExecutionError('Could not fetch lists: ...') because it cannot verify the list exists or read the before member_count.","triggerScenarios":"The in-page fetch of /i/api/graphql/<id>/ListsManagementPageTimeline returns a non-2xx status (401/403 auth, 429 rate limit, 5xx from X), producing listsData.__error like 'HTTP 400' or 'HTTP 429'.","commonSituations":"X rotated the ListsManagementPageTimeline queryId (stale fallback constant → HTTP 400); session expired mid-run; hitting rate limits while batch-processing; X incident/5xx; LISTS_MANAGEMENT_FEATURES payload no longer accepted by the API.","solutions":["Check the HTTP status in the message: 429 → wait and retry with a larger batch interval; 401/403 → re-login to x.com.","Update the LISTS_MANAGEMENT_QUERY_ID fallback if the live resolveTwitterQueryId lookup is failing and X rotated the queryId.","Retry later if the status is 5xx (X-side outage).","Verify the features payload still matches what the X web client sends if you get persistent 400s."],"exampleFix":"// before\nopencli twitter list-batch-add --interval 0 ...\nCommandExecutionError: Could not fetch lists: HTTP 429\n// after\nopencli twitter list-batch-add --interval 30 ...","handlingStrategy":"retry","validationCode":"// Preflight: confirm session + that the lists endpoint responds\nconst cookies = await page.getCookies({ url: 'https://x.com' });\nif (!cookies.some((c) => c.name === 'ct0')) throw new Error('Not logged into x.com');","typeGuard":"function listsFetchFailed(listsData) {\n  return Boolean(listsData && typeof listsData === 'object' && listsData.__error);\n}","tryCatchPattern":"try {\n  await listAddUser(page, { listId, username });\n} catch (e) {\n  const m = /Could not fetch lists: HTTP (\\d+)/.exec(e.message);\n  if (m) {\n    const status = Number(m[1]);\n    if (status === 429) return retryLater(/* backoff minutes */ 5);\n    if (status === 401 || status === 403) return reloginAndRetry();\n    if (status >= 500) return retryLater(/* X outage */ 15);\n  }\n  throw e;\n}","preventionTips":["Set a generous batch interval (e.g. --interval 30) to avoid 429 rate limits.","Confirm login before long batch jobs; sessions can expire mid-run.","Watch X API status for 5xx outages before large runs.","Update queryId fallbacks when X rotates GraphQL query ids (persistent HTTP 400)."],"tags":["twitter","graphql","http","network"],"backgroundTag":"graphql-http-error","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}