CherryHQ/cherry-studio · info · Error

Login cancelled

Error message

Login cancelled

What it means

Thrown by loginFlow() in WeChatProtocol at the top of the outer QR-retry loop when the abort signal is already aborted before fetching a new QR code. This is a cancellation path: the caller (typically WeixinBot.login via the adapter, or runLoop's session-expiry re-login) signalled abort and the loop honored it before doing any network I/O for this iteration. Distinct from error 137 which fires mid-poll.

Source

Thrown at src/main/ai/channels/adapters/wechat/WeChatProtocol.ts:603

  force?: boolean
  signal?: AbortSignal
  onQrUrl?: (url: string) => void
}

/** Maximum number of expired QR codes before giving up. */
const MAX_QR_RETRIES = 3

async function loginFlow(options: LoginOptions): Promise<Credentials> {
  if (!options.force) {
    const existing = await loadCredentials(options.tokenPath)
    if (existing) return existing
  }

  let qrRetries = 0

  while (qrRetries < MAX_QR_RETRIES) {
    if (options.signal?.aborted) {
      throw new Error('Login cancelled')
    }

    const qr = await fetchQrCode(options.baseUrl)
    options.onQrUrl?.(qr.qrcode_img_content)
    logger.info('QR code generated, waiting for scan', { attempt: qrRetries + 1, maxAttempts: MAX_QR_RETRIES })

    let lastStatus: string | undefined

    for (;;) {
      if (options.signal?.aborted) {
        throw new Error('Login cancelled')
      }

      const status = await pollQrStatus(options.baseUrl, qr.qrcode)

      if (status.status !== lastStatus) {
        if (status.status === 'scaned') {
          logger.info('QR code scanned, waiting for confirmation')

View on GitHub (pinned to 726446b54c)

Solutions

  1. This is expected cancellation — catch it in the connect path and treat as a clean disconnect, not an error. WeChatAdapter.performConnect already guards with signal.aborted checks (lines 49, 60).
  2. Ensure the catch in performConnect distinguishes 'Login cancelled' from real failures (the existing .catch at line 51 checks signal.aborted).
  3. Do not retry on this error — the user intentionally cancelled.
  4. Surface a 'Login cancelled' status to the UI rather than an error toast.

Example fix

// before — cancellation surfaces as a generic connect error
const credentials = await bot.login({ signal })

// after — recognize cancellation explicitly
try {
  const credentials = await bot.login({ signal })
} catch (e) {
  if (signal.aborted || (e instanceof Error && e.message === 'Login cancelled')) {
    this.sendQrToRenderer('', 'disconnected')
    return // clean cancel, not an error
  }
  throw e
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Cancellation cannot be prevented — it is the caller's intent. But you can avoid
// starting loginFlow if the signal is already aborted, and check before showing a QR.
async function safeLogin(bot: WeixinBot, signal: AbortSignal) {
  if (signal.aborted) {
    logger.info('Login cancelled before start')
    return null
  }
  return await bot.login({ signal })
}

Type guard

function isLoginCancelled(e: unknown): boolean {
  return e instanceof Error && e.message === 'Login cancelled'
}

function isAbortError(e: unknown): boolean {
  return e instanceof Error && (e.name === 'AbortError' || e.name === 'TimeoutError')
}

Try / catch

// Treat 'Login cancelled' as a clean stop, not an error — WeChatAdapter.performConnect
// already checks signal.aborted around the login call (WeChatAdapter.ts:49,60).
try {
  const credentials = await bot.login({ signal })
} catch (e) {
  if (isLoginCancelled(e) || signal.aborted) {
    this.sendQrToRenderer('', 'disconnected')
    return // clean cancel
  }
  throw e
}

Prevention

When it happens

Trigger: options.signal.aborted is true at the top of the while (qrRetries < MAX_QR_RETRIES) loop. The signal is AbortSignal.any of the caller's signal and the bot's internal loginAbort (WeChatProtocol.ts:708). WeixinBot.stop() aborts loginAbort (line 864), so calling stop() during login triggers this. Also fires if the adapter's performConnect signal was aborted (e.g. disconnect called during connect, WeChatAdapter.ts:49).

Common situations: The user clicked disconnect while the QR code was on screen (performDisconnect → bot.stop → loginAbort.abort → next loop iteration throws this); the app is shutting down during login; the user navigated away from the QR screen and the UI cancelled the login; session-expiry re-login in runLoop was aborted because the bot was stopped.

Related errors


AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12). Data as JSON: /api/errors/d1184c2e335d2970. Report an issue: GitHub.