{"record":{"id":"badd843d5e04fdb8","repo":"yikart/AiToEarn","slug":"data-error-description","errorCode":null,"errorMessage":"刷新令牌失败: ${data.error_description}","messagePattern":"刷新令牌失败: (.+?)","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.auth.service.ts","lineNumber":436,"sourceCode":"    this.logger.log(`尝试刷新TikTok令牌: userId=${userId}, accountId=${accountId}`);\n    try {\n      const params = new URLSearchParams({\n        client_key: this.clientId,\n        client_secret: this.clientSecret,\n        grant_type: 'refresh_token',\n        refresh_token: refreshToken\n      });\n\n      const { data } = await firstValueFrom(\n        this.httpService.post(this.refreshTokenUrl, params.toString(), {\n          headers: {\n            'Content-Type': 'application/x-www-form-urlencoded'\n          }\n        })\n      );\n\n      if (data.error) {\n        throw new BadRequestException(`刷新令牌失败: ${data.error_description}`);\n      }\n\n      // 保存新令牌到Redis\n      await this.redisService.setKey(\n        `tiktok:accessToken:${accountId}`,\n        {\n          access_token: data.access_token,\n          refresh_token: data.refresh_token || refreshToken, // 有些OAuth提供商在刷新时不返回新的刷新令牌\n          expires_in: data.expires_in,\n          expiry_time: getCurrentTimestamp() + data.expires_in\n        },\n        data.expires_in - 300 // 令牌过期前5分钟\n      );\n\n      // 更新数据库中的刷新令牌\n      if (data.refresh_token) {\n        await this.accountTokenModel.updateOne(\n          { accountId: accountId, platform: TokenPlatform.TIKTOK },","sourceCodeStart":418,"sourceCodeEnd":454,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/tiktok/tiktok.auth.service.ts#L418-L454","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (data.error) {\n  throw new BadRequestException(`刷新令牌失败: ${data.error_description}`);\n}\n// after\nif (data.error) {\n  if (data.error === 'invalid_refresh_token' || data.error_description?.includes('refresh_token')) {\n    await this.accountTokenModel.updateOne({ accountId, platform: TokenPlatform.TIKTOK }, { $unset: { refreshToken: '' } });\n  }\n  throw new BadRequestException(`刷新令牌失败: ${data.error_description}`);\n}","handlingStrategy":"retry","validationCode":"// before refreshing, confirm a refresh token exists and was issued recently\nconst rec = await db.accountTokens.findOne({ accountId, platform: 'TIKTOK' });\nif (!rec?.refreshToken) throw new Error('no refresh token; re-authorization required');\nif (Date.now() - rec.refreshedAt < 30_000) throw new Error('refresh in progress elsewhere');","typeGuard":"function isInvalidRefreshToken(data: unknown): data is { error: string; error_description?: string } {\n  return typeof data === 'object' && data !== null &&\n    ['invalid_refresh_token', 'invalid_request'].includes((data as any).error);\n}","tryCatchPattern":"try {\n  return await api.refreshTikTokToken(refreshToken);\n} catch (e) {\n  if (isInvalidRefreshToken((e as any).response?.data)) {\n    await clearStoredRefreshToken(accountId);\n    throw new ReauthorizationRequiredError(accountId);\n  }\n  throw e;\n}","preventionTips":["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)."],"tags":["tiktok","refresh-token","oauth","token-rotation"],"backgroundTag":"invalid-refresh-token","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}