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

  1. Branch by Content-Type: application/json -> V3 Server flow, text/xml -> handlePaidCallback; reject GETs with 405
  2. Ensure the raw body reaches the library: pass the PSR-7 ServerRequest untouched, disable CSRF/ body-rewriting middleware for this route
  3. 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

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


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