{"record":{"id":"cbbb87544e7c663f","repo":"w7corp/easywechat","slug":"invalid-request-body","errorCode":null,"errorMessage":"Invalid request body.","messagePattern":"Invalid request body\\.","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"src/Pay/Server.php","lineNumber":219,"sourceCode":"\n        // 微信支付的回调数据回调，偶尔是 XML https://github.com/w7corp/easywechat/issues/2737\n        $contentType = ($request ?? $this->getRequest())->getHeaderLine('content-type');\n        $isXml = (str_contains($contentType, 'text/xml') || str_contains($contentType, 'application/xml')) && str_starts_with($originContent, '<xml');\n        $attributes = $isXml ? $this->decodeXmlMessage($originContent) : $this->decodeJsonMessage($originContent);\n\n        return new Message($attributes, $originContent);\n    }\n\n    /**\n     * @throws InvalidArgumentException\n     * @throws RuntimeException\n     */\n    protected function decodeXmlMessage(string $contents): array\n    {\n        $attributes = Xml::parse($contents);\n\n        if (! is_array($attributes)) {\n            throw new RuntimeException('Invalid request body.');\n        }\n\n        if (! empty($attributes['req_info'])) {\n            $key = $this->merchant->getV2SecretKey();\n\n            if (empty($key)) {\n                throw new InvalidArgumentException('V2 secret key is required.');\n            }\n\n            $attributes = Xml::parse(AesEcb::decrypt($attributes['req_info'], md5($key), iv: ''));\n        }\n\n        if (\n            is_array($attributes)\n            && array_key_exists('event_ciphertext', $attributes) && is_string($attributes['event_ciphertext'])\n            && array_key_exists('event_nonce', $attributes) && is_string($attributes['event_nonce'])\n            && array_key_exists('event_associated_data', $attributes) && is_string($attributes['event_associated_data'])\n        ) {","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Pay/Server.php#L201-L237","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n// one route handles everything\n$app->server->handlePaidCallback(fn ($message) => ...); // XML parse of JSON body fails\n// after\nif (str_contains($request->getHeaderLine('content-type'), 'json')) {\n    $message = $app->server->handleV3Callback($fn);\n} else {\n    $message = $app->server->handlePaidCallback($fn);\n}","handlingStrategy":"type-guard","validationCode":"$raw = $request->getBody()->getContents();\nif (trim($raw) === '' || ! str_starts_with(trim($raw), '<')) {\n    return new \\Nyholm\\Psr7\\Response(400); // not XML: reject early\n}","typeGuard":"function isXmlWebhook(\\Psr\\Http\\Message\\ServerRequestInterface $r): bool\n{\n    $body = trim((string) $r->getBody());\n    return str_starts_with($body, '<');\n}","tryCatchPattern":"try {\n    $app->server->handlePaidCallback($fn);\n} catch (\\EasyWeChat\\Kernel\\Exceptions\\RuntimeException $e) {\n    if ($e->getMessage() === 'Invalid request body.') {\n        // log raw input, reply 400 so WeChat marks the push failed and retries later\n        return response('fail', 400);\n    }\n    throw $e;\n}","preventionTips":["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"],"tags":["wechat-pay","webhook","xml-parsing","v2-api"],"backgroundTag":"webhook-payload-invalid","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}