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
- 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).
- Verify the QR is actually displayed: the onQrUrl callback (WeChatAdapter.ts:41) emits to renderer — check the QR rendering path end-to-end.
- Restart the login via the config tool as the message suggests (WeixinBot.login({force:true})).
- 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
- Verify the QR data URL actually renders in the UI before relying on user scan — a broken <img> will silently exhaust retries.
- Increase MAX_QR_RETRIES or remove the hard cap and auto-regenerate expired QRs while the login view is open.
- Check the user's WeChat app camera permission if scans never register.
- Ensure the device scanning is the correct WeChat account tied to the expected workspace.
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
- QR login confirmed, but the API did not return bot credentia
- Bot is not connected
- Login cancelled
- Feishu app registration QR code expired
- WeChat can only forward image files, not "${file.media_type}
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/13df25b104fdace6.
Report an issue: GitHub.