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

  1. Verify Redis connectivity and that all app instances share the same Redis instance/DB (check REDIS config)
  2. Check the `twitter:accessToken:<accountId>` key directly in Redis after a refresh call (redis-cli GET / TTL) to confirm the write landed
  3. 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
  4. 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

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


AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31). Data as JSON: /api/errors/561e632299e7c976. Report an issue: GitHub.