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
- 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
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
- 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
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
- Failed to get access_token: %s
- Failed to get jssdk ticket: %s
- Failed to get component_access_token: %s
- No secret configured.
- token or aes_key cannot be empty.
AI-assisted analysis of w7corp/easywechat@f0cf0a8b83 (2026-08-21).
Data as JSON: /api/errors/e3be47adfa1556b1.
Report an issue: GitHub.