CherryHQ/cherry-studio · error · Error

QR login failed after ${MAX_QR_RETRIES} expired QR codes. Us

Error message

QR login failed after ${MAX_QR_RETRIES} expired QR codes. Use config tool to reconnect.

What it means

Thrown by loginFlow() after MAX_QR_RETRIES (3) QR codes have expired in succession without a confirmed scan. Each QR code is generated via fetchQrCode, polled until status:'expired', and the loop increments qrRetries. After 3 expired QRs (so 3 full generate→poll→expired cycles, each lasting however long WeChat keeps a QR valid), the loop exits and throws this. The message directs the user to the config tool to reconnect. This is a deliberate user-facing failure: the login window elapsed without action three times.

Source

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

        const credentials: Credentials = {
          token: status.bot_token,
          baseUrl: status.baseurl ?? options.baseUrl,
          accountId: status.ilink_bot_id,
          userId: status.ilink_user_id
        }
        await saveCredentials(credentials, options.tokenPath)
        return credentials
      }

      if (status.status === 'expired') break

      await delay(QR_POLL_INTERVAL_MS)
    }

    qrRetries++
  }

  throw new Error(`QR login failed after ${MAX_QR_RETRIES} expired QR codes. Use config tool to reconnect.`)
}

// --------------- WeixinBot ---------------

type MessageHandler = (msg: IncomingMessage) => void | Promise<void>

export interface WeixinBotOptions {
  baseUrl?: string
  tokenPath?: string
  onError?: (error: unknown) => void
  onQrUrl?: (url: string) => void
}

/** Normalize a base URL to origin form (no trailing slash). */
function normalizeBaseUrl(baseUrl: string): string {
  return baseUrl.replace(/\/+$/, '')
}

View on GitHub (pinned to 726446b54c)

Solutions

  1. Increase MAX_QR_RETRIES if 3 is too few for the use case — but better to fix the underlying UX (auto-regenerate the QR without a hard cap).
  2. Verify the QR is actually displayed: the onQrUrl callback (WeChatAdapter.ts:41) emits to renderer — check the QR rendering path end-to-end.
  3. Restart the login via the config tool as the message suggests (WeixinBot.login({force:true})).
  4. Check that the device scanning the QR has the WeChat app and camera permission, and is scanning within the QR validity window (~1-2 minutes).

Example fix

// before — hard cap of 3, then a fatal throw
while (qrRetries < MAX_QR_RETRIES) {
  // ...
  qrRetries++
}
throw new Error(`QR login failed after ${MAX_QR_RETRIES} expired QR codes. Use config tool to reconnect.`)

// after — auto-regenerate expired QRs indefinitely while the login view is open
while (!options.signal?.aborted) {
  // generate QR, poll until confirmed or expired
  // ...
  if (status.status === 'expired') {
    options.onQrUrl?.(null) // clear old QR in UI
    continue // regenerate without incrementing a hard cap
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// The hard cap (MAX_QR_RETRIES=3) is the throw's cause. To avoid hitting it,
// ensure the QR is actually displayed and scannable. Verify the onQrUrl callback
// delivers a renderable data URL before starting login:
async function startWeChatLogin(bot: WeixinBot, signal: AbortSignal, onQr: (url: string) => void) {
  let qrDisplayed = false
  const wrappedOnQr = (url: string) => {
    qrDisplayed = true
    onQr(url)
  }
  const loginPromise = bot.login({ signal, onQrUrl: wrappedOnQr })
  // If the QR never renders, abort early rather than letting 3 QRs expire silently
  return loginPromise
}

Type guard

// The throw is deterministic after MAX_QR_RETRIES expirations — no runtime type guard,
// but you can detect the message to branch recovery:
function isQrRetriesExhausted(e: unknown): boolean {
  return e instanceof Error && /QR login failed after \d+ expired QR codes/.test(e.message)
}

Try / catch

// Auto-restart the QR flow one more time, or surface a clear user action
try {
  await bot.login({ signal })
} catch (e) {
  if (isQrRetriesExhausted(e)) {
    // Option A: restart the flow with a fresh WeixinBot
    logger.warn('WeChat QR retries exhausted, restarting login once')
    await bot.login({ force: true, signal })
    // Option B: surface to user
    // showUserError('WeChat QR expired 3 times — open Channel settings to scan a new QR')
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: Three consecutive QR codes reach 'expired' status. For each: fetchQrCode returns a QR, the inner loop polls pollQrStatus every QR_POLL_INTERVAL_MS (2s), status transitions wait→scaned→confirmed OR wait→expired. If expired, the inner loop breaks (line 645), qrRetries increments, and if it reaches MAX_QR_RETRIES=3, the while loop exits and this throws. Note: a QR that is generated but never scanned within its validity window expires.

Common situations: The user walked away from the screen with the QR displayed; the QR image was not rendered correctly in the UI (the onQrUrl callback delivered a data URL that did not display) so the user never saw it; the user's WeChat app could not scan (camera/camera-permission issue); the user scanned with the wrong WeChat account; network issues prevented the scan status from reaching the server.

Related errors


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