w7corp/easywechat · error · RuntimeException
Invalid request.
Error message
Invalid request.
What it means
Thrown by Pay/Server::decodeJsonMessage() when the V3 notification's resource object lacks a non-empty string ciphertext. Without ciphertext there is nothing to AES-GCM decrypt, so the notification is considered malformed. Genuine WeChat Pay V3 pushes always include it, so hitting this means the payload was altered, truncated, or hand-made.
Source
Thrown at src/Pay/Server.php:270
/**
* @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;
$nonce = $resource['nonce'] ?? null;
$associatedData = $resource['associated_data'] ?? null;
if (! is_string($ciphertext) || $ciphertext === '') {
throw new RuntimeException('Invalid request.');
}
if (! is_string($nonce) || ! is_string($associatedData)) {
throw new RuntimeException('Invalid request resource.');
}
$attributes = json_decode(
AesGcm::decrypt(
$ciphertext,
$this->merchant->getSecretKey(),
$nonce,
$associatedData,
),
true
);
if (! is_array($attributes)) {
throw new RuntimeException('Failed to decrypt request message.');View on GitHub (pinned to f0cf0a8b83)
Solutions
- Capture and log the exact raw body received at the route boundary; compare against WeChat's notification samples
- Raise body-size limits (client_max_body_size, post_max_size) if truncation is suspected
- When testing, use a recorded real notification payload including resource.ciphertext
Example fix
// before - fixture without ciphertext
$payload = ['event_type' => 'TRANSACTION.SUCCESS', 'resource' => ['nonce' => 'x', 'associated_data' => 'y']];
// after - full recorded payload
$payload = json_decode(file_get_contents('tests/fixtures/v3_notification.json'), true); Defensive patterns
Strategy: type-guard
Validate before calling
$resource = json_decode($raw, true)['resource'] ?? null;
if (! (is_array($resource) && is_string($resource['ciphertext'] ?? null) && $resource['ciphertext'] !== '')) {
return response('fail', 400);
} Type guard
function hasCiphertext(array $resource): bool
{
return is_string($resource['ciphertext'] ?? null) && $resource['ciphertext'] !== '';
} Prevention
- Use recorded production payloads for webhook tests
- Watch body-size limits on proxies and PHP for large notifications
- Validate resource shape at the route boundary before calling Server
When it happens
Trigger: A JSON body with resource present but ciphertext missing, null, non-string, or '' — e.g. custom test payloads, a middleware truncating large bodies (upload_max/ post limits), or a proxy re-encoding JSON and dropping binary-ish fields.
Common situations: Simulating webhooks with hand-written fixtures for dev; nginx/php-fpm body-size limits clipping large notifications; JSON re-serialization losing empty-string associated_data vs ciphertext distinctions in test harnesses.
Related errors
- Invalid request resource.
- Invalid event type.
- Invalid request body.
- V2 secret key is required.
- Failed to decrypt request message.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/029326a6574c35c0.
Report an issue: GitHub.