{"record":{"id":"8affa2fbf42e31d4","repo":"w7corp/easywechat","slug":"failed-to-decrypt-request-message","errorCode":null,"errorMessage":"Failed to decrypt request message.","messagePattern":"Failed to decrypt request message\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/Pay/Server.php","lineNumber":247,"sourceCode":"            $attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));\n        }\n\n        if (\n            is_array($attributes)\n            && array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])\n            && array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])\n            && array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])\n        ) {\n            $attributes += Xml::parse(AesGcm::decrypt(\n                $attributes['event_ciphertext'],\n                $this->merchant->getSecretKey(),\n                $attributes['event_nonce'],\n                $attributes['event_associated_data'] // maybe empty string\n            ));\n        }\n\n        if (! is_array($attributes)) {\n            throw new RuntimeException('Failed to decrypt request message.');\n        }\n\n        return $attributes;\n    }\n\n    /**\n     * @throws RuntimeException\n     */\n    protected function decodeJsonMessage(string $contents): array\n    {\n        $attributes = json_decode($contents, true);\n\n        if (! (is_array($attributes) && is_array($attributes['resource']))) {\n            throw new RuntimeException('Invalid request body.');\n        }\n\n        $resource = $attributes['resource'];\n        $ciphertext = $resource['ciphertext'] ?? null;","sourceCodeStart":229,"sourceCodeEnd":265,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Pay/Server.php#L229-L265","documentation":"Thrown by Pay/Server::decodeXmlMessage() (final check) when, after optionally decrypting req_info and/or event_ciphertext, the result is still not an array. In practice the decrypt path ran but produced something unparseable: wrong V2 key yields garbage after AES-ECB decrypt, wrong APIv3 key makes AesGcm::decrypt of event_ciphertext throw or return junk, and the subsequent Xml::parse fails.","triggerScenarios":"V2 XML callback where req_info was decrypted with a mismatched V2 key, or a hybrid callback carrying event_ciphertext/event_nonce/event_associated_data decrypted with the wrong APIv3 secretKey — decrypted bytes are not valid XML, so $attributes ends up non-array.","commonSituations":"V2 key regenerated in console but old value still in config (or vice versa); APIv3 key rotated on one side only; environments (staging/prod) swapped keys; ciphertext truncated by body-size middleware.","solutions":["Verify the V2 API key and APIv3 secretKey in the merchant console exactly match the values in the Merchant config of the receiving environment","Log base64_decode-level intermediate values (or enable debug) to see whether req_info or event_ciphertext is the failing leg","Re-trigger the notification from the merchant console (or a test push) after fixing keys"],"exampleFix":"// before - keys from different environments\n$merchant = new Merchant($mchId, $privateKey, $certificate, $secretKeyStaging, $v2KeyProd);\n// after - both keys from the SAME merchant account/environment\n$merchant = new Merchant($mchId, $privateKey, $certificate, $secretKeyProd, $v2KeyProd);","handlingStrategy":"validation","validationCode":"$plain = \\EasyWeChat\\Kernel\\Support\\AesEcb::decrypt($reqInfo, md5($merchant->getV2SecretKey()), iv: '');\nif (! str_starts_with(trim($plain), '<')) {\n    // wrong V2 key: ack-fail and alert instead of letting RuntimeException bubble\n}","typeGuard":null,"tryCatchPattern":"try {\n    $message = $app->server->getRequestMessage();\n} catch (\\EasyWeChat\\Kernel\\Exceptions\\RuntimeException $e) {\n    if ($e->getMessage() === 'Failed to decrypt request message.') {\n        logger()->critical('V2/V3 key mismatch on webhook', ['raw' => $rawBody]);\n        return response('fail', 500); // force WeChat retry after key fix\n    }\n    throw $e;\n}","preventionTips":["Single source of truth for keys per merchant+environment","Rotate keys on both sides in one maintenance window","Log raw bodies for any decrypt failure to identify which leg failed"],"tags":["wechat-pay","webhook","decryption","aes","key-mismatch"],"backgroundTag":"aes-decryption-failed","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}