yikart/AiToEarn · error · BadRequestException

刷新令牌失败: ${data.error_description}

Error message

刷新令牌失败: ${data.error_description}

What it means

A BadRequestException thrown by refreshAccessToken when TikTok's token endpoint accepts the refresh request (HTTP 200) but the body contains an error field. The message carries error_description, which for refresh flows is typically invalid_request or invalid_refresh_token.

Source

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

    this.logger.log(`尝试刷新TikTok令牌: userId=${userId}, accountId=${accountId}`);
    try {
      const params = new URLSearchParams({
        client_key: this.clientId,
        client_secret: this.clientSecret,
        grant_type: 'refresh_token',
        refresh_token: refreshToken
      });

      const { data } = await firstValueFrom(
        this.httpService.post(this.refreshTokenUrl, params.toString(), {
          headers: {
            'Content-Type': 'application/x-www-form-urlencoded'
          }
        })
      );

      if (data.error) {
        throw new BadRequestException(`刷新令牌失败: ${data.error_description}`);
      }

      // 保存新令牌到Redis
      await this.redisService.setKey(
        `tiktok:accessToken:${accountId}`,
        {
          access_token: data.access_token,
          refresh_token: data.refresh_token || refreshToken, // 有些OAuth提供商在刷新时不返回新的刷新令牌
          expires_in: data.expires_in,
          expiry_time: getCurrentTimestamp() + data.expires_in
        },
        data.expires_in - 300 // 令牌过期前5分钟
      );

      // 更新数据库中的刷新令牌
      if (data.refresh_token) {
        await this.accountTokenModel.updateOne(
          { accountId: accountId, platform: TokenPlatform.TIKTOK },

View on GitHub (pinned to d3aa8bea5b)

Solutions

  1. If invalid_refresh_token: the stored token is spent or expired — require the user to re-authorize the TikTok account.
  2. Persist the NEW refresh_token returned by each refresh response immediately; TikTok refresh tokens are single-use (rotating).
  3. Serialize refreshes per accountId (lock in Redis) to prevent two concurrent refreshes invalidating each other.
  4. Verify client_key/client_secret match the environment that issued the original tokens.

Example fix

// before
if (data.error) {
  throw new BadRequestException(`刷新令牌失败: ${data.error_description}`);
}
// after
if (data.error) {
  if (data.error === 'invalid_refresh_token' || data.error_description?.includes('refresh_token')) {
    await this.accountTokenModel.updateOne({ accountId, platform: TokenPlatform.TIKTOK }, { $unset: { refreshToken: '' } });
  }
  throw new BadRequestException(`刷新令牌失败: ${data.error_description}`);
}
Defensive patterns

Strategy: retry

Validate before calling

// before refreshing, confirm a refresh token exists and was issued recently
const rec = await db.accountTokens.findOne({ accountId, platform: 'TIKTOK' });
if (!rec?.refreshToken) throw new Error('no refresh token; re-authorization required');
if (Date.now() - rec.refreshedAt < 30_000) throw new Error('refresh in progress elsewhere');

Type guard

function isInvalidRefreshToken(data: unknown): data is { error: string; error_description?: string } {
  return typeof data === 'object' && data !== null &&
    ['invalid_refresh_token', 'invalid_request'].includes((data as any).error);
}

Try / catch

try {
  return await api.refreshTikTokToken(refreshToken);
} catch (e) {
  if (isInvalidRefreshToken((e as any).response?.data)) {
    await clearStoredRefreshToken(accountId);
    throw new ReauthorizationRequiredError(accountId);
  }
  throw e;
}

Prevention

When it happens

Trigger: POST to oauth/token/ with grant_type=refresh_token returns { error: ... } because the refresh_token was already used (TikTok rotates refresh tokens on every refresh), it expired (refresh tokens live ~1 year), or client credentials are wrong.

Common situations: Stale refresh_token persisted in the accountToken collection after a previous refresh rotated it, two servers refreshing concurrently with the same token, refresh token expired after inactivity, wrong client_key/secret for the environment.

Related errors


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