{"record":{"id":"8aa7625033beb38d","repo":"jackwener/OpenCLI","slug":"spa-navigation-to-twitter-followers-failed","errorCode":null,"errorMessage":"SPA navigation to Twitter followers failed","messagePattern":"SPA navigation to Twitter followers failed","errorType":"exception","errorClass":"CommandExecutionError","httpStatus":null,"severity":"error","filePath":"clis/twitter/followers.js","lineNumber":146,"sourceCode":"            throw new ArgumentError('twitter followers user cannot be empty', 'Example: opencli twitter followers @elonmusk --limit 100');\n        }\n\n        await page.installInterceptor('/Followers?');\n        const targetPath = `/${targetUser}/followers`;\n        await page.evaluate(`() => {\n            const targetPath = ${JSON.stringify(targetPath)};\n            window.history.pushState({}, '', targetPath);\n            window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));\n        }`);\n        try {\n            await page.waitForCapture(CAPTURE_TIMEOUT_SECONDS);\n        }\n        catch {\n            throw new TimeoutError('twitter followers API capture', CAPTURE_TIMEOUT_SECONDS, 'No Followers response was observed after opening the followers list.');\n        }\n        const currentPath = unwrapBrowserResult(await page.evaluate('() => window.location.pathname'));\n        if (typeof currentPath !== 'string' || !currentPath.toLowerCase().endsWith('/followers')) {\n            throw new CommandExecutionError('SPA navigation to Twitter followers failed');\n        }\n\n        const allFollowers = [];\n        const seen = new Set();\n        let cursor = null;\n        let lastRawResponse = null;\n        let pages = 0;\n\n        const consumeCaptured = async () => {\n            const requests = await page.getInterceptedRequests();\n            if (!Array.isArray(requests)) {\n                throw new CommandExecutionError('Twitter followers interceptor returned malformed responses');\n            }\n            for (const request of requests) {\n                const { data, users, nextCursor } = parseFollowers(request);\n                const graphqlError = twitterGraphqlError(data);\n                if (graphqlError)\n                    throw new CommandExecutionError(graphqlError);","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/twitter/followers.js#L128-L164","documentation":"After capturing the first Followers response, the command verifies via window.location.pathname that the SPA actually ended on a path ending in '/followers'. If the path is missing, non-string, or a different route, the client-side pushState/popstate navigation to the followers list failed. This is a CommandExecutionError guarding against extracting follower data from the wrong page.","triggerScenarios":"The popstate-triggered navigation was redirected (e.g. to /login or /home), X intercepted the route change and routed elsewhere, the browser bridge returned a non-string from page.evaluate, or the history API push was blocked.","commonSituations":"Auth wall redirecting to /login; X routing experimental layouts to different paths; a service worker or client router canceling the navigation; browser bridge unwrapping issues producing non-string pathname.","solutions":["Re-authenticate if the URL shows /login after navigation","Re-run the command — transient SPA failures often clear","Check window.location.pathname manually in the same profile to see where navigation lands","Update the navigation logic if X changed route structure"],"exampleFix":"// before\nawait page.evaluate(`() => { window.history.pushState({}, '', targetPath); window.dispatchEvent(new PopStateEvent('popstate', { state: {} })); }`);\n// after (direct navigation fallback)\nif (!currentPath.endsWith('/followers')) await page.goto('https://x.com/' + targetUser + '/followers');","handlingStrategy":"retry","validationCode":"null","typeGuard":"const pathIsFollowers = (p) => typeof p === 'string' && p.toLowerCase().endsWith('/followers');","tryCatchPattern":"try {\n  const rows = await opencli.twitter.followers(user, { limit });\n} catch (err) {\n  if (err.message.includes('SPA navigation')) {\n    // fall back to direct navigation variant or retry once after re-auth\n    await ensureXLogin();\n    return opencli.twitter.followers(user, { limit });\n  } else throw err;\n}","preventionTips":["Ensure an active session so the route is not redirected to /login","Retry once on transient SPA failures","Verify the landing URL manually when debugging a specific account","Keep browser bridge/unwrap logic consistent with library version"],"tags":["twitter","spa","navigation","scraping"],"backgroundTag":"spa-navigation-failed","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}