w7corp/easywechat · error · BadRequestException
Invalid request signature.
Error message
Invalid request signature.
What it means
The signature check sorts [token, timestamp, nonce, ciphertext] (encrypted messages) or [token, timestamp, nonce] (plain/echostr requests) as strings and compares sha1 of their concatenation to the received signature with hash_equals. Any mismatch throws this BadRequestException, and in practice it almost always means the token in the app config differs from the Token configured in the WeChat/Work console, or the ciphertext used in the hash is not the exact Encrypt string from the original body.
Source
Thrown at src/Kernel/Traits/DecryptMessage.php:106
return is_scalar($value) ? strval($value) : '';
}
/**
* @param array<int, int|string> $params
*
* @throws BadRequestException
*/
protected function assertSignatureMatches(array $params, string $signature): void
{
if (empty($signature)) {
throw new BadRequestException('Request signature must not be empty.');
}
sort($params, SORT_STRING);
if (! hash_equals(sha1(implode($params)), $signature)) {
throw new BadRequestException('Invalid request signature.');
}
}
}
View on GitHub (pinned to f0cf0a8b83)
Solutions
- Copy the Token field character-for-character from the console into config 'token' (watch leading/trailing spaces).
- Give the SDK the raw, undecoded request body — never a re-serialized copy.
- Confirm the correct param set: encrypted → msg_signature over token+timestamp+nonce+ciphertext; echostr → signature over token+timestamp+nonce.
- Debug once by logging sort($params, SORT_STRING), sha1(implode($params)) and the received signature side by side.
Example fix
// before: stale env token
$app = new Application(['app_id' => $id, 'secret' => $secret, 'token' => env('WECHAT_TOKEN_OLD')]);
// after: single trimmed source of truth identical to the console value
$app = new Application(['app_id' => $id, 'secret' => $secret, 'token' => trim((string) env('WECHAT_TOKEN'))]); Defensive patterns
Strategy: try-catch
Validate before calling
$p = [$token, (string) $ts, (string) $nonce, $cipher];
sort($p, SORT_STRING);
if (!hash_equals(sha1(implode($p)), $sig)) { return new \Nyholm\Psr7\Response(403); } // mirrors assertSignatureMatches without throwing Try / catch
try { return $server->serve(); } catch (\EasyWeChat\Kernel\Exceptions\BadRequestException $e) { if (str_contains($e->getMessage(), 'Invalid request signature')) { \Log::alert('signature mismatch', ['token_len' => strlen($token), 'query' => $request->getUri()->getQuery()]); return new \Nyholm\Psr7\Response(403); } throw $e; } Prevention
- Store the token once per app/environment and trim it
- Re-verify the callback URL in the console after any token change
- Never mutate the raw body or query before serve()
When it happens
Trigger: Config 'token' typo/whitespace/stale value versus the console Token; the body was re-encoded (CDATA unwrapped, HTML entities decoded, charset converted) so the ciphertext hashed differs byte-wise from the one WeChat signed; validating an encrypted message with the plain-parameter algorithm or vice versa; timestamp or nonce altered in transit.
Common situations: Env-specific tokens (dev vs prod) crossed; multiple apps sharing one callback with per-app tokens wired to the wrong app; console token regenerated during re-verification; middleware mutating the raw XML before serve().
Related errors
- Request ciphertext must not be empty.
- Request signature must not be empty.
- token or aes_key cannot be empty.
- Failed to decode content. Content must be valid XML or JSON.
- getPhoneNumber error: %s
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/7a06db3f4105d86d.
Report an issue: GitHub.