w7corp/easywechat · error · BadRequestException

Failed to decode content. Content must be valid XML or JSON.

Error message

Failed to decode content. Content must be valid XML or JSON.

What it means

MessageParser::parse accepts only content that JSON-decodes to a non-empty array or XML-parses to a non-empty array; anything else throws this BadRequestException. Its main internal use is DecryptMessage::decryptMessage (src/Kernel/Traits/DecryptMessage.php:41), which parses the plaintext of a decrypted push. Garbage almost always means the aes_key was wrong, or the content was never WeChat push data at all.

Source

Thrown at src/Kernel/Support/MessageParser.php:39

        $content = trim($content);

        // Try JSON format first
        $parsed = json_decode($content, true);

        if (json_last_error() === JSON_ERROR_NONE && is_array($parsed) && ! empty($parsed)) {
            /** @var array<string, mixed> $parsed */
            return $parsed;
        }

        // If JSON decode failed or result is not an array, try XML format
        $parsed = Xml::parse($content);

        if (is_array($parsed) && ! empty($parsed)) {
            /** @var array<string, mixed> $parsed */
            return $parsed;
        }

        throw new BadRequestException('Failed to decode content. Content must be valid XML or JSON.');
    }
}

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Verify aes_key (43-char EncodingAESKey) matches the WeChat/Work console exactly.
  2. Log the raw input that reaches the parser — if it is 'success'/HTML/bot noise, restrict or protect the callback route.
  3. Ensure the request body reaches the SDK unmodified (no framework re-encoding of XML/JSON).
  4. If you parse arbitrary payloads yourself, pre-check with json_validate()/simplexml_load_string and handle null instead of calling parse().

Example fix

// before: arbitrary text hits the parser
$attrs = MessageParser::parse($body);
// after: pre-check plausibility so non-message traffic is ignored
$t = trim($body);
if ($t === '' || ($t[0] !== '<' && $t[0] !== '{')) { return new Response(200, [], 'success'); }
$attrs = MessageParser::parse($body);
Defensive patterns

Strategy: try-catch

Validate before calling

$t = trim($body);
$looksLikeMessage = $t !== '' && ($t[0] === '<' || $t[0] === '{' || $t[0] === '[');

Type guard

function isParseableMessageBody(string $body): bool { $t = trim($body); if ($t === '') { return false; } if ($t[0] === '<') { return @simplexml_load_string($t) !== false; } json_decode($t); return json_last_error() === JSON_ERROR_NONE; }

Try / catch

try { $attrs = \EasyWeChat\Kernel\Support\MessageParser::parse($plain); } catch (\EasyWeChat\Kernel\Exceptions\BadRequestException $e) { \Log::warning('unparseable push payload', ['head' => substr($plain, 0, 64)]); return new \Nyholm\Psr7\Response(200, [], 'success'); }

Prevention

When it happens

Trigger: A decrypted push whose EncodingAESKey does not match the WeChat side, so the plaintext is binary garbage (the signature check only covers token/timestamp/nonce/ciphertext, so a bad aes_key passes it); calling MessageParser::parse() on 'success', an echostr, an HTML error page, '{}' or '[]' (empty arrays are rejected by the ! empty check); a callback route hit by bots or uptime monitors.

Common situations: aes_key copied from another app or truncated; monitoring services POSTing to the WeChat callback URL; local tests sending arbitrary strings; proxies returning an HTML error page recorded as the response body.

Understand the failure class

Related errors


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