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
- Verify aes_key (43-char EncodingAESKey) matches the WeChat/Work console exactly.
- Log the raw input that reaches the parser — if it is 'success'/HTML/bot noise, restrict or protect the callback route.
- Ensure the request body reaches the SDK unmodified (no framework re-encoding of XML/JSON).
- 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
- Keep callback routes dedicated to WeChat traffic
- Log undecryptable payloads together with the aes_key fingerprint (md5 of it) used
- Re-compare aes_key with the console after any app migration
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- -40012
- The type of `xml` must be string or array.
- The type of `json` must be string or array.
- 400
- Request ciphertext must not be empty.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/7e0a9724313cb784.
Report an issue: GitHub.