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

  1. Capture and log the exact raw body received at the route boundary; compare against WeChat's notification samples
  2. Raise body-size limits (client_max_body_size, post_max_size) if truncation is suspected
  3. 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

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


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