w7corp/easywechat · error · BadRequestException

Request signature must not be empty.

Error message

Request signature must not be empty.

What it means

assertSignatureMatches requires a non-empty signature; the servers read it from the query string — msg_signature for encrypted traffic (src/OfficialAccount/Server.php:205, src/Work/Server.php:211) and signature for echostr/plain validation. getQueryValue() returns '' for missing or non-scalar values, so a rewritten URL without its query string, or ?msg_signature[]=x, yields the empty-signature error before any hash is computed.

Source

Thrown at src/Kernel/Traits/DecryptMessage.php:100

     *
     * @param  array<string,mixed>  $query
     */
    protected function getQueryValue(array $query, string $key): string
    {
        $value = $query[$key] ?? '';

        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

  1. Forward the complete original query string (msg_signature, timestamp, nonce) to the SDK.
  2. Check proxy/router rewrite rules preserve '?' — log $request->getUri()->getQuery() once to verify.
  3. Use the callback URL exactly as configured in the WeChat console.
  4. In tests, generate a valid msg_signature (sha1 of sorted token/timestamp/nonce/ciphertext) instead of omitting it.

Example fix

// before (test): GET /callback with no signature params
// after (test): sign like WeChat does
$params = [$token, $ts, $nonce, $cipher];
sort($params, SORT_STRING);
$sig = sha1(implode($params));
// GET /callback?msg_signature={$sig}&timestamp={$ts}&nonce={$nonce}&encrypt_type=aes
Defensive patterns

Strategy: validation

Validate before calling

$q = $request->getQueryParams();
foreach (['msg_signature', 'timestamp', 'nonce'] as $k) {
    if (empty($q[$k]) || !is_scalar($q[$k])) { return new \Nyholm\Psr7\Response(400); }
}

Try / catch

try { $response = $server->serve(); } catch (\EasyWeChat\Kernel\Exceptions\BadRequestException $e) { if (str_contains($e->getMessage(), 'signature must not be empty')) { \Log::warning('callback without signature', ['query' => $request->getUri()->getQuery()]); return new \Nyholm\Psr7\Response(400); } throw $e; }

Prevention

When it happens

Trigger: A reverse proxy or router rewrite that drops the callback URL's query string; local tests calling the callback route without msg_signature/timestamp/nonce; receiving the signature in the POST body instead of the URL; array-typed query params produced by malformed rewrites.

Common situations: nginx try_files / internal redirects losing the query string; hitting the route from Postman without params; a WAF normalizing URLs before forwarding to PHP.

Related errors


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