{"record":{"id":"17e440ac86941348","repo":"zeroclaw-labs/zeroclaw","slug":"wecom-webhook-send-failed-status-err","errorCode":null,"errorMessage":"WeCom webhook send failed ({status}): {err}","messagePattern":"WeCom webhook send failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wecom.rs","lineNumber":94,"sourceCode":"    async fn send(&self, message: &SendMessage) -> anyhow::Result<()> {\n        let body = serde_json::json!({\n            \"msgtype\": \"text\",\n            \"text\": {\n                \"content\": message.content,\n            }\n        });\n\n        let resp = self\n            .http_client()\n            .post(self.webhook_url())\n            .json(&body)\n            .send()\n            .await?;\n\n        if !resp.status().is_success() {\n            let status = resp.status();\n            let err = resp.text().await.unwrap_or_default();\n            anyhow::bail!(\"WeCom webhook send failed ({status}): {err}\");\n        }\n\n        // WeCom returns {\"errcode\":0,\"errmsg\":\"ok\"} on success.\n        let result: serde_json::Value = resp.json().await?;\n        let errcode = result.get(\"errcode\").and_then(|v| v.as_i64()).unwrap_or(-1);\n        if errcode != 0 {\n            let errmsg = result\n                .get(\"errmsg\")\n                .and_then(|v| v.as_str())\n                .unwrap_or(\"unknown error\");\n            anyhow::bail!(\"WeCom API error (errcode={errcode}): {errmsg}\");\n        }\n\n        Ok(())\n    }\n\n    async fn listen(&self, tx: tokio::sync::mpsc::Sender<ChannelMessage>) -> anyhow::Result<()> {\n        ::zeroclaw_log::record!(","sourceCodeStart":76,"sourceCodeEnd":112,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wecom.rs#L76-L112","documentation":"The WeCom webhook channel POSTs the message payload to the robot's webhook URL; a non-2xx HTTP status bails with the status and body. It is raised in send(), which both outbound dispatch and health_check exercise, so a bad webhook surfaces immediately on health probes.","triggerScenarios":"Calling send() or health_check() when the WeCom webhook endpoint answers 404 (wrong or rotated key in the URL), 413 (payload too large), or 5xx (WeCom gateway error).","commonSituations":"Webhook key regenerated in the WeCom admin console but not updated in channel config; copied URL truncated; markdown/text content exceeding WeCom's per-message size limit rejected at the gateway; WeCom API outage.","solutions":["Verify the full webhook URL including ?key=... matches the currently issued robot webhook","Act on the status: 404 → wrong URL/key; 413/payload errors → shorten the message; 5xx → retry later","Read the embedded body — WeCom gateway errors are explicit about the cause","Fix config before queueing more sends; use health_check to confirm the fix"],"exampleFix":"// before\nlet m = ChannelMessage::markdown(&scope, &huge_report); // gateway rejects size\n// after\nfor chunk in split_markdown(&huge_report, 2000) {\n    channel.send(ChannelMessage::markdown(&scope, chunk)).await?;\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"Parse the embedded status from 'WeCom webhook send failed': 404 → disable the channel and alert (bad key/URL); 413 → chunk the payload and resend; 5xx → retry with backoff, then dead-letter.","preventionTips":["Run health_check right after any webhook config change to catch bad URLs before real traffic","Chunk markdown/text payloads to stay under WeCom content limits","Treat the webhook key as a rotating secret and update config when reissued"],"tags":["wecom","webhook","http","send","health-check"],"backgroundTag":"upstream-api-http-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}