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

QR payload is empty

Error message

QR payload is empty

What it means

`render_login_qr` turns the WeChat login QR payload into a Unicode QR block for the terminal. It bails when the payload is empty after trimming, i.e. the iLink `get_bot_qrcode` endpoint returned 200 with an empty `qrcode` string and no usable `qrcode_img_content`. The QR payload comes straight from the API response in `qr_login`, so an empty payload means the server sent a structurally valid but contentless response. Note that `qr_login` catches this error, logs it as WARN ('failed to render terminal QR code'), and continues, so login can still complete via the image URL or the emitted LoginEvent::Qr payload.

Source

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

    }

    result = lines.join("\n");
    result = HEADING_RE.replace_all(&result, "").into_owned();
    result = BLOCKQUOTE_RE.replace_all(&result, "").into_owned();
    result = BULLET_RE.replace_all(&result, "").into_owned();
    result = EMPHASIS_RE.replace_all(&result, "").into_owned();

    while result.contains("\n\n\n") {
        result = result.replace("\n\n\n", "\n\n");
    }

    result.trim().to_string()
}

fn render_login_qr(code: &str) -> anyhow::Result<String> {
    let payload = code.trim();
    if payload.is_empty() {
        anyhow::bail!("QR payload is empty");
    }

    let qr = qrcode::QrCode::new(payload.as_bytes()).map_err(|err| {
        ::zeroclaw_log::record!(
            ERROR,
            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                .with_attrs(::serde_json::json!({"error": format!("{}", err)})),
            "Failed to encode WeChat QR payload"
        );
        anyhow::Error::msg(format!("Failed to encode WeChat QR payload: {err}"))
    })?;

    Ok(qr
        .render::<qrcode::render::unicode::Dense1x2>()
        .quiet_zone(true)
        .build())
}

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Retry `qr_login()` — the QR ticket is re-fetched on each loop iteration and a fresh one is usually non-empty.
  2. Inspect the raw `get_bot_qrcode` response body (log it before `.json()`) to confirm the `qrcode`/`qrcode_img_content` field names and values the backend currently returns.
  3. Check the configured WeChat API base URL and `bot_type=3` support for that environment; an empty QR often means this bot type is not provisioned.
  4. Consume the `LoginEvent::Qr { payload, image_url }` event instead of relying on the terminal rendering; it carries the raw payload and image URL even when terminal rendering fails.
  5. If the backend persistently returns empty `qrcode`, escalate to the iLink/WeChat bot service owner — the client cannot manufacture a login QR.

Example fix

// before: assuming terminal QR always renders
let channel = wechat_channel.login_qr().await?; // WARN logged, no QR shown

// after: consume the LoginEvent::Qr payload/image_url yourself
use crate::login_events::{LoginEvent, LoginEventSink};
LoginEvent::subscribe(|ev| match ev {
    LoginEvent::Qr { payload, image_url, .. } => {
        if payload.trim().is_empty() {
            eprintln!("backend returned an empty QR payload; retry login");
        } else if let Some(url) = image_url {
            eprintln!("open QR image: {url}");
        }
    }
    _ => {}
});
Defensive patterns

Strategy: fallback

Validate before calling

// before rendering/depending on a QR payload, check it yourself
let payload = qr_payload.trim();
if payload.is_empty() {
    // ask the channel to refresh instead of rendering
    return refresh_qr().await;
}

Type guard

fn is_renderable_qr_payload(payload: &str) -> bool {
    !payload.trim().is_empty()
}

Try / catch

// qr_login already downgrades this to a WARN; in your own wrapper mirror that:
match render_login_qr(&payload) {
    Ok(qr) => println!("{qr}"),
    Err(err) if err.to_string().contains("QR payload is empty") => {
        // fall back to the LoginEvent::Qr image_url / raw payload
        show_image_url_fallback();
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Calling `qr_login()` when GET {api_base}/get_bot_qrcode?bot_type=3 returns JSON such as {"qrcode":"","qrcode_img_content":""}. Because `qrcode` is extracted with `.as_str()` (which succeeds on "") and `qrcode_img_content` defaults to "" via `unwrap_or("")`, both candidates can be empty and `render_login_qr("")` bails. A whitespace-only `qrcode` string hits the same `payload.is_empty()` check after `trim()`.

Common situations: iLink backend rotating or failing to mint a QR ticket but still answering 200; a backend version change renaming the `qrcode`/`qrcode_img_content` fields so both lookups miss; an API gateway stripping response fields; proxy or environment returning an empty JSON object. Typically seen right after backend maintenance or when pointing `api_url` at an environment that does not support `bot_type=3`.

Related errors


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