w7corp/easywechat · error · RuntimeException
Invalid request body.
Error message
Invalid request body.
What it means
Thrown by Pay/Server::decodeXmlMessage() when Xml::parse() of the raw V2 callback body does not yield an array — i.e. the request body is not well-formed XML at all. WeChat Pay V2 notifies with an XML document; anything else (empty body, form-encoded data, HTML error page from a proxy) fails here.
Source
Thrown at src/Pay/Server.php:219
// 微信支付的回调数据回调,偶尔是 XML https://github.com/w7corp/easywechat/issues/2737
$contentType = ($request ?? $this->getRequest())->getHeaderLine('content-type');
$isXml = (str_contains($contentType, 'text/xml') || str_contains($contentType, 'application/xml')) && str_starts_with($originContent, '<xml');
$attributes = $isXml ? $this->decodeXmlMessage($originContent) : $this->decodeJsonMessage($originContent);
return new Message($attributes, $originContent);
}
/**
* @throws InvalidArgumentException
* @throws RuntimeException
*/
protected function decodeXmlMessage(string $contents): array
{
$attributes = Xml::parse($contents);
if (! is_array($attributes)) {
throw new RuntimeException('Invalid request body.');
}
if (! empty($attributes['req_info'])) {
$key = $this->merchant->getV2SecretKey();
if (empty($key)) {
throw new InvalidArgumentException('V2 secret key is required.');
}
$attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));
}
if (
is_array($attributes)
&& array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])
&& array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])
&& array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])
) {View on GitHub (pinned to f0cf0a8b83)
Solutions
- Branch by Content-Type: application/json -> V3 Server flow, text/xml -> handlePaidCallback; reject GETs with 405
- Ensure the raw body reaches the library: pass the PSR-7 ServerRequest untouched, disable CSRF/ body-rewriting middleware for this route
- Log the raw contents on failure to see what actually arrived
Example fix
// before
// one route handles everything
$app->server->handlePaidCallback(fn ($message) => ...); // XML parse of JSON body fails
// after
if (str_contains($request->getHeaderLine('content-type'), 'json')) {
$message = $app->server->handleV3Callback($fn);
} else {
$message = $app->server->handlePaidCallback($fn);
} Defensive patterns
Strategy: type-guard
Validate before calling
$raw = $request->getBody()->getContents();
if (trim($raw) === '' || ! str_starts_with(trim($raw), '<')) {
return new \Nyholm\Psr7\Response(400); // not XML: reject early
} Type guard
function isXmlWebhook(\Psr\Http\Message\ServerRequestInterface $r): bool
{
$body = trim((string) $r->getBody());
return str_starts_with($body, '<');
} Try / catch
try {
$app->server->handlePaidCallback($fn);
} catch (\EasyWeChat\Kernel\Exceptions\RuntimeException $e) {
if ($e->getMessage() === 'Invalid request body.') {
// log raw input, reply 400 so WeChat marks the push failed and retries later
return response('fail', 400);
}
throw $e;
} Prevention
- Disable body-consuming middleware (CSRF) on webhook routes
- Pass the raw PSR-7 request into Server, never a filtered array
- Branch V2/V3 by Content-Type at the router level
When it happens
Trigger: Calling handlePaidCallback()/getRequestMessage() on a route where the body is not XML: V3 JSON notification posted to the V2 handler, a GET probe/health check hitting the webhook, middleware already consuming/rewriting php://input, or a proxy stripping the body.
Common situations: Sharing one webhook URL for V2 and V3 without content-type branching; local tunnel (ngrok-style) injecting an interstitial page; frameworks reading the body before the handler (CSRF middleware consuming request); XML with BOM/whitespace prefixes when the gateway re-encodes.
Related errors
- Invalid event type.
- V2 secret key is required.
- Failed to decrypt request message.
- Invalid request.
- Invalid request resource.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/cbbb87544e7c663f.
Report an issue: GitHub.