{"record":{"id":"af8681823d873989","repo":"jackwener/OpenCLI","slug":"state-verify-fail-follow-button-did-not-flip-to-f","errorCode":null,"errorMessage":"STATE_VERIFY_FAIL: follow button did not flip to Following within 5s; relation may not have been recorded","messagePattern":"STATE_VERIFY_FAIL: follow button did not flip to Following within 5s; relation may not have been recorded","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"clis/tiktok/follow.js","lineNumber":64,"sourceCode":"    return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-friends' }];\n  }\n  if (buttonExists(FOLLOWING_LABELS)) {\n    return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-following' }];\n  }\n\n  const followBtn = findButtonByText(FOLLOW_LABELS);\n  if (!followBtn) {\n    throw new Error('BUTTON_NOT_FOUND: Follow button not on profile page (logged out, private account, or selectors changed)');\n  }\n\n  const target = followBtn.closest('button') || followBtn.closest('[role=\"button\"]') || followBtn;\n  target.click();\n\n  // State verification: button text should flip to Following / 已关注 etc.\n  const flipped = await waitFor(() => buttonExists(ALREADY_LABELS), { timeoutMs: 5000 });\n  if (!flipped) {\n    ensureNoRateLimitOrThrow();\n    throw new Error('STATE_VERIFY_FAIL: follow button did not flip to Following within 5s; relation may not have been recorded');\n  }\n\n  // Re-check rate limit AFTER click — TikTok sometimes flashes captcha\n  // mid-flight even when the button text appears to flip.\n  ensureNoRateLimitOrThrow();\n\n  return [{\n    username,\n    url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username),\n    result: 'followed',\n  }];\n})()\n`;\n}\n\nasync function followUser(page, args) {\n    const username = normalizeUsername(args.username);\n    const throwFailure = (error) => throwButtonWalkerError(error, {","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/jackwener/OpenCLI/blob/49907e53dc3ade5c223ff0c4c2c2785687cec4e6/clis/tiktok/follow.js#L46-L82","documentation":"The follow command clicks the profile page Follow button and then verifies the button text actually flipped to 'Following'/互关 variants within 5 seconds. If it did not, the library cannot confirm the follow relation was recorded, so it throws this typed error instead of silently reporting success. It is thrown inside the injected page script after target.click(), after a pre-click rate-limit check passes.","triggerScenarios":"Clicking the Follow button on tiktok.com/@<username> when the click did not take effect: TikTok rendered a captcha or rate-limit interstitial that the check missed, the DOM re-rendered and replaced the button mid-flight, a slow network delayed the optimistic UI flip beyond the 5s waitFor timeout, or the button was for a private/restricted account that does not flip.","commonSituations":"Automating follows in quick succession hits TikTok's action rate limiting; running from a datacenter IP flagged for bot traffic; logged-in session with weakened permissions; localized UI showing a label not in FOLLOWING_LABELS (non en/zh/ja locales).","solutions":["Retry the command — TikTok dedupes follow, so a retry is safe (the error is marked retryable in RETRYABLE_HINTS.relationFailure).","Wait and back off: pause several minutes before retrying to clear TikTok's rate limit / captcha window.","Verify manually in a browser that the account was followed despite the UI not flipping.","Use a warmer, residential session/cookies and reduce follow frequency.","Check the target profile's locale; if the button uses an unsupported language label, extend the label list in follow.js."],"exampleFix":"// before: single attempt\nconst rows = await followUser(page, { username: 'someuser' });\n// after: retry with backoff on state-verify failure\nfor (let i = 0; i < 3; i++) {\n  try { rows = await followUser(page, { username: 'someuser' }); break; }\n  catch (e) {\n    if (!/STATE_VERIFY_FAIL/.test(e.message)) throw e;\n    await new Promise(r => setTimeout(r, 5000 * (i + 1)));\n  }\n}","handlingStrategy":"retry","validationCode":"// before following, ensure a fresh logged-in session and sane pacing\nif (!(await isLoggedIn(page))) throw new Error('login required before follow');\nif (Date.now() - lastFollowAt < 15000) throw new Error('throttle: wait between follows');","typeGuard":"function isStateVerifyFail(e) {\n  return e instanceof Error && e.message.includes('STATE_VERIFY_FAIL');\n}","tryCatchPattern":"try {\n  rows = await followUser(page, { username });\n} catch (e) {\n  if (isStateVerifyFail(e)) {\n    await sleep(30_000);            // back off for captcha/rate window\n    rows = await followUser(page, { username }); // safe: TikTok dedupes\n  } else throw e;\n}","preventionTips":["Space out write actions (follows) by seconds-to-minutes to avoid rate limiting","Use a warmed, logged-in residential session rather than a fresh datacenter profile","Retry on STATE_VERIFY_FAIL — follows are idempotent on TikTok","Check the rendered page for captcha elements before clicking","Keep the Following/Friends label list current for your account locale"],"tags":["tiktok","browser-automation","rate-limit","state-verification"],"backgroundTag":"ui-state-verification-timeout","analyzedSha":"49907e53dc3ade5c223ff0c4c2c2785687cec4e6","analyzedAt":"2026-08-29T08:14:47.543Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}