yikart/AiToEarn · error · BadRequestException
刷新令牌后未能获取访问令牌
Error message
刷新令牌后未能获取访问令牌
What it means
After refreshAccessToken() runs, getUserAccessToken() re-reads `twitter:accessToken:<accountId>` from Redis and expects the fresh access_token to be there. If Redis still has no token (or no access_token field), it throws this BadRequestException. Since refreshAccessToken writes to Redis before returning (twitter.auth.service.ts:435-439), reaching this throw usually means Redis read/write inconsistency, a race, or an exception swallowed between the write and the read.
Source
Thrown at project/aitoearn-electron/server/src/modules/plat/twitter/twitter.auth.service.ts:495
}
// 如果缓存中没有,尝试刷新
const accountTokenInfo = await this.accountTokenModel.findOne({accountId: accountId});
if (!accountTokenInfo || !accountTokenInfo.refreshToken) {
throw new BadRequestException('无效的账号或刷新令牌丢失');
}
// 刷新并获取新令牌
const refreshResult = await this.refreshAccessToken(
accountTokenInfo.userId,
accountTokenInfo.accountId,
accountTokenInfo.refreshToken
);
// 刷新后再次从Redis获取
const newToken = await this.redisService.get(`twitter:accessToken:${accountId}`);
if (!newToken || !newToken.access_token) {
throw new BadRequestException('刷新令牌后未能获取访问令牌');
}
return newToken.access_token;
}
/**
* 检查用户是否已授权Twitter
* @param accountId 账号ID
* @returns 是否已授权
*/
async isAuthorized(accountId: string): Promise<boolean> {
try {
const accessToken = await this.getUserAccessToken(accountId);
return !!accessToken;
} catch (error) {
return false;
}
}View on GitHub (pinned to d3aa8bea5b)
Solutions
- Verify Redis connectivity and that all app instances share the same Redis instance/DB (check REDIS config)
- Check the `twitter:accessToken:<accountId>` key directly in Redis after a refresh call (redis-cli GET / TTL) to confirm the write landed
- Inspect the Twitter refresh response logged at 'Twitter API响应:' — if access_token is undefined, the refresh token is invalid/expired and the account must re-authorize
- Instead of re-reading Redis, refactor getUserAccessToken to use refreshResult's access_token directly, removing the Redis round-trip race
Example fix
// before
const refreshResult = await this.refreshAccessToken(accountTokenInfo.userId, accountTokenInfo.accountId, accountTokenInfo.refreshToken);
const newToken = await this.redisService.get(`twitter:accessToken:${accountId}`);
if (!newToken || !newToken.access_token) {
throw new BadRequestException('刷新令牌后未能获取访问令牌');
}
// after: use the refresh result directly, fall back to Redis
const newToken = refreshResult?.access_token
? refreshResult
: await this.redisService.get(`twitter:accessToken:${accountId}`);
if (!newToken?.access_token) {
throw new BadRequestException('刷新令牌后未能获取访问令牌');
} Defensive patterns
Strategy: retry
Validate before calling
// Confirm the Redis key exists and has a TTL before relying on the cache
const ttl = await redis.ttl(`twitter:accessToken:${accountId}`);
if (ttl < 0) console.warn(`twitter:accessToken:${accountId} missing from Redis; a refresh will occur`); Type guard
interface TwitterToken { access_token: string; refresh_token?: string; expires_in?: number }
function isValidToken(t: unknown): t is TwitterToken {
return !!t && typeof t === 'object' && typeof (t as TwitterToken).access_token === 'string' && (t as TwitterToken).access_token.length > 0;
} Try / catch
try {
return await twitterAuthService.getUserAccessToken(accountId);
} catch (e) {
if (e instanceof BadRequestException && e.message === '刷新令牌后未能获取访问令牌') {
await sleep(200); // brief retry to absorb Redis replication/visibility delay
return await twitterAuthService.getUserAccessToken(accountId);
}
throw e;
} Prevention
- Point all app instances at the same shared Redis instance/DB
- Avoid Redis eviction policies (allkeys-lru) that can drop token keys under memory pressure
- Log the Twitter refresh HTTP response and alert when access_token is absent
- Refactor to use the refresh call's return value directly instead of re-reading Redis
When it happens
Trigger: Redis was flushed or the key evicted between refreshAccessToken's setKey and the follow-up get; multiple server instances pointing at different Redis DBs; refreshAccessToken silently failed to derive access_token from the Twitter response (empty access_token in data); key mismatch caused by accountId casing/whitespace.
Common situations: Multi-node deployments with per-node in-memory Redis; Redis maxmemory eviction policies dropping keys; a proxy/LB routing refresh and read to different backends; expired refresh token causing the refresh HTTP call to return 200 with error body that still stored undefined access_token.
Related errors
- ResponseCode.ChannelAuthSessionInvalid
- ChannelAuthRefreshTokenMissing
- Twitter user profile not found
- Failed to refresh access token
- 无效的状态参数或状态已过期
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/561e632299e7c976.
Report an issue: GitHub.