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

  1. Verify the V2 API key and APIv3 secretKey in the merchant console exactly match the values in the Merchant config of the receiving environment
  2. Log base64_decode-level intermediate values (or enable debug) to see whether req_info or event_ciphertext is the failing leg
  3. 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

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


AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21). Data as JSON: /api/errors/8affa2fbf42e31d4. Report an issue: GitHub.