jackwener/OpenCLI · error · CommandExecutionError
Could not fetch lists: ${listsData.__error}
Error message
Could not fetch lists: ${listsData.__error} What it means
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.
Source
Thrown at clis/twitter/list-add-core.js:159
throw new CommandExecutionError(`Could not resolve user @${username}`);
}
// ListsManagementPageTimeline — used for list existence check + before/after member_count.
const listsQueryId = await resolveTwitterQueryId(page, 'ListsManagementPageTimeline', LISTS_MANAGEMENT_QUERY_ID);
const listsUrl = `/i/api/graphql/${listsQueryId}/ListsManagementPageTimeline?features=${encodeURIComponent(JSON.stringify(LISTS_MANAGEMENT_FEATURES))}`;
const listsDataRaw = await page.evaluate(`async () => {
const r = await fetch(${JSON.stringify(listsUrl)}, { headers: ${headers}, credentials: 'include' });
if (!r.ok) return { __error: 'HTTP ' + r.status };
return await r.json();
}`);
// Don't unwrap listsData: opencli spreads GraphQL response to top-level + adds session;
// parseListsManagement reads `.data.viewer.*` from this shape directly.
const listsData = listsDataRaw;
const parsedLists = listsData && !listsData.__error
? parseListsManagement(listsData, new Set())
: [];
if (listsData && listsData.__error) {
throw new CommandExecutionError(`Could not fetch lists: ${listsData.__error}`);
}
const targetList = parsedLists.find((l) => l.id === listId);
if (!targetList) {
throw new CommandExecutionError(`List ${listId} not found among your lists (${parsedLists.length} lists fetched).`);
}
// Direct GraphQL ListAddMember mutation.
//
// Previously this command opened the X profile, clicked "…" → "Add/remove from Lists",
// navigated the dialog and used nativeClick on the Save button. In 2026-05 X replaced
// the dialog with a full-page route (/i/lists/add_member), breaking that UI flow.
//
// The mutation is the same one the UI fires under the hood; calling it directly is
// both more reliable and ~10x faster (no goto-profile + scroll-dialog roundtrip).
const memberCountBefore = Number(targetList.members) || 0;
const listAddMemberQueryId = await resolveTwitterQueryId(page, 'ListAddMember', LIST_ADD_MEMBER_QUERY_ID);
const addUrl = `/i/api/graphql/${listAddMemberQueryId}/ListAddMember`;
const addBody = JSON.stringify({View on GitHub (pinned to 49907e53dc)
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.
Example fix
// before opencli twitter list-batch-add --interval 0 ... CommandExecutionError: Could not fetch lists: HTTP 429 // after opencli twitter list-batch-add --interval 30 ...
Defensive patterns
Strategy: retry
Validate before calling
// Preflight: confirm session + that the lists endpoint responds
const cookies = await page.getCookies({ url: 'https://x.com' });
if (!cookies.some((c) => c.name === 'ct0')) throw new Error('Not logged into x.com'); Type guard
function listsFetchFailed(listsData) {
return Boolean(listsData && typeof listsData === 'object' && listsData.__error);
} Try / catch
try {
await listAddUser(page, { listId, username });
} catch (e) {
const m = /Could not fetch lists: HTTP (\d+)/.exec(e.message);
if (m) {
const status = Number(m[1]);
if (status === 429) return retryLater(/* backoff minutes */ 5);
if (status === 401 || status === 403) return reloginAndRetry();
if (status >= 500) return retryLater(/* X outage */ 15);
}
throw e;
} Prevention
- 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).
When it happens
Trigger: 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'.
Common situations: 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.
Related errors
- LessWrong API returned HTTP ${resp.status}
- Failed to add @${username} to list ${listId}: HTTP ${addResu
- HTTP ${result.httpStatus} from CreateList: ${snippet}
- ${probe.detail}
- 12306 queryByTrainNo returned HTTP ${resp.status}
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/bf7853559ea4059a.
Report an issue: GitHub.