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
- Download attachments immediately when the event arrives — the rkey in the URL expires, so persisted or replayed URLs fail later
- Retry the GET once or twice with short backoff; transient 5xx from the QQ CDN is common
- 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
- 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
- Download QQ attachments immediately when the event arrives; the rkey query parameter expires
- Never persist QQ multimedia URLs for later processing
- Treat a 401/403 download as 'attachment unavailable' and degrade to text instead of failing the message
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
- QQ send message failed ({status}): {err}
- QQ WebSocket connection closed: invalid session (fresh auth
- QQ WebSocket connection closed: close_code={code}, reason="{
- token refresh failed ({status}): {body}
- comment reply failed ({status}): {body}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/8767311744ef7def.
Report an issue: GitHub.