{"record":{"id":"a9602e18e3e30322","repo":"zeroclaw-labs/zeroclaw","slug":"qr-payload-is-empty","errorCode":null,"errorMessage":"QR payload is empty","messagePattern":"QR payload is empty","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"warning","filePath":"crates/zeroclaw-channels/src/wechat.rs","lineNumber":615,"sourceCode":"    }\n\n    result = lines.join(\"\\n\");\n    result = HEADING_RE.replace_all(&result, \"\").into_owned();\n    result = BLOCKQUOTE_RE.replace_all(&result, \"\").into_owned();\n    result = BULLET_RE.replace_all(&result, \"\").into_owned();\n    result = EMPHASIS_RE.replace_all(&result, \"\").into_owned();\n\n    while result.contains(\"\\n\\n\\n\") {\n        result = result.replace(\"\\n\\n\\n\", \"\\n\\n\");\n    }\n\n    result.trim().to_string()\n}\n\nfn render_login_qr(code: &str) -> anyhow::Result<String> {\n    let payload = code.trim();\n    if payload.is_empty() {\n        anyhow::bail!(\"QR payload is empty\");\n    }\n\n    let qr = qrcode::QrCode::new(payload.as_bytes()).map_err(|err| {\n        ::zeroclaw_log::record!(\n            ERROR,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)\n                .with_outcome(::zeroclaw_log::EventOutcome::Failure)\n                .with_attrs(::serde_json::json!({\"error\": format!(\"{}\", err)})),\n            \"Failed to encode WeChat QR payload\"\n        );\n        anyhow::Error::msg(format!(\"Failed to encode WeChat QR payload: {err}\"))\n    })?;\n\n    Ok(qr\n        .render::<qrcode::render::unicode::Dense1x2>()\n        .quiet_zone(true)\n        .build())\n}","sourceCodeStart":597,"sourceCodeEnd":633,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wechat.rs#L597-L633","documentation":"`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.","triggerScenarios":"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()`.","commonSituations":"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`.","solutions":["Retry `qr_login()` — the QR ticket is re-fetched on each loop iteration and a fresh one is usually non-empty.","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.","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.","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.","If the backend persistently returns empty `qrcode`, escalate to the iLink/WeChat bot service owner — the client cannot manufacture a login QR."],"exampleFix":"// before: assuming terminal QR always renders\nlet channel = wechat_channel.login_qr().await?; // WARN logged, no QR shown\n\n// after: consume the LoginEvent::Qr payload/image_url yourself\nuse crate::login_events::{LoginEvent, LoginEventSink};\nLoginEvent::subscribe(|ev| match ev {\n    LoginEvent::Qr { payload, image_url, .. } => {\n        if payload.trim().is_empty() {\n            eprintln!(\"backend returned an empty QR payload; retry login\");\n        } else if let Some(url) = image_url {\n            eprintln!(\"open QR image: {url}\");\n        }\n    }\n    _ => {}\n});","handlingStrategy":"fallback","validationCode":"// before rendering/depending on a QR payload, check it yourself\nlet payload = qr_payload.trim();\nif payload.is_empty() {\n    // ask the channel to refresh instead of rendering\n    return refresh_qr().await;\n}","typeGuard":"fn is_renderable_qr_payload(payload: &str) -> bool {\n    !payload.trim().is_empty()\n}","tryCatchPattern":"// qr_login already downgrades this to a WARN; in your own wrapper mirror that:\nmatch render_login_qr(&payload) {\n    Ok(qr) => println!(\"{qr}\"),\n    Err(err) if err.to_string().contains(\"QR payload is empty\") => {\n        // fall back to the LoginEvent::Qr image_url / raw payload\n        show_image_url_fallback();\n    }\n    Err(err) => return Err(err),\n}","preventionTips":["Treat the terminal QR as best-effort: subscribe to LoginEvent::Qr and render from its payload/image_url, which does not go through this check.","Retry qr_login on empty payloads; each iteration fetches a fresh get_bot_qrcode ticket.","When testing against stub backends, make the stub return a non-empty qrcode string so the login path is exercised realistically."],"tags":["wechat","qr-code","login","empty-response","terminal"],"backgroundTag":"empty-api-response-field","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}