{"record":{"id":"d508307f8b39b754","repo":"jackwener/OpenCLI","slug":"could-not-verify-list-removal-unexpected-lists-pa","errorCode":null,"errorMessage":"Could not verify list removal: unexpected lists payload shape","messagePattern":"Could not verify list removal: unexpected lists payload shape","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/list-remove-core.js","lineNumber":246,"sourceCode":"            }\n            if (!uiResult.saveClickX) {\n                throw new CommandExecutionError('Save button not found in dialog.');\n            }\n            const memberCountBefore = Number(targetList.members) || 0;\n            await page.nativeClick(uiResult.rowClickX, uiResult.rowClickY);\n            await new Promise((r) => setTimeout(r, 800));\n            await page.nativeClick(uiResult.saveClickX, uiResult.saveClickY);\n            await new Promise((r) => setTimeout(r, 3500));\n            const listsAfter = unwrapBrowserResult(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            if (listsAfter && listsAfter.__error) {\n                throw new CommandExecutionError(`Could not verify list removal: ${listsAfter.__error}`);\n            }\n            if (!getListsManagementInstructions(listsAfter)) {\n                throw new CommandExecutionError('Could not verify list removal: unexpected lists payload shape');\n            }\n            const parsedAfter = parseListsManagement(listsAfter, new Set());\n            const afterList = parsedAfter.find((l) => l.id === listId);\n            if (!afterList) {\n                throw new CommandExecutionError(`Could not verify list removal: list ${listId} missing from post-delete payload`);\n            }\n            const memberCountAfter = Number(afterList.members) || 0;\n            if (memberCountAfter < memberCountBefore) {\n                verifiedBy = `member_count ${memberCountBefore} → ${memberCountAfter}`;\n            } else {\n                throw new CommandExecutionError(`Failed to remove @${username} from list ${listId}: member_count unchanged (${memberCountBefore} → ${memberCountAfter}).`);\n            }\n        }\n\n    return [{\n        listId,\n        username,\n        userId: String(userId),","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/list-remove-core.js#L228-L264","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nconst res = await listRemoveUser(page, { listId, username });\n// after\nlet res;\ntry { res = await listRemoveUser(page, { listId, username }); }\ncatch (e) {\n  if (e.message.includes('unexpected lists payload shape')) {\n    // treat as unverified, not failed: check membership manually\n    const lists = await runTwitterCommand('lists', { limit: 100 });\n    console.warn('removal unverified:', e.message, lists.find(l => l.id === listId));\n  } else throw e;\n}","handlingStrategy":"try-catch","validationCode":"// sanity-check the read path first\nconst probe = await run('twitter', 'lists', { limit: 5 });\nif (!Array.isArray(probe) || probe.length === 0) throw new Error('lists payload looks wrong; fix session/library before mutating');","typeGuard":"const hasListsShape = (d) => d && typeof d === 'object' && !Array.isArray(d) && !('error' in d);","tryCatchPattern":"try {\n  await listRemoveUser(page, { listId, username });\n} catch (e) {\n  if (String(e.message).includes('unexpected lists payload shape')) {\n    console.warn('Removal attempted but unverified — check membership manually.');\n  } else throw e;\n}","preventionTips":["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"],"tags":["schema-change","payload-shape","twitter-api"],"backgroundTag":"unexpected-payload-shape","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}