jackwener/OpenCLI · error · Error

BUTTON_NOT_FOUND

BUTTON_NOT_FOUND

Error message

BUTTON_NOT_FOUND: neither Follow nor Following button found (page not rendered, blocked, or selectors changed)

What it means

The unfollow script builds a browser script that locates the Follow/Following relation button by text (RELATION_LABELS). If no such element exists it throws Error with code BUTTON_NOT_FOUND, refusing to silently succeed on a page that never rendered the profile action row. This guards against unfollowing based on a broken or blocked page.

Source

Thrown at clis/tiktok/unfollow.js:55

  ensureLoggedInOrThrow();
  ensureNoRateLimitOrThrow();

  const FOLLOW_LABELS = ['Follow', '关注', 'フォロー'];
  const FOLLOWING_LABELS = ['Following', '已关注', 'フォロー中'];
  const FRIENDS_LABELS = ['Friends', '互关', 'フレンド'];
  const RELATION_LABELS = FOLLOWING_LABELS.concat(FRIENDS_LABELS);
  const CONFIRM_LABELS = ['Unfollow', '取消关注'];

  // Idempotent fast path: not currently following.
  if (!buttonExists(RELATION_LABELS) && buttonExists(FOLLOW_LABELS)) {
    return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-not-following' }];
  }

  const relationBtn = findButtonByText(RELATION_LABELS);
  if (!relationBtn) {
    // Neither Follow nor Following — page may not have rendered, or
    // private account / blocked: refuse to silently succeed.
    throw new Error('BUTTON_NOT_FOUND: neither Follow nor Following button found (page not rendered, blocked, or selectors changed)');
  }

  const target = relationBtn.closest('button') || relationBtn.closest('[role="button"]') || relationBtn;
  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 },

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Ensure a logged-in session and that the profile page loads fully (check screenshot/HTML) before unfollowing
  2. Verify the target username's profile renders a Follow/Following button in a normal browser
  3. Update RELATION_LABELS / findButtonByText to match current button text and locale if TikTok changed the UI
  4. Retry with a real-browser fingerprint (less headless detection) if a challenge page is being served
Defensive patterns

Strategy: validation

Validate before calling

await page.goto(`https://www.tiktok.com/@${username}`, { waitUntil: 'networkidle' });
const hasRelationBtn = await page.evaluate(() => document.body.innerText.includes('Follow') || document.body.innerText.includes('Following'));
if (!hasRelationBtn) throw new Error(`Profile page for ${username} did not render a relation button`);

Type guard

function relationButtonExists(doc, labels) {
  return labels.some(l => Array.from(doc.querySelectorAll('button, [role="button"]')).some(el => el.textContent.trim().toLowerCase().includes(l.toLowerCase())));
}

Try / catch

try {
  await unfollowUser(username);
} catch (e) {
  if (e.message.includes('BUTTON_NOT_FOUND')) {
    console.error(`Cannot unfollow ${username}: page not rendered, blocked, or selectors changed`);
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: buildUnfollowScript's findButtonByText(RELATION_LABELS) returns null: profile page didn't render (bot wall/captcha), navigation timed out, account is private or blocked you, or TikTok changed the button text/attributes so labels no longer match.

Common situations: Not logged in so profile shows a different layout; headless browser detected and served a challenge; profile of a suspended user; RELATION_LABELS text (e.g. localized 'Follow') no longer matching after a TikTok UI or locale change.

Related errors


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