jackwener/OpenCLI · error · Error

BUTTON_NOT_FOUND: Follow button not on profile page (logged

Error message

BUTTON_NOT_FOUND: Follow button not on profile page (logged out, private account, or selectors changed)

What it means

The injected follow script finds the Follow button by text label, but neither a Follow label nor a Following label was present on the profile page. The library throws a plain Error prefixed BUTTON_NOT_FOUND, listing the plausible causes: logged out, private account, or changed selectors.

Source

Thrown at clis/tiktok/follow.js:54

  ensureLoggedInOrThrow();
  ensureNoRateLimitOrThrow();

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

  // Idempotent fast path: already in target state.
  if (buttonExists(FRIENDS_LABELS)) {
    return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-friends' }];
  }
  if (buttonExists(FOLLOWING_LABELS)) {
    return [{ username, url: ${JSON.stringify(TIKTOK_HOST)} + '/@' + encodeURIComponent(username), result: 'already-following' }];
  }

  const followBtn = findButtonByText(FOLLOW_LABELS);
  if (!followBtn) {
    throw new Error('BUTTON_NOT_FOUND: Follow button not on profile page (logged out, private account, or selectors changed)');
  }

  const target = followBtn.closest('button') || followBtn.closest('[role="button"]') || followBtn;
  target.click();

  // State verification: button text should flip to Following / 已关注 etc.
  const flipped = await waitFor(() => buttonExists(ALREADY_LABELS), { timeoutMs: 5000 });
  if (!flipped) {
    ensureNoRateLimitOrThrow();
    throw new Error('STATE_VERIFY_FAIL: follow button did not flip to Following within 5s; relation may not have been recorded');
  }

  // Re-check rate limit AFTER click — TikTok sometimes flashes captcha
  // mid-flight even when the button text appears to flip.
  ensureNoRateLimitOrThrow();

  return [{
    username,

View on GitHub (pinned to 49907e53dc)

Solutions

  1. Verify the Chrome profile is logged in to TikTok before running the follow command
  2. Open the target profile manually to confirm it's public and shows a Follow button
  3. Check locale — if the UI is in a language whose label isn't in FOLLOW_LABELS, force en or add the label
  4. Update the library if TikTok changed the button text/DOM structure
  5. Catch the error and inspect a page screenshot to see what the profile actually rendered

Example fix

// before
await followUser(page, { username: 'target' });
// after
try {
  await followUser(page, { username: 'target' });
} catch (e) {
  if (/BUTTON_NOT_FOUND/.test(e.message)) {
    await ensureTikTokLogin(page); // re-auth then retry
    await followUser(page, { username: 'target' });
  } else throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const btn = await page.evaluate(() => !!document.querySelector('button')) && await page.content().then(html => /Follow|Following|关注/i.test(html));
if (!btn) throw new Error('Follow/Following button text not present — check login state and locale');

Type guard

function isButtonNotFound(e) { return /BUTTON_NOT_FOUND/.test(e.message); }

Try / catch

try {
  await followUser(page, { username });
} catch (e) {
  if (isButtonNotFound(e)) {
    await ensureTikTokLogin(page);      // most common cause: logged out
    await followUser(page, { username });
  } else throw e;
}

Prevention

When it happens

Trigger: findButtonByText(FOLLOW_LABELS) returned null on the profile page: not logged in (buttons render differently), the target account is private/blocked/unavailable, the page showed a login wall, or TikTok renamed button labels so FOLLOW_LABELS/FOLLOWING_LABELS no longer match.

Common situations: Session expired so TikTok shows logged-out profile markup, following a private or region-blocked account, non-English locale where button text isn't in FOLLOW_LABELS, or TikTok UI redesign changing button markup.

Related errors


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