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
- If invalid_refresh_token: the stored token is spent or expired — require the user to re-authorize the TikTok account.
- Persist the NEW refresh_token returned by each refresh response immediately; TikTok refresh tokens are single-use (rotating).
- Serialize refreshes per accountId (lock in Redis) to prevent two concurrent refreshes invalidating each other.
- 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
- Persist the rotated refresh_token from every refresh response immediately.
- Use a Redis lock per accountId so only one refresh runs at a time.
- Track refresh token age and prompt re-auth before the ~1 year expiry.
- Never copy refresh tokens across environments (cn vs ai).
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
- No response from Gemini
- ChannelRefreshTokenFailed
- ChannelAuthRefreshTokenMissing
- ChannelAuthRefreshTokenMissing
- ChannelAuthRefreshTokenMissing
AI-assisted analysis of yikart/AiToEarn@d3aa8bea5b (2026-08-31).
Data as JSON: /api/errors/badd843d5e04fdb8.
Report an issue: GitHub.