w7corp/easywechat · error · RuntimeException
Failed to decrypt request message.
Error message
Failed to decrypt request message.
What it means
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.
Source
Thrown at src/Pay/Server.php:247
$attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));
}
if (
is_array($attributes)
&& array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])
&& array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])
&& array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])
) {
$attributes += Xml::parse(AesGcm::decrypt(
$attributes['event_ciphertext'],
$this->merchant->getSecretKey(),
$attributes['event_nonce'],
$attributes['event_associated_data'] // maybe empty string
));
}
if (! is_array($attributes)) {
throw new RuntimeException('Failed to decrypt request message.');
}
return $attributes;
}
/**
* @throws RuntimeException
*/
protected function decodeJsonMessage(string $contents): array
{
$attributes = json_decode($contents, true);
if (! (is_array($attributes) && is_array($attributes['resource']))) {
throw new RuntimeException('Invalid request body.');
}
$resource = $attributes['resource'];
$ciphertext = $resource['ciphertext'] ?? null;View on GitHub (pinned to f0cf0a8b83)
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
Example fix
// before - keys from different environments $merchant = new Merchant($mchId, $privateKey, $certificate, $secretKeyStaging, $v2KeyProd); // after - both keys from the SAME merchant account/environment $merchant = new Merchant($mchId, $privateKey, $certificate, $secretKeyProd, $v2KeyProd);
Defensive patterns
Strategy: validation
Validate before calling
$plain = \EasyWeChat\Kernel\Support\AesEcb::decrypt($reqInfo, md5($merchant->getV2SecretKey()), iv: '');
if (! str_starts_with(trim($plain), '<')) {
// wrong V2 key: ack-fail and alert instead of letting RuntimeException bubble
} Try / catch
try {
$message = $app->server->getRequestMessage();
} catch (\EasyWeChat\Kernel\Exceptions\RuntimeException $e) {
if ($e->getMessage() === 'Failed to decrypt request message.') {
logger()->critical('V2/V3 key mismatch on webhook', ['raw' => $rawBody]);
return response('fail', 500); // force WeChat retry after key fix
}
throw $e;
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Encrypt AES ECB failed.
- Decrypt failed
- Invalid event type.
- Invalid request body.
- V2 secret key is required.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/8affa2fbf42e31d4.
Report an issue: GitHub.