{"record":{"id":"9e2e52347f21b6bb","repo":"w7corp/easywechat","slug":"missing-header-header","errorCode":null,"errorMessage":"Missing Header: {$header}","messagePattern":"Missing Header: (.+?)","errorType":"exception","errorClass":"InvalidSignatureException","httpStatus":null,"severity":"error","filePath":"src/Pay/Validator.php","lineNumber":36,"sourceCode":"    public const HEADER_NONCE = 'Wechatpay-Nonce';\n\n    public const HEADER_SERIAL = 'Wechatpay-Serial';\n\n    public const HEADER_SIGNATURE = 'Wechatpay-Signature';\n\n    public function __construct(protected MerchantInterface $merchant)\n    {\n    }\n\n    /**\n     * @throws InvalidConfigException\n     * @throws InvalidSignatureException\n     */\n    public function validate(MessageInterface $message): void\n    {\n        foreach ([self::HEADER_SIGNATURE, self::HEADER_TIMESTAMP, self::HEADER_SERIAL, self::HEADER_NONCE] as $header) {\n            if (! $message->hasHeader($header)) {\n                throw new InvalidSignatureException(\"Missing Header: {$header}\");\n            }\n        }\n\n        [$timestamp] = $message->getHeader(self::HEADER_TIMESTAMP);\n        [$nonce] = $message->getHeader(self::HEADER_NONCE);\n        [$serial] = $message->getHeader(self::HEADER_SERIAL);\n        [$signature] = $message->getHeader(self::HEADER_SIGNATURE);\n\n        $body = (string) $message->getBody();\n\n        $message = \"{$timestamp}\\n{$nonce}\\n{$body}\\n\";\n\n        if (\\time() - \\intval($timestamp) > self::MAX_ALLOWED_CLOCK_OFFSET) {\n            throw new InvalidSignatureException('Clock Offset Exceeded');\n        }\n\n        $publicKey = $this->merchant->getPlatformCert($serial);\n","sourceCodeStart":18,"sourceCodeEnd":54,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/Pay/Validator.php#L18-L54","documentation":"Validator::validate() (src/Pay/Validator.php:34-38) first asserts that the PSR-7 message carries all four WeChat Pay signing headers — Wechatpay-Signature, Wechatpay-Timestamp, Wechatpay-Serial, Wechatpay-Nonce — and throws InvalidSignatureException('Missing Header: ...') before any crypto runs. Without these headers the message provenance cannot be verified, so the SDK treats their absence as an unauthenticatable message. The same validator runs for inbound webhooks (Server::serve(), src/Pay/Server.php) and for API responses (ResponseValidator, src/Pay/ResponseValidator.php:31).","triggerScenarios":"Validating any PSR-7 message lacking the Wechatpay-* headers: hand-built test or simulated callback requests; Server::serve() on a route hit by browsers, crawlers or health checks; a reverse proxy, WAF or CDN stripping non-standard vendor headers; validating a rebuilt request/response object instead of the original; using a mocked PSR-18 client for API calls so responses never carry Wechatpay-* headers and ResponseValidator trips.","commonSituations":"PHPUnit or local webhook simulation posted as bare JSON; nginx/Cloudflare/gateway config dropping unknown headers; framework middleware replacing the request object; wrong notify URL registered in the WeChat console so non-WeChat traffic arrives; mock HTTP clients in tests breaking response signature validation.","solutions":["Log every incoming header at the callback route to see exactly what arrives and which of the four Wechatpay-* headers are missing.","Pass the original PSR-7 server request with its untouched body into Server/Validator; don't validate reconstructed objects.","Fix the proxy layer (nginx, CDN, WAF) to forward Wechatpay-Signature, Wechatpay-Timestamp, Wechatpay-Serial and Wechatpay-Nonce to PHP.","In tests, build the request with all four headers plus a real signature, or stub/bypass validation deliberately.","Confirm the notify URL in the WeChat Pay console points to the route that actually receives WeChat traffic."],"exampleFix":"// before: simulated callback without signed headers\n$request = (new ServerRequest('POST', '/webhook/wechat'))\n    ->withHeader('Content-Type', 'application/json');\n$request->getBody()->write(json_encode($payload));\n$app->getValidator()->validate($request); // Missing Header: Wechatpay-Signature\n\n// after: include all four Wechatpay-* headers\n$request = (new ServerRequest('POST', '/webhook/wechat'))\n    ->withHeader('Wechatpay-Timestamp', (string) time())\n    ->withHeader('Wechatpay-Nonce', bin2hex(random_bytes(16)))\n    ->withHeader('Wechatpay-Serial', '<platform-cert-serial>')\n    ->withHeader('Wechatpay-Signature', base64_encode($signature));\n$request->getBody()->write(json_encode($payload));\n$app->getValidator()->validate($request);","handlingStrategy":"validation","validationCode":"$required = [\n    'Wechatpay-Signature',\n    'Wechatpay-Timestamp',\n    'Wechatpay-Serial',\n    'Wechatpay-Nonce',\n];\n$missing = array_filter($required, fn (string $h): bool => ! $request->hasHeader($h));\n\nif ($missing !== []) {\n    // Not a WeChat Pay notification — reject before signature validation\n    return new \\GuzzleHttp\\Psr7\\Response(400);\n}\n\n$app->getValidator()->validate($request);","typeGuard":"function isWechatPaySignedRequest(\\Psr\\Http\\Message\\ServerRequestInterface $request): bool\n{\n    foreach (['Wechatpay-Signature', 'Wechatpay-Timestamp', 'Wechatpay-Serial', 'Wechatpay-Nonce'] as $header) {\n        if (! $request->hasHeader($header)) {\n            return false;\n        }\n    }\n\n    return true;\n}","tryCatchPattern":"use EasyWeChat\\Pay\\Exceptions\\InvalidSignatureException;\n\ntry {\n    $app->getValidator()->validate($request);\n} catch (InvalidSignatureException $e) {\n    // Reject so WeChat Pay retries with a well-formed request; never process the message.\n    // Note: Server::serve() converts this internally into a 500 ERROR JSON response.\n    return $response->withStatus(400, 'Invalid Request');\n}","preventionTips":["Forward Wechatpay-* headers through nginx/CDN/WAF to PHP","Test callbacks with captured real signed requests, not bare JSON posts","Log all inbound headers at the notify route during integration","Register the notify URL only on routes meant to receive WeChat traffic"],"tags":["wechat-pay","webhook","http-headers","psr-7","php"],"backgroundTag":"missing-http-header","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}