{"record":{"id":"e3be47adfa1556b1","repo":"w7corp/easywechat","slug":"failed-to-get-stable-access-token-s","errorCode":null,"errorMessage":"Failed to get stable access_token: %s","messagePattern":"Failed to get stable access_token: (.+?)","errorType":"exception","errorClass":"HttpException","httpStatus":null,"severity":"critical","filePath":"src/OfficialAccount/AccessToken.php","lineNumber":97,"sourceCode":"     * @throws HttpException\n     */\n    public function getStableAccessToken(bool $force_refresh = false): string\n    {\n        $response = $this->httpClient->request(\n            'POST',\n            'https://api.weixin.qq.com/cgi-bin/stable_token',\n            [\n                'json' => [\n                    'grant_type' => 'client_credential',\n                    'appid' => $this->appId,\n                    'secret' => $this->secret,\n                    'force_refresh' => $force_refresh,\n                ],\n            ]\n        )->toArray(false);\n\n        if (empty($response['access_token'])) {\n            throw new HttpException('Failed to get stable access_token: '.json_encode($response, JSON_UNESCAPED_UNICODE));\n        }\n\n        $this->cache->set($this->getKey(), $response['access_token'], intval($response['expires_in']));\n\n        return $response['access_token'];\n    }\n\n    /**\n     * @throws HttpException\n     */\n    public function getAccessToken(): string\n    {\n        $response = $this->httpClient->request(\n            'GET',\n            'cgi-bin/token',\n            [\n                'query' => [\n                    'grant_type' => 'client_credential',","sourceCodeStart":79,"sourceCodeEnd":115,"githubUrl":"https://github.com/w7corp/easywechat/blob/f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8/src/OfficialAccount/AccessToken.php#L79-L115","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["json_decode the message tail and read errcode: 40164 → whitelist, 40125/41004 → bad secret, 40013 → bad appid","Add the server's public egress IP to MP console → 基本配置 → IP名单 and wait a few minutes for it to take effect","Verify the exact appid/secret pair against the console and redeploy with fresh env values","Drop force_refresh except in a dedicated rotation job, especially with multiple instances sharing the token cache"],"exampleFix":"// before: every node force-refreshes, invalidating tokens peers still use\n$token = $accessToken->getStableAccessToken(force_refresh: true);\n\n// after: normal stable fetch; rotate only from one scheduled job\n$token = $accessToken->getStableAccessToken();","handlingStrategy":"try-catch","validationCode":"foreach (['app_id', 'secret'] as $key) {\n    if (blank($app->getConfig()->get($key))) {\n        throw new \\RuntimeException(\"EasyWeChat '{$key}' is empty — token requests would fail.\");\n    }\n}","typeGuard":null,"tryCatchPattern":"use EasyWeChat\\Kernel\\Exceptions\\HttpException;\n\ntry {\n    $token = $accessToken->getStableAccessToken();\n} catch (HttpException $e) {\n    $payload = json_decode(strstr($e->getMessage(), '{') ?: '[]', true) ?: [];\n    $errcode = $payload['errcode'] ?? null;\n    if ($errcode === 40164) {\n        // ops: whitelist this server's egress IP in the MP console\n    } elseif (in_array($errcode, [40125, 41002, 41004, 40013], true)) {\n        // config owner: appid/secret mismatch\n    }\n    report($e);\n}","preventionTips":["Keep the IP whitelist updated for every environment (CI, staging, prod, new runners)","Rotate secrets atomically: console and env together, then clear caches","Use stable tokens without force_refresh when scaling horizontally; share the token cache via Redis","Alert on errcode 40164 as an infra event, not an app bug"],"tags":["php","easywechat","wechat","official-account","access-token","credentials","ip-whitelist"],"backgroundTag":"access-token-request-failed","analyzedSha":"f0cf0a8b8361417ed683b8246d0ecbaf0aafcaa8","analyzedAt":"2026-08-21T05:29:19.565Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}