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
- 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).
- Ensure the catch in performConnect distinguishes 'Login cancelled' from real failures (the existing .catch at line 51 checks signal.aborted).
- Do not retry on this error — the user intentionally cancelled.
- 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
- Check signal.aborted before starting loginFlow to avoid a needless QR fetch.
- Map 'Login cancelled' to a 'disconnected' UI status, not an error toast.
- Make abort idempotent: WeixinBot.stop() (WeChatProtocol.ts:861) aborts loginAbort once; subsequent calls are safe.
- Do not retry on cancellation — the user intended to stop.
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
- Registration polling aborted
- Bot is not connected
- QR login confirmed, but the API did not return bot credentia
- QR login failed after ${MAX_QR_RETRIES} expired QR codes. Us
- Agent not found: ${agentId}
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/d1184c2e335d2970.
Report an issue: GitHub.