jackwener/OpenCLI · error · CommandExecutionError
Could not verify list removal: unexpected lists payload shap
Error message
Could not verify list removal: unexpected lists payload shape
What it means
The verification fetch succeeded, but the JSON payload did not match the expected lists-management schema (getListsManagementInstructions returned falsy). The library refuses to interpret an unrecognized payload shape as success or failure, so it throws instead of guessing.
Source
Thrown at clis/twitter/list-remove-core.js:246
}
if (!uiResult.saveClickX) {
throw new CommandExecutionError('Save button not found in dialog.');
}
const memberCountBefore = Number(targetList.members) || 0;
await page.nativeClick(uiResult.rowClickX, uiResult.rowClickY);
await new Promise((r) => setTimeout(r, 800));
await page.nativeClick(uiResult.saveClickX, uiResult.saveClickY);
await new Promise((r) => setTimeout(r, 3500));
const listsAfter = unwrapBrowserResult(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();
}`));
if (listsAfter && listsAfter.__error) {
throw new CommandExecutionError(`Could not verify list removal: ${listsAfter.__error}`);
}
if (!getListsManagementInstructions(listsAfter)) {
throw new CommandExecutionError('Could not verify list removal: unexpected lists payload shape');
}
const parsedAfter = parseListsManagement(listsAfter, new Set());
const afterList = parsedAfter.find((l) => l.id === listId);
if (!afterList) {
throw new CommandExecutionError(`Could not verify list removal: list ${listId} missing from post-delete payload`);
}
const memberCountAfter = Number(afterList.members) || 0;
if (memberCountAfter < memberCountBefore) {
verifiedBy = `member_count ${memberCountBefore} → ${memberCountAfter}`;
} else {
throw new CommandExecutionError(`Failed to remove @${username} from list ${listId}: member_count unchanged (${memberCountBefore} → ${memberCountAfter}).`);
}
}
return [{
listId,
username,
userId: String(userId),View on GitHub (pinned to 49907e53dc)
Solutions
- Update the library / twitter-openapi dependency to pick up the current GraphQL queryId and parser
- Log in again in the controlled browser — logged-out responses have a different envelope
- Dump the raw response (page.evaluate fetch of the same listsUrl) and confirm whether X changed the schema, then report a bug with the payload
- Retry later if it's intermittent — anti-bot interstitials can substitute payloads temporarily
Example fix
// before
const res = await listRemoveUser(page, { listId, username });
// after
let res;
try { res = await listRemoveUser(page, { listId, username }); }
catch (e) {
if (e.message.includes('unexpected lists payload shape')) {
// treat as unverified, not failed: check membership manually
const lists = await runTwitterCommand('lists', { limit: 100 });
console.warn('removal unverified:', e.message, lists.find(l => l.id === listId));
} else throw e;
} Defensive patterns
Strategy: try-catch
Validate before calling
// sanity-check the read path first
const probe = await run('twitter', 'lists', { limit: 5 });
if (!Array.isArray(probe) || probe.length === 0) throw new Error('lists payload looks wrong; fix session/library before mutating'); Type guard
const hasListsShape = (d) => d && typeof d === 'object' && !Array.isArray(d) && !('error' in d); Try / catch
try {
await listRemoveUser(page, { listId, username });
} catch (e) {
if (String(e.message).includes('unexpected lists payload shape')) {
console.warn('Removal attempted but unverified — check membership manually.');
} else throw e;
} Prevention
- Keep opencli and its twitter-openapi queryId source updated
- Re-login when X changes envelopes; logged-out responses look different
- Avoid proxies/extensions that rewrite x.com responses
- Report persistent shape errors with the raw payload so the parser can be fixed
When it happens
Trigger: The ListsManagementPageTimeline GraphQL response arrived OK (HTTP 200) but lacked the expected instructions/timeline structure — typically because X changed the GraphQL schema, the wrong queryId produced a different endpoint's payload, or the response was a JSON error object with HTTP 200.
Common situations: X ships a schema change while the library's pinned twitter-openapi queryId still routes to a now-incompatible response; a proxy or anti-bot interstitial returned 200 with HTML/JSON that is not the timeline; logged-out state returns a different envelope.
Related errors
- Twitter lists returned an unexpected payload shape
- ${label} returned an unexpected payload shape; expected an a
- AIbase daily page returned an unreadable payload
- Barchart greeks returned an unreadable options payload${data
- Barchart greeks returned an unreadable options payload
AI-assisted analysis of jackwener/OpenCLI@49907e53dc (2026-08-29).
Data as JSON: /api/errors/d508307f8b39b754.
Report an issue: GitHub.