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

  1. Read the {err} body first — QQ returns a JSON body whose code identifies the exact cause (auth, param, rate limit)
  2. For 401/token errors, force a fresh token (clear the cached token so get_token re-authenticates) and verify the AppID/AppSecret config
  3. For invalid-param codes, check the recipient string format and the markdown payload built by build_text_markdown_body
  4. If the body indicates the msg_id/passive reply expired, send as an active message without the reply reference
  5. 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

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


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