{"record":{"id":"13921e336581667c","repo":"zeroclaw-labs/zeroclaw","slug":"attachment-download-failed-status-body","errorCode":null,"errorMessage":"attachment download failed ({status}): {body}","messagePattern":"attachment download failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wechat.rs","lineNumber":1186,"sourceCode":"        &self,\n        url: &str,\n        kind: WeChatAttachmentKind,\n    ) -> anyhow::Result<WeChatMediaPayload> {\n        if !url.starts_with(\"https://\") {\n            anyhow::bail!(\"refusing non-HTTPS attachment URL: {url}\");\n        }\n        let resp = self\n            .client\n            .get(url)\n            .timeout(API_TIMEOUT)\n            .send()\n            .await\n            .with_context(|| format!(\"attachment download failed: {url}\"))?;\n\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let body = resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"attachment download failed ({status}): {body}\");\n        }\n\n        if let Some(len) = resp.content_length()\n            && len > WECHAT_MEDIA_MAX_BYTES\n        {\n            anyhow::bail!(\n                \"attachment Content-Length ({len} bytes) exceeds {} MB limit\",\n                WECHAT_MEDIA_MAX_BYTES / (1024 * 1024)\n            );\n        }\n\n        let content_type = resp\n            .headers()\n            .get(reqwest::header::CONTENT_TYPE)\n            .and_then(|value| value.to_str().ok())\n            .map(str::to_string);\n        let bytes = resp.bytes().await?.to_vec();\n","sourceCodeStart":1168,"sourceCodeEnd":1204,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wechat.rs#L1168-L1204","documentation":"The HTTPS request for a remote WeChat attachment completed at the transport level, but the server answered with a non-2xx status. The message embeds both the status code and the response body (body may be empty when the error response has no payload). This is the generic remote-side failure for the attachment download step of `load_attachment_payload`.","triggerScenarios":"Any `send` with an `https://` attachment target where the origin returns an error status: 403/404 for expired or deleted CDN links (very common with time-limited signed URLs from other chat platforms), 401 for auth-gated URLs missing a token, 410 for gone media, 5xx during origin outages, or 429 rate limiting. Note DNS/TLS/timeout failures raise different errors from `send()` itself, surfaced with the `attachment download failed: {url}` context.","commonSituations":"Reposting media whose source link has expired (WeChat CDN, S3 presigned URLs past expiry, Slack/Discord media links); hotlinking from sites that block non-browser clients (403 with an HTML anti-bot body — the HTML then shows up in the error message); transient CDN 5xx; misconfigured attachment URLs in test fixtures pointing at stub servers that return 404.","solutions":["Verify the URL still works in a browser/curl; if expired, obtain a fresh link or download the file locally and attach it as a workspace file.","Retry transient statuses (5xx, 429) with backoff — a single `send` attempt does not retry downloads.","For 403 anti-bot bodies, set an appropriate `User-Agent`/`Referer` on the hosting side or mirror the file to storage you control.","If you control the origin, check its logs with the embedded status/body to find why it rejected the request."],"exampleFix":"// before: one-shot send with a possibly-stale URL\nchannel.send(msg_with_attachment(\"https://cdn.example.com/tmp/a.png\")).await?;\n\n// after: retry transient download failures, fall back to local file\nlet mut attempt = 0;\nloop {\n    match channel.send(msg_with_attachment(&url)).await {\n        Ok(_) => break,\n        Err(err) if attempt < 3 && err.to_string().contains(\"attachment download failed (5\") => {\n            attempt += 1;\n            tokio::time::sleep(std::time::Duration::from_secs(2u64 * attempt)).await;\n        }\n        Err(err) if err.to_string().contains(\"attachment download failed (\") => {\n            // permanent (4xx): mirror the file into the workspace instead\n            let local = mirror_to_workspace(&url).await?;\n            channel.send(msg_with_attachment(&local)).await?;\n            break;\n        }\n        Err(err) => return Err(err),\n    }\n}","handlingStrategy":"retry","validationCode":"// cheap preflight: is the URL live before wiring it into a message?\nasync fn url_ok(client: &reqwest::Client, url: &str) -> bool {\n    matches!(client.head(url).send().await, Ok(resp) if resp.status().is_success())\n        || matches!(client.get(url).timeout(std::time::Duration::from_secs(15)).send().await,\n                   Ok(resp) if resp.status().is_success())\n}","typeGuard":null,"tryCatchPattern":"let mut backoff = 1;\nloop {\n    match channel.send(msg_with_attachment(&url)).await {\n        Ok(_) => break,\n        Err(err) => {\n            let msg = err.to_string();\n            let transient = msg.contains(\"attachment download failed (5\") || msg.contains(\"attachment download failed (429\");\n            if transient && backoff <= 8 {\n                tokio::time::sleep(std::time::Duration::from_secs(backoff)).await;\n                backoff *= 2;\n            } else if msg.contains(\"attachment download failed (\") {\n                anyhow::bail!(\"attachment source rejected the download (4xx): {msg}\");\n            } else {\n                return Err(err);\n            }\n        }\n    }\n}","preventionTips":["Pre-check attachment URLs with a HEAD request before including them in outbound messages.","Prefer links you control (object storage with long-lived signed URLs) over third-party CDN links that expire.","Distinguish 4xx (permanent: refresh the link or mirror the file) from 5xx/429 (transient: retry with backoff) using the embedded status code.","Log the embedded body on failure; anti-bot HTML bodies tell you the origin blocked the client, not that the file is gone."],"tags":["network","http","attachment","download","wechat","cdn"],"backgroundTag":"http-download-failed","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}