yikart/AiToEarn · error · BadRequestException

刷新令牌后未能获取访问令牌

Error message

刷新令牌后未能获取访问令牌

What it means

A BadRequestException thrown by getUserAccessToken after refreshAccessToken succeeded but the re-read of tiktok:accessToken:{accountId} from Redis returned nothing or a record without access_token. Indicates the refresh flow did not persist the new access token to Redis as expected.

Source

Thrown at project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.auth.service.ts:520

      accountId: accountId,
      platform: TokenPlatform.TIKTOK
    });

    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(`tiktok:accessToken:${accountId}`);
    if (!newToken || !newToken.access_token) {
      throw new BadRequestException('刷新令牌后未能获取访问令牌');
    }

    return newToken.access_token;
  }

  /**
   * 检查用户是否已授权TikTok
   * @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. Check refreshAccessToken: ensure the setKey(`tiktok:accessToken:${accountId}`) call actually runs and stores the response's access_token.
  2. Confirm both refresh and read hit the same Redis instance/DB and the key TTL exceeds TikTok's access token lifetime (~24h).
  3. Instead of re-reading Redis, have refreshAccessToken return the new access_token directly and use it.
  4. Inspect Redis memory/eviction config if keys disappear under load.

Example fix

// before
await this.refreshAccessToken(accountTokenInfo.userId, accountTokenInfo.accountId, accountTokenInfo.refreshToken);
const newToken = await this.redisService.get(`tiktok:accessToken:${accountId}`);
// after
const refreshed = await this.refreshAccessToken(accountTokenInfo.userId, accountTokenInfo.accountId, accountTokenInfo.refreshToken);
if (!refreshed?.access_token) {
  throw new BadRequestException('刷新令牌后未能获取访问令牌');
}
return refreshed.access_token;
Defensive patterns

Strategy: fallback

Validate before calling

// after refresh, verify the cache entry before trusting it
const cached = await redis.get(`tiktok:accessToken:${accountId}`);
if (!cached?.access_token) {
  throw new Error('refresh did not persist access token to redis');
}

Type guard

function isStoredAccessToken(v: unknown): v is { access_token: string; expires_in: number } {
  return typeof v === 'object' && v !== null && typeof (v as any).access_token === 'string';
}

Try / catch

try {
  return await api.getTikTokAccessToken(accountId);
} catch (e) {
  if (/刷新令牌后未能获取/.test((e as Error).message)) {
    // fallback: force a direct refresh and read the returned token
    const fresh = await api.forceRefreshTikTokToken(accountId);
    return fresh.access_token;
  }
  throw e;
}

Prevention

When it happens

Trigger: refreshAccessToken completed but its Redis setKey call failed or was skipped, a Redis eviction/TTL removed the key between refresh and read, or a race where another flow overwrote/deleted the key, leaving no access_token field.

Common situations: Redis TTL set shorter than the access token lifetime, shared Redis instance with an eviction policy (allkeys-lru) under memory pressure, multi-instance deployment where refresh wrote to a different Redis DB than the read, key name typo/namespace drift.

Related errors


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