{"record":{"id":"2e7bc0f58c8af359","repo":"w7corp/easywechat","slug":"request-ciphertext-must-not-be-empty","errorCode":null,"errorMessage":"Request ciphertext must not be empty.","messagePattern":"Request ciphertext must not be empty\\.","errorType":"exception","errorClass":"BadRequestException","httpStatus":null,"severity":"error","filePath":"src/Kernel/Traits/DecryptMessage.php","lineNumber":29,"sourceCode":"trait DecryptMessage\n{\n    /**\n     * Decrypt message (automatically detects XML or JSON format).\n     *\n     * @throws RuntimeException\n     * @throws BadRequestException\n     */\n    public function decryptMessage(\n        Message $message,\n        Encryptor $encryptor,\n        string $signature,\n        int|string $timestamp,\n        string $nonce\n    ): Message {\n        $ciphertext = $message->Encrypt ?? $message->encrypt ?? null;\n\n        if (! is_string($ciphertext) || $ciphertext === '') {\n            throw new BadRequestException('Request ciphertext must not be empty.');\n        }\n\n        $this->validateSignature($encryptor->getToken(), $ciphertext, $signature, $timestamp, $nonce);\n\n        $plaintext = $encryptor->decrypt(\n            ciphertext: $ciphertext,\n            msgSignature: $signature,\n            nonce: $nonce,\n            timestamp: $timestamp\n        );\n\n        $attributes = MessageParser::parse($plaintext);\n\n        $message->merge($attributes);\n\n        return $message;\n    }\n","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Kernel/Traits/DecryptMessage.php#L11-L47","documentation":"decryptMessage requires the incoming push to carry an encrypted body — the Encrypt (XML) or encrypt (JSON) field. OfficialAccount/MiniApp servers only take the decryption path when the request looks encrypted (encrypt_type=aes query param or an Encrypt node, src/OfficialAccount/Server.php:154), and Work always decrypts (src/Work/Server.php:53). When the trigger says encrypted but the parsed message has no ciphertext node, this BadRequestException is thrown.","triggerScenarios":"encrypt_type=aes on the callback URL while the account was switched to plaintext mode in the console (or vice versa); the raw request body already consumed or emptied by framework middleware so Message::createFromRequest sees no Encrypt field; a Work callback routed through a proxy that rewrites the body; hand-crafted test requests without an Encrypt node.","commonSituations":"Reading php://input (or creating the PSR-7 request without a body) before the SDK; mode changes between plaintext/compatible/safe in the console after URL verification; re-posting queued callback bodies in a rebuilt, incomplete form.","solutions":["Pass the original untouched ServerRequestInterface to the app/server (setRequest/serve).","Align modes: in plaintext mode remove encrypt_type=aes expectations, or set require_encryption correctly for your mode.","If middleware must inspect the body, rewind/rebuild it before dispatching to the SDK.","For custom flows, check $message->Encrypt/encrypt exists before calling getDecryptedMessage()."],"exampleFix":"// before: middleware consumed php://input, so the message carries no Encrypt node\nfile_get_contents('php://input');\n$app->getServer()->serve();\n// after: hand the pristine PSR-7 request (raw body intact) to the SDK\n$app->setRequest($psrRequest);\n$app->getServer()->serve();","handlingStrategy":"validation","validationCode":"$body = (string) $request->getBody();\n$parsed = str_contains($request->getHeaderLine('content-type'), 'json') ? json_decode($body, true) : (array) simplexml_load_string($body);\n$hasCipher = is_array($parsed) && (!empty($parsed['Encrypt']) || !empty($parsed['encrypt']));\nif (!hasCipher && ($request->getQueryParams()['encrypt_type'] ?? '') === 'aes') { return new \\Nyholm\\Psr7\\Response(400); }","typeGuard":null,"tryCatchPattern":"try { $message = $app->getServer()->getDecryptedMessage(); } catch (\\EasyWeChat\\Kernel\\Exceptions\\BadRequestException $e) { if (str_contains($e->getMessage(), 'ciphertext must not be empty')) { \\Log::warning('push without Encrypt field', ['body' => substr((string) $request->getBody(), 0, 200)]); return new \\Nyholm\\Psr7\\Response(400); } throw $e; }","preventionTips":["Never read php://input before the SDK","Keep console mode (plaintext/safe) and app expectations in sync","Replay real WeChat payloads in tests, not hand-trimmed ones"],"tags":["php","callback","decryption","message-push","wechat"],"backgroundTag":"missing-encrypted-payload","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}