zeroclaw-labs/zeroclaw · error

QQ upload media failed ({status}): {err}

Error message

QQ upload media failed ({status}): {err}

What it means

upload_media POSTs the file bytes to QQ's rich-media upload endpoint and bails with status and body on any non-2xx response. It runs after ensure_https and the local 10 MiB size check, so this error means QQ itself rejected the upload — commonly a token/permission problem, a media type inconsistent with the declared file type, or a platform-side failure.

Source

Thrown at crates/zeroclaw-channels/src/qq.rs:853

        // QQ API uses file_name for File type to display the filename in chat
        if file_type == QQMediaFileType::File
            && let Some(name) = file_name
        {
            body["file_name"] = json!(name);
        }

        let resp = self
            .http_client()
            .post(&api_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 upload media failed ({status}): {err}");
        }

        let upload_resp: QQUploadResponse = resp.json().await?;
        Ok((upload_resp.file_info, upload_resp.ttl))
    }

    /// Build the request body for a markdown text message (msg_type=2).
    ///
    /// Pure function — no I/O, no token fetch, no HTTP. Extracted from
    /// `send_text_markdown` so the body shape (including the optional
    /// `msg_id` for passive group replies) can be asserted directly in
    /// tests.
    fn build_text_markdown_body(content: &str, in_reply_to: Option<&str>) -> serde_json::Value {
        let mut body = json!({
            "markdown": {
                "content": content,
            },
            "msg_type": 2,

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Read the status and body in the message — 4xx with token errors means re-auth/restart the channel; other 4xx usually means the declared media type mismatches the bytes, fix the marker
  2. Verify the file is a valid, non-corrupt media file under 10 MiB and that the marker type matches the content
  3. For 5xx bodies, retry the send once after a short delay; QQ media endpoints have transient failures
Defensive patterns

Strategy: retry

Validate before calling

// Pre-validate the local file exactly like the channel does:
async fn qq_uploadable(path: &Path) -> anyhow::Result<()> {
    anyhow::ensure!(path.exists(), "attachment missing: {}", path.display());
    let len = tokio::fs::metadata(path).await?.len();
    anyhow::ensure!(len <= 10 * 1024 * 1024, "attachment {} bytes exceeds 10 MiB", len);
    Ok(())
}

Try / catch

match ch.send(&msg).await {
    Err(e) if e.to_string().contains("QQ upload media failed") => {
        let s = e.to_string();
        if s.contains("4") && !s.contains("5") {
            // 4xx: media type mismatch or auth — inspect body, fix marker/creds; do not blind-retry
        } else {
            // 5xx: transient platform failure — retry once after a short delay
        }
    }
    rest => rest?,
}

Prevention

When it happens

Trigger: send_attachment uploading a local file whose declared marker type ([IMAGE:] / [VIDEO:] / [AUDIO:] / [DOCUMENT:]) does not match the actual content; access token expired/invalid at upload time; QQ-side errors (5xx) or media quotas.

Common situations: Audio sent as [DOCUMENT:] after transcription provider changes; mislabeled attachments from agent output; uploads close to platform quotas during bursts; stale tokens on long-lived processes.

Related errors


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