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

  1. Check the HTTP status in the message: 429 → wait and retry with a larger batch interval; 401/403 → re-login to x.com.
  2. Update the LISTS_MANAGEMENT_QUERY_ID fallback if the live resolveTwitterQueryId lookup is failing and X rotated the queryId.
  3. Retry later if the status is 5xx (X-side outage).
  4. 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

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


AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29). Data as JSON: /api/errors/bf7853559ea4059a. Report an issue: GitHub.