w7corp/easywechat · error · RuntimeException

-40005

-40005

Error message

Invalid appId.

What it means

After a successful AES decryption, Encryptor::decrypt() reads the 4-byte big-endian content length and requires the remaining tail of the plaintext to equal $this->receiveId (the app_id/corp_id passed to the constructor). A mismatch throws RuntimeException('Invalid appId.', -40005 ERROR_INVALID_APP_ID): the message was encrypted for a different WeChat account than the one you configured.

Source

Thrown at src/Kernel/Encryptor.php:206

        if (! hash_equals($signature, $msgSignature)) {
            throw new RuntimeException('Invalid Signature.', self::ERROR_INVALID_SIGNATURE);
        }

        $plaintext = Pkcs7::unpadding(
            openssl_decrypt(
                base64_decode($ciphertext, true) ?: '',
                'aes-256-cbc',
                $this->aesKey,
                OPENSSL_NO_PADDING,
                iv: substr($this->aesKey, 0, self::BLOCK_SIZE)
            ) ?: '',
            blockSize: strlen($this->aesKey)
        );
        $plaintext = substr($plaintext, self::BLOCK_SIZE);
        $contentLength = (unpack('N', substr($plaintext, 0, 4)) ?: [])[1];

        if ($this->receiveId && trim(substr($plaintext, $contentLength + 4)) !== $this->receiveId) {
            throw new RuntimeException('Invalid appId.', self::ERROR_INVALID_APP_ID);
        }

        return substr($plaintext, 4, $contentLength);
    }
}

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. Verify the app_id (or corp_id / component app_id) used to build the Encryptor is the same account that WeChat encrypted the callback for
  2. For OpenPlatform callbacks, make sure you use the authorizer's app_id context, not the open platform component app_id, when constructing the decryptor
  3. Confirm the aes_key belongs to the same account — a wrong key decrypts to garbage and corrupts the length/tail parsing
Defensive patterns

Strategy: try-catch

Try / catch

try {
    $message = $app->getServer()->handle($request);
} catch (\EasyWeChat\Kernel\Exceptions\RuntimeException $e) {
    if ((int) $e->getCode() === \EasyWeChat\Kernel\Encryptor::ERROR_INVALID_APP_ID) {
        // callback arrived for a different account than this app is configured with
        log_mismatched_callback($request);
        return new Response('', 403);
    }
    throw $e;
}

Prevention

When it happens

Trigger: An OpenPlatform component callback (authorized appid payload) being decrypted by an Encryptor built with the component app_id, or vice versa; app_id/corp_id from another environment (test vs prod); occasionally a wrong aes_key producing garbage plaintext with a bogus contentLength, making the tail comparison fail.

Common situations: OpenPlatform apps mixing component_id/app_id values, env config bleeding between deployments, one callback endpoint serving several official accounts but a single hardcoded app_id, wrong aes_key after key rotation.

Related errors


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