zeroclaw-labs/zeroclaw · error
QQ send message failed ({status}): {err}
Error message
QQ send message failed ({status}): {err} What it means
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().
Source
Thrown at crates/zeroclaw-channels/src/qq.rs:1285
let (scope, id) = Self::resolve_recipient(recipient);
let url = format!("{QQ_API_BASE}/v2/{scope}/{id}/messages");
ensure_https(&url)?;
let body = Self::build_text_markdown_body(content, in_reply_to);
let resp = self
.http_client()
.post(&url)
.header("Authorization", format!("QQBot {token}"))
.json(&body)
.send()
.await?;
if !resp.status().is_success() {
let status = resp.status();
let err = resp.text().await.unwrap_or_default();
anyhow::bail!("QQ send message failed ({status}): {err}");
}
Ok(())
}
}
impl ::zeroclaw_api::attribution::Attributable for QQChannel {
fn role(&self) -> ::zeroclaw_api::attribution::Role {
::zeroclaw_api::attribution::Role::Channel(::zeroclaw_api::attribution::ChannelKind::Qq)
}
fn alias(&self) -> &str {
&self.alias
}
}
#[async_trait]
impl Channel for QQChannel {
fn name(&self) -> &str {View on GitHub (pinned to 88bb9c8533)
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
Defensive patterns
Strategy: try-catch
Try / catch
if let Err(err) = channel.send(recipient, content).await {
let msg = format!("{err:#}");
if msg.starts_with("QQ send message failed (401") {
channel.invalidate_token_cache().await; // force fresh AppAccess token
channel.send(recipient, content).await?;
} else if msg.starts_with("QQ send message failed (429") {
tokio::time::sleep(Duration::from_secs(5)).await;
return channel.send(recipient, content).await;
} else {
return Err(err);
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Discord send message failed ({status}): {err}
- QQ token request failed ({status}): {err}
- QQ gateway request failed ({status}): {err}
- Download failed ({}): {url}
- token refresh failed ({status}): {body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/27176a863416c9eb.
Report an issue: GitHub.