w7corp/easywechat · error · BadRequestException

Encrypted message is required, plaintext message rejected.

Error message

Encrypted message is required, plaintext message rejected.

What it means

Server::serve() rejects an inbound callback when the Server was constructed with requireEncryption=true but the request carries no ciphertext (no encrypt_type=aes marker). It is a deliberate security control: plaintext mode drops encryption protection, so with the flag set EasyWeChat throws BadRequestException instead of processing. Per the class docblock, both secure and compatible mode push encrypt_type=aes — only plaintext mode has neither.

Source

Thrown at src/OfficialAccount/Server.php:58

     * @throws InvalidConfigException
     */
    public function serve(): ResponseInterface
    {
        $query = $this->getRequest()->getQueryParams();

        if ($str = $this->getQueryValue($query, 'echostr')) {
            $this->validatePlainRequest($query);

            return new Response(200, [], $str);
        }

        $message = $this->getRequestMessage($this->getRequest());

        if ($this->encryptor && $this->isEncryptedRequest($query, $message)) {
            $this->prepend($this->decryptRequestMessage($query));
        } else {
            if ($this->requireEncryption) {
                throw new BadRequestException('Encrypted message is required, plaintext message rejected.');
            }

            $this->validatePlainRequest($query);
        }

        $response = $this->handle(new Response(200, [], 'success'), $message);

        if (! ($response instanceof ResponseInterface)) {
            $response = $this->transformToReply($response, $message, $this->encryptor);
        }

        return ServerResponse::make($response);
    }

    /**
     * @throws Throwable
     */
    public function addMessageListener(string $type, callable|string $handler): static

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Switch the official account to 安全模式 (or compatible mode) in the MP console so pushes carry encrypt_type=aes and the Encrypt node
  2. Or stop requiring encryption: construct the Server without requireEncryption and keep plain signature validation
  3. For local tests, POST a real encrypted envelope (Encrypt node plus encrypt_type=aes&msg_signature query) instead of raw XML

Example fix

// before: strict server + WeChat account still in plaintext mode
$server = new \EasyWeChat\OfficialAccount\Server($request, $app->getEncryptor(), $token, requireEncryption: true);
$response = $server->serve(); // BadRequestException

// after (option A): switch the console to 安全模式 so pushes are encrypted and keep the strict server
// after (option B): keep the console in plaintext and drop the flag
$server = new \EasyWeChat\OfficialAccount\Server($request, $app->getEncryptor(), $token);
$response = $server->serve();
Defensive patterns

Strategy: validation

Validate before calling

$query = $request->getQueryParams();
$encrypted = strtolower((string) ($query['encrypt_type'] ?? '')) === 'aes';
if (! $encrypted) {
    return new \Nyholm\Psr7\Response(400, [], 'encrypted payloads only');
}

Try / catch

try {
    return $app->getServer()->serve();
} catch (\EasyWeChat\Kernel\Exceptions\BadRequestException $e) {
    // plaintext rejected on purpose: log a security event, return 400
    return new \Nyholm\Psr7\Response(400, [], 'bad request');
}

Prevention

When it happens

Trigger: MP console 消息加解密 set to 明文模式 (plaintext) while the Server requires encryption; replaying or hand-crafting a POST to the callback URL without encrypt_type=aes and the Encrypt node; test harnesses posting raw XML.

Common situations: Team enables strict encryption in code but never switches the mode in the MP console (or vice versa); different accounts per environment where one is plaintext; curl-based webhook testing against a strict server.

Related errors


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