jackwener/OpenCLI · error · Error

STATE_VERIFY_FAIL

STATE_VERIFY_FAIL

Error message

STATE_VERIFY_FAIL: relation did not flip back to Follow within 5s; unfollow may not have been recorded

What it means

After clicking unfollow (and confirming), the script waits up to 5s for the button to flip from Following back to Follow. If the flip never happens it throws with code STATE_VERIFY_FAIL, meaning the unfollow action likely was not recorded by TikTok. This is a post-action state verification, not a selector problem.

Source

Thrown at clis/tiktok/unfollow.js:77

  target.click();

  // TikTok shows a confirm-unfollow dialog. Wait for it to render.
  const dialogShown = await waitFor(() => buttonExists(CONFIRM_LABELS), { timeoutMs: 3000 });
  if (dialogShown) {
    const confirmBtn = findButtonByText(CONFIRM_LABELS);
    if (!confirmBtn) {
      throw new Error('BUTTON_NOT_FOUND: confirm-unfollow dialog detected but confirm button could not be located');
    }
    confirmBtn.click();
  }

  const flipped = await waitFor(
    () => buttonExists(FOLLOW_LABELS) && !buttonExists(RELATION_LABELS),
    { timeoutMs: 5000 },
  );
  if (!flipped) {
    ensureNoRateLimitOrThrow();
    throw new Error('STATE_VERIFY_FAIL: relation did not flip back to Follow within 5s; unfollow may not have been recorded');
  }

  ensureNoRateLimitOrThrow();

  return [{
    username,
    url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username),
    result: 'unfollowed',
  }];
})()
`;
}

async function unfollowUser(page, args) {
    const username = normalizeUsername(args.username);
    const throwFailure = (error) => throwButtonWalkerError(error, {
        authMessage: 'TikTok requires login to unfollow users',
        failureMessage: `Failed to unfollow @${username}`,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Re-run the unfollow for that user after a delay, then verify the relation state before continuing
  2. Slow down bulk unfollows (add delays between actions) to avoid silent rate-limit rejections
  3. Extend the 5s waitFor timeout if network latency is the cause
  4. Check ensureNoRateLimitOrThrow / any rate-limit notices on the page and back off when signaled

Example fix

// before
await unfollowUser(username);
// after
try {
  await unfollowUser(username);
} catch (e) {
  if (e.message.includes('STATE_VERIFY_FAIL')) {
    await sleep(60000);           // back off, then verify and retry
    await unfollowUser(username);
  } else throw e;
}
Defensive patterns

Strategy: retry

Validate before calling

const before = await getRelationState(username); // 'Follow' | 'Following'
if (before !== 'Following') throw new Error(`${username} is not followed; nothing to unfollow`);

Type guard

function relationIsFollowed(state) {
  return state === 'Following' || state === 'Friends';
}

Try / catch

async function unfollowSafe(username, attempts = 2) {
  for (let i = 0; i < attempts; i++) {
    try {
      return await unfollowUser(username);
    } catch (e) {
      if (!e.message.includes('STATE_VERIFY_FAIL') || i === attempts - 1) throw e;
      await new Promise(r => setTimeout(r, 30000)); // back off before retry
    }
  }
}

Prevention

When it happens

Trigger: The confirm dialog was accepted but the relation button never showed a Follow label within 5s: the click didn't register, TikTok silently rejected the action (rate limit/bot detection), the network request behind the unfollow failed, or the page was stale.

Common situations: Mass-unfollowing triggering server-side throttling that silently drops the action; slow network exceeding the 5s window; session expired so the mutation is rejected client-side; TikTok UI changing the post-action button state text.

Related errors


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