w7corp/easywechat · error · BadRequestException
Request ciphertext must not be empty.
Error message
Request ciphertext must not be empty.
What it means
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.
Source
Thrown at src/Kernel/Traits/DecryptMessage.php:29
trait DecryptMessage
{
/**
* Decrypt message (automatically detects XML or JSON format).
*
* @throws RuntimeException
* @throws BadRequestException
*/
public function decryptMessage(
Message $message,
Encryptor $encryptor,
string $signature,
int|string $timestamp,
string $nonce
): Message {
$ciphertext = $message->Encrypt ?? $message->encrypt ?? null;
if (! is_string($ciphertext) || $ciphertext === '') {
throw new BadRequestException('Request ciphertext must not be empty.');
}
$this->validateSignature($encryptor->getToken(), $ciphertext, $signature, $timestamp, $nonce);
$plaintext = $encryptor->decrypt(
ciphertext: $ciphertext,
msgSignature: $signature,
nonce: $nonce,
timestamp: $timestamp
);
$attributes = MessageParser::parse($plaintext);
$message->merge($attributes);
return $message;
}
View on GitHub (pinned to f0cf0a8b83)
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().
Example fix
// before: middleware consumed php://input, so the message carries no Encrypt node
file_get_contents('php://input');
$app->getServer()->serve();
// after: hand the pristine PSR-7 request (raw body intact) to the SDK
$app->setRequest($psrRequest);
$app->getServer()->serve(); Defensive patterns
Strategy: validation
Validate before calling
$body = (string) $request->getBody();
$parsed = str_contains($request->getHeaderLine('content-type'), 'json') ? json_decode($body, true) : (array) simplexml_load_string($body);
$hasCipher = is_array($parsed) && (!empty($parsed['Encrypt']) || !empty($parsed['encrypt']));
if (!hasCipher && ($request->getQueryParams()['encrypt_type'] ?? '') === 'aes') { return new \Nyholm\Psr7\Response(400); } Try / catch
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; } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Request signature must not be empty.
- Invalid request signature.
- token or aes_key cannot be empty.
- Decrypt AES ECB failed.
- Decrypt failed
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/2e7bc0f58c8af359.
Report an issue: GitHub.