{"record":{"id":"7b058c35234c00ec","repo":"zeroclaw-labs/zeroclaw","slug":"invalid-wecom-padding-empty-payload","errorCode":null,"errorMessage":"invalid WeCom padding: empty payload","messagePattern":"invalid WeCom padding: empty payload","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/wecom_ws.rs","lineNumber":1866,"sourceCode":"    async fn cancel_draft(&self, _recipient: &str, message_id: &str) -> Result<()> {\n        let req_id = self\n            .req_id_map\n            .lock()\n            .remove(message_id)\n            .unwrap_or_default();\n        if !req_id.is_empty() {\n            self.ws_send_respond_msg(&req_id, message_id, \"\", true)\n                .await?;\n        }\n        Ok(())\n    }\n}\n\n// ── Helper functions ─────────────────────────────────────────────────\n\nfn strip_wecom_padding(input: &[u8]) -> Result<&[u8]> {\n    let Some(last) = input.last() else {\n        anyhow::bail!(\"invalid WeCom padding: empty payload\");\n    };\n    let pad_len = *last as usize;\n    if pad_len == 0 || pad_len > 32 || pad_len > input.len() {\n        anyhow::bail!(\"invalid WeCom padding length\");\n    }\n    Ok(&input[..input.len() - pad_len])\n}\n\nfn is_wecom_data_version_conflict_error(err: &anyhow::Error) -> bool {\n    let msg = err.to_string();\n    msg.contains(\"errcode=6000\") || msg.contains(\"data version conflict\")\n}\n\nfn parse_inbound_payload(payload: Value) -> Result<ParsedInbound> {\n    let msg_type = payload\n        .get(\"msgtype\")\n        .and_then(Value::as_str)\n        .unwrap_or(\"\")","sourceCodeStart":1848,"sourceCodeEnd":1884,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/wecom_ws.rs#L1848-L1884","documentation":"After AES decryption, WeCom payloads are unpadded by reading the last byte as the pad length (strip_wecom_padding, wecom_ws.rs:1864). An empty decrypted payload has no last byte and bails here. In practice the ciphertext decrypted to zero bytes: wrong aes_key or an empty/invalid frame from upstream.","triggerScenarios":"The decrypt path receives an empty input: a mismatched aes_key producing empty plaintext, an upstream empty media body, or a malformed frame that skipped the real ciphertext.","commonSituations":"aes_key from a different WeCom app or environment (test vs prod); a zero-byte attachment routed through the decrypt path; upstream sending an empty body on error.","solutions":["Verify the aes_key matches the exact WeCom app (43-char EncodingAESKey decoding to 32 bytes)","Log the ciphertext length before decrypt — zero-length input indicts the fetch/upstream, not the key","Skip and dead-letter the message; empty payloads are deterministic and retries will not help"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Guard the decrypt path: empty payloads are skipped before unpadding\nif ciphertext.is_empty() {\n    return Ok(None); // nothing to decrypt\n}","typeGuard":null,"tryCatchPattern":"Catch decrypt/padding errors per message, log msg_id and payload length, and skip that message; if every message fails, treat it as key drift — disable the channel and fix the aes_key rather than skipping forever.","preventionTips":["Validate the aes_key at startup (decodes to >= 32 bytes)","Alert when padding/decrypt failures exceed a threshold — it signals credential mismatch","Never retry padding errors; they are deterministic"],"tags":["wecom","decryption","padding","aes","payload"],"backgroundTag":"decryption-padding-error","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}