{"record":{"id":"f0e4eb5b86a069bb","repo":"chenhg5/cc-connect","slug":"wecom-ws-empty-ciphertext","errorCode":null,"errorMessage":"wecom-ws: empty ciphertext","messagePattern":"wecom-ws: empty ciphertext","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"platform/wecom/websocket_media.go","lineNumber":272,"sourceCode":"}\n\nfunc isHexString(s string) bool {\n\tfor i := 0; i < len(s); i++ {\n\t\tc := s[i]\n\t\tswitch {\n\t\tcase c >= '0' && c <= '9', c >= 'a' && c <= 'f', c >= 'A' && c <= 'F':\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// wecomDecryptFile decrypts payload from WeCom WS media URLs (AES-256-CBC, IV = first 16 key bytes).\n// Same algorithm as @wecom/aibot-node-sdk decryptFile.\nfunc wecomDecryptFile(ciphertext []byte, aesKeyB64 string) ([]byte, error) {\n\tif len(ciphertext) == 0 {\n\t\treturn nil, fmt.Errorf(\"wecom-ws: empty ciphertext\")\n\t}\n\tkey, err := decodeWeComAESKey(aesKeyB64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkey32 := key[:32]\n\tiv := key32[:16]\n\n\tblock, err := aes.NewCipher(key32)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(ciphertext)%aes.BlockSize != 0 {\n\t\treturn nil, fmt.Errorf(\"wecom-ws: ciphertext not multiple of block size\")\n\t}\n\tplain := make([]byte, len(ciphertext))\n\tcipher.NewCBCDecrypter(block, iv).CryptBlocks(plain, ciphertext)\n\treturn pkcs7UnpadWeCom(plain)","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/chenhg5/cc-connect/blob/4000b2338aa6e850c99df54f8b0ed6ed7460b401/platform/wecom/websocket_media.go#L254-L290","documentation":"wecomDecryptFile rejects an empty ciphertext slice before doing any crypto work. WeCom websocket media downloads are AES-256-CBC encrypted with IV = first 16 key bytes; decrypting zero bytes is meaningless, so the function fails fast rather than returning empty output.","triggerScenarios":"downloadWeComWSMedia passes the downloaded body to wecomDecryptFile when aesKey != \"\", but the HTTP response body was empty (0 bytes) — e.g. server returned 200 with no content, or a LimitReader consumed nothing.","commonSituations":"Expired or already-downloaded WeCom media URL returning an empty 200; a proxy stripping the body; fetch succeeded but the media was deleted server-side.","solutions":["Check len(raw) > 0 after the download and before decryption; log the HTTP status and headers.","Re-request the media URL — WeCom media URLs are short-lived; fetch a fresh one via the API.","If aesKey is set but server now returns plaintext, verify whether the URL is actually an encrypted WS media URL."],"exampleFix":"// before\nraw, _ := io.ReadAll(resp.Body)\nreturn wecomDecryptFile(raw, aesKey)\n// after\nraw, _ := io.ReadAll(resp.Body)\nif len(raw) == 0 {\n    return nil, fmt.Errorf(\"wecom media download returned empty body (status %s)\", resp.Status)\n}\nreturn wecomDecryptFile(raw, aesKey)","handlingStrategy":"try-catch","validationCode":"if len(body) == 0 {\n    return fmt.Errorf(\"media download returned empty body\")\n}","typeGuard":null,"tryCatchPattern":"raw, err := downloadWeComWSMedia(url, key)\nif err != nil {\n    if strings.Contains(err.Error(), \"empty ciphertext\") {\n        // fetch a fresh media URL and retry once\n    }\n}","preventionTips":["Always check len(body) before decrypting.","Log status + Content-Type on every media fetch.","Treat empty 200s as expired-URL signals and re-request the media."],"tags":["wecom","crypto","empty-input","media-download"],"backgroundTag":"empty-response-body","analyzedSha":"4000b2338aa6e850c99df54f8b0ed6ed7460b401","analyzedAt":"2026-09-06T11:45:09.575Z","contentChangedAt":"2026-09-06T11:45:09.575Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}