w7corp/easywechat · critical · HttpException

Failed to get stable access_token: %s

Error message

Failed to get stable access_token: %s

What it means

OfficialAccount\AccessToken::getStableAccessToken() POSTs appid/secret/force_refresh to /cgi-bin/stable_token and throws HttpException with the raw WeChat JSON when the response contains no access_token — WeChat refused to issue a token. This endpoint is used when the token client runs in stable mode. The refusal's errcode/errmsg is embedded in the message.

Source

Thrown at src/OfficialAccount/AccessToken.php:97

     * @throws HttpException
     */
    public function getStableAccessToken(bool $force_refresh = false): string
    {
        $response = $this->httpClient->request(
            'POST',
            'https://api.weixin.qq.com/cgi-bin/stable_token',
            [
                'json' => [
                    'grant_type' => 'client_credential',
                    'appid' => $this->appId,
                    'secret' => $this->secret,
                    'force_refresh' => $force_refresh,
                ],
            ]
        )->toArray(false);

        if (empty($response['access_token'])) {
            throw new HttpException('Failed to get stable access_token: '.json_encode($response, JSON_UNESCAPED_UNICODE));
        }

        $this->cache->set($this->getKey(), $response['access_token'], intval($response['expires_in']));

        return $response['access_token'];
    }

    /**
     * @throws HttpException
     */
    public function getAccessToken(): string
    {
        $response = $this->httpClient->request(
            'GET',
            'cgi-bin/token',
            [
                'query' => [
                    'grant_type' => 'client_credential',

View on GitHub (pinned to f0cf0a8b83)

Solutions

  1. json_decode the message tail and read errcode: 40164 → whitelist, 40125/41004 → bad secret, 40013 → bad appid
  2. Add the server's public egress IP to MP console → 基本配置 → IP名单 and wait a few minutes for it to take effect
  3. Verify the exact appid/secret pair against the console and redeploy with fresh env values
  4. Drop force_refresh except in a dedicated rotation job, especially with multiple instances sharing the token cache

Example fix

// before: every node force-refreshes, invalidating tokens peers still use
$token = $accessToken->getStableAccessToken(force_refresh: true);

// after: normal stable fetch; rotate only from one scheduled job
$token = $accessToken->getStableAccessToken();
Defensive patterns

Strategy: try-catch

Validate before calling

foreach (['app_id', 'secret'] as $key) {
    if (blank($app->getConfig()->get($key))) {
        throw new \RuntimeException("EasyWeChat '{$key}' is empty — token requests would fail.");
    }
}

Try / catch

use EasyWeChat\Kernel\Exceptions\HttpException;

try {
    $token = $accessToken->getStableAccessToken();
} catch (HttpException $e) {
    $payload = json_decode(strstr($e->getMessage(), '{') ?: '[]', true) ?: [];
    $errcode = $payload['errcode'] ?? null;
    if ($errcode === 40164) {
        // ops: whitelist this server's egress IP in the MP console
    } elseif (in_array($errcode, [40125, 41002, 41004, 40013], true)) {
        // config owner: appid/secret mismatch
    }
    report($e);
}

Prevention

When it happens

Trigger: Invalid appid/secret pair (40125/41002/41004-class codes); the server's outbound IP is not in the MP console IP whitelist (errcode 40164); force_refresh=true invalidating tokens other instances still serve; empty env vars in the deployed environment.

Common situations: Secret rotated in the MP console but not in the app env; deploying to a new server, container image, or CI runner whose egress IP was never whitelisted; multi-node setups mixing force_refresh; production .env not loaded so appid/secret end up empty.

Related errors


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