{"record":{"id":"27176a863416c9eb","repo":"zeroclaw-labs/zeroclaw","slug":"qq-send-message-failed-status-err","errorCode":null,"errorMessage":"QQ send message failed ({status}): {err}","messagePattern":"QQ send message failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/qq.rs","lineNumber":1285,"sourceCode":"        let (scope, id) = Self::resolve_recipient(recipient);\n\n        let url = format!(\"{QQ_API_BASE}/v2/{scope}/{id}/messages\");\n        ensure_https(&url)?;\n\n        let body = Self::build_text_markdown_body(content, in_reply_to);\n\n        let resp = self\n            .http_client()\n            .post(&url)\n            .header(\"Authorization\", format!(\"QQBot {token}\"))\n            .json(&body)\n            .send()\n            .await?;\n\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let err = resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"QQ send message failed ({status}): {err}\");\n        }\n\n        Ok(())\n    }\n}\n\nimpl ::zeroclaw_api::attribution::Attributable for QQChannel {\n    fn role(&self) -> ::zeroclaw_api::attribution::Role {\n        ::zeroclaw_api::attribution::Role::Channel(::zeroclaw_api::attribution::ChannelKind::Qq)\n    }\n    fn alias(&self) -> &str {\n        &self.alias\n    }\n}\n\n#[async_trait]\nimpl Channel for QQChannel {\n    fn name(&self) -> &str {","sourceCodeStart":1267,"sourceCodeEnd":1303,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/qq.rs#L1267-L1303","documentation":"Raised by QQChannel::send_text_markdown when POST {QQ_API_BASE}/v2/{scope}/{id}/messages (msg_type=2 markdown) returns a non-success status. The request is authorized with the cached QQBot AppAccess token obtained via get_token(); the error string includes both the HTTP status and QQ's response body, which usually carries a machine-readable code (e.g. authentication or param errors). It is the generic outbound-send failure for the QQ channel's send().","triggerScenarios":"send() -> send_text_markdown after resolve_recipient splits the recipient into (scope, id); any non-2xx from the QQ open platform triggers the bail: 401 from an invalid/expired AppAccess token, 400 from malformed content or an invalid msg_id in a passive reply, 429-style rate limiting, or a bad scope/id combination.","commonSituations":"AppID/AppSecret mismatch so the cached QQBot token is invalid; the token cache outliving token validity after a long idle; replying with msg_id outside QQ's passive-reply validity window; markdown content QQ rejects; sending to a user who has never interacted with the bot (no active message session); clock skew breaking token issuance.","solutions":["Read the {err} body first — QQ returns a JSON body whose code identifies the exact cause (auth, param, rate limit)","For 401/token errors, force a fresh token (clear the cached token so get_token re-authenticates) and verify the AppID/AppSecret config","For invalid-param codes, check the recipient string format and the markdown payload built by build_text_markdown_body","If the body indicates the msg_id/passive reply expired, send as an active message without the reply reference","On rate-limit codes, back off before the next send"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if let Err(err) = channel.send(recipient, content).await {\n    let msg = format!(\"{err:#}\");\n    if msg.starts_with(\"QQ send message failed (401\") {\n        channel.invalidate_token_cache().await; // force fresh AppAccess token\n        channel.send(recipient, content).await?;\n    } else if msg.starts_with(\"QQ send message failed (429\") {\n        tokio::time::sleep(Duration::from_secs(5)).await;\n        return channel.send(recipient, content).await;\n    } else {\n        return Err(err);\n    }\n}","preventionTips":["Log the full error body — QQ's JSON body code pinpoints auth vs param vs rate-limit causes","Ensure the bot only replies within QQ's passive-reply window using fresh msg_ids","Verify the recipient string format matches what resolve_recipient expects before sending"],"tags":["qq","send-message","http-status","bot-api","markdown","authentication"],"backgroundTag":"bot-api-send-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}