{"record":{"id":"029326a6574c35c0","repo":"w7corp/easywechat","slug":"invalid-request","errorCode":null,"errorMessage":"Invalid request.","messagePattern":"Invalid request\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/Pay/Server.php","lineNumber":270,"sourceCode":"\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;\n        $nonce = $resource['nonce'] ?? null;\n        $associatedData = $resource['associated_data'] ?? null;\n\n        if (! is_string($ciphertext) || $ciphertext === '') {\n            throw new RuntimeException('Invalid request.');\n        }\n\n        if (! is_string($nonce) || ! is_string($associatedData)) {\n            throw new RuntimeException('Invalid request resource.');\n        }\n\n        $attributes = json_decode(\n            AesGcm::decrypt(\n                $ciphertext,\n                $this->merchant->getSecretKey(),\n                $nonce,\n                $associatedData,\n            ),\n            true\n        );\n\n        if (! is_array($attributes)) {\n            throw new RuntimeException('Failed to decrypt request message.');","sourceCodeStart":252,"sourceCodeEnd":288,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Pay/Server.php#L252-L288","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before - fixture without ciphertext\n$payload = ['event_type' => 'TRANSACTION.SUCCESS', 'resource' => ['nonce' => 'x', 'associated_data' => 'y']];\n// after - full recorded payload\n$payload = json_decode(file_get_contents('tests/fixtures/v3_notification.json'), true);","handlingStrategy":"type-guard","validationCode":"$resource = json_decode($raw, true)['resource'] ?? null;\nif (! (is_array($resource) && is_string($resource['ciphertext'] ?? null) && $resource['ciphertext'] !== '')) {\n    return response('fail', 400);\n}","typeGuard":"function hasCiphertext(array $resource): bool\n{\n    return is_string($resource['ciphertext'] ?? null) && $resource['ciphertext'] !== '';\n}","tryCatchPattern":null,"preventionTips":["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"],"tags":["wechat-pay","webhook","json-parsing","payload-malformed"],"backgroundTag":"webhook-payload-invalid","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}