zeroclaw-labs/zeroclaw · error

Download failed ({}): {url}

Error message

Download failed ({}): {url}

What it means

Raised by QQChannel::download_attachment when the HTTP GET for a QQ multimedia URL returns any non-2xx status. QQ attachment URLs are authenticated solely by an rkey query parameter carried inside the URL itself (no Authorization header is sent, matching openclaw-qqbot's downloadFile), so a non-success status almost always means the URL was rejected: an expired rkey, deleted media, or an unreachable/misrouted CDN. The download is also capped at QQ_MAX_UPLOAD_BYTES (10 MiB) by read_response_body_limited, but that limit produces a different error.

Source

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

            .unwrap_or("file");
        let ext = Path::new(filename)
            .extension()
            .and_then(|e| e.to_str())
            .unwrap_or("");
        let unique = &Uuid::new_v4().to_string()[..8];
        let safe_name = if ext.is_empty() {
            format!("{stem}_{unique}")
        } else {
            format!("{stem}_{unique}.{ext}")
        };

        let dest = dir.join(&safe_name);

        // QQ multimedia URLs carry rkey auth in query params — no Authorization header needed
        // (consistent with openclaw-qqbot's downloadFile implementation)
        let resp = self.http_client().get(url).send().await?;
        if !resp.status().is_success() {
            anyhow::bail!("Download failed ({}): {url}", resp.status());
        }

        let bytes = crate::util::read_response_body_limited(resp, QQ_MAX_UPLOAD_BYTES).await?;
        tokio::fs::write(&dest, &bytes).await?;

        Ok((dest, bytes))
    }

    async fn try_transcribe_audio_data(&self, audio_data: &[u8], filename: &str) -> Option<String> {
        let manager = self.transcription_manager.as_deref()?;

        if audio_data.len() as u64 > QQ_MAX_AUDIO_TRANSCRIPTION_BYTES {
            ::zeroclaw_log::record!(
                WARN,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                    .with_attrs(::serde_json::json!({
                        "bytes": audio_data.len(),

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Download attachments immediately when the event arrives — the rkey in the URL expires, so persisted or replayed URLs fail later
  2. Retry the GET once or twice with short backoff; transient 5xx from the QQ CDN is common
  3. If the status is 401/403, treat the attachment as unavailable and skip it (compose the message text without the attachment) instead of failing the whole inbound message
  4. If every download fails, check the runtime proxy settings for channel.qq and network egress to QQ multimedia domains

Example fix

// before — one failed download fails the whole composed message
let (path, bytes) = self.download_attachment(&url, &dir, &name).await?;

// after — skip the attachment, keep the message
let attachment = match self.download_attachment(&url, &dir, &name).await {
    Ok(v) => Some(v),
    Err(e) => {
        ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_outcome(::zeroclaw_log::EventOutcome::Failure), format!("attachment download failed: {e:#}"));
        None
    }
};
Defensive patterns

Strategy: retry

Try / catch

match channel.compose_qq_message(&event).await {
    Err(err) => {
        let msg = format!("{err:#}");
        if msg.starts_with("Download failed (5") || msg.starts_with("Download failed (429") {
            tokio::time::sleep(Duration::from_secs(2)).await;
            channel.compose_qq_message(&event).await // one retry
        } else if msg.starts_with("Download failed (40") {
            Ok(skip_attachment_and_retry_text(&event)) // 401/403/404: rkey or media gone, skip attachment
        } else {
            Err(err)
        }
    }
    ok => ok,
}

Prevention

When it happens

Trigger: compose_qq_message calls download_attachment with the file URL of an inbound QQ message (image/audio/video/file). Any 401/403 (expired rkey), 404 (media removed), or 5xx from the QQ CDN hits the 'if !resp.status().is_success()' bail before the body is read.

Common situations: Replaying or queueing QQ events after a delay, so the rkey embedded in the URL has expired; long-running bots that persist attachment URLs instead of downloading immediately; runtime proxy config for channel.qq routing to a host that cannot reach QQ's multimedia domain; media deleted by the sender before the bot fetched it.

Related errors


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