zeroclaw-labs/zeroclaw · error · anyhow::Error

WeChat QR code expired {$max} times, giving up.

Error message

WeChat QR code expired {$max} times, giving up.

What it means

During QR-code login the WeChat channel polls for a scan; when the QR code expires it refreshes it, tracking qr_refresh_count. Once the count exceeds MAX_QR_REFRESH = 3 (wechat.rs:39) it emits LoginEvent::Failed ('WeChat QR login gave up after repeated expiry') and bails with the localized 'QR code expired {max} times, giving up' reason.

Source

Thrown at crates/zeroclaw-channels/src/wechat.rs:1645

    /// Perform QR-code login flow. Returns (bot_token, account_id, user_id).
    async fn qr_login(&self) -> anyhow::Result<(String, String, Option<String>)> {
        let mut qr_refresh_count = 0u32;

        loop {
            qr_refresh_count += 1;
            if qr_refresh_count > MAX_QR_REFRESH {
                let max = MAX_QR_REFRESH.to_string();
                let reason = wechat_cli_string_with_args(
                    "cli-wechat-qr-expired-giving-up",
                    &[("max", &max)],
                );
                crate::login_events::LoginEvent::Failed { reason: &reason }.emit(
                    self.name(),
                    &self.alias,
                    "WeChat QR login gave up after repeated expiry",
                );
                anyhow::bail!("{reason}");
            }

            // Fetch QR code
            let qr_url = format!("{}?bot_type=3", self.api_url("get_bot_qrcode"));
            let resp = self
                .client
                .get(&qr_url)
                .timeout(API_TIMEOUT)
                .send()
                .await
                .with_context(|| wechat_cli_string("cli-wechat-qr-fetch-failed"))?;

            if !resp.status().is_success() {
                let status = resp.status().to_string();
                let body = resp.text().await.unwrap_or_default();
                anyhow::bail!(
                    "{}",
                    wechat_cli_string_with_args(

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Restart the login flow to get a fresh QR session — the refresh counter resets per login attempt — and scan promptly
  2. Make sure the QR is actually rendered where a human can scan it (terminal, dashboard via LoginEvent, forwarded image channel)
  3. If scans never register, verify the WeChat account is not restricted and the iLink api_url/token is valid
  4. Automate recovery: listen for LoginEvent::Failed and re-invoke login with a bounded retry cap

Example fix

// before
channel.login().await?; // dies after 3 QR expiries
// after
for attempt in 0..5 {
    match channel.login().await {
        Ok(()) => break,
        Err(e) if e.to_string().contains("QR code expired") && attempt < 4 => continue,
        Err(e) => return Err(e),
    }
}
Defensive patterns

Strategy: retry

Try / catch

Catch the login error, test for 'QR code expired' in the message, and re-invoke login() for a fresh QR session; cap the outer loop (e.g. 5 attempts) so a genuinely unattended bot does not spin forever.

Prevention

When it happens

Trigger: Starting WeChat channel login and nobody scanning the QR code before expiry, four times in a row (initial code plus three refreshes). Each expiry increments qr_refresh_count; exceeding 3 aborts the login attempt.

Common situations: Unattended bot startup where no one is watching the QR; the QR is printed to a log or console nobody sees; the WeChat mobile app never accepts the scan (device offline, account restricted) so every code lapses.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/9fb4aef8e6960823. Report an issue: GitHub.