{"record":{"id":"561e632299e7c976","repo":"yikart/AiToEarn","slug":"error-561e63","errorCode":null,"errorMessage":"刷新令牌后未能获取访问令牌","messagePattern":"刷新令牌后未能获取访问令牌","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/twitter/twitter.auth.service.ts","lineNumber":495,"sourceCode":"    }\n\n    // 如果缓存中没有，尝试刷新\n    const accountTokenInfo = await this.accountTokenModel.findOne({accountId: accountId});\n    if (!accountTokenInfo || !accountTokenInfo.refreshToken) {\n      throw new BadRequestException('无效的账号或刷新令牌丢失');\n    }\n\n    // 刷新并获取新令牌\n    const refreshResult = await this.refreshAccessToken(\n      accountTokenInfo.userId,\n      accountTokenInfo.accountId,\n      accountTokenInfo.refreshToken\n    );\n\n    // 刷新后再次从Redis获取\n    const newToken = await this.redisService.get(`twitter:accessToken:${accountId}`);\n    if (!newToken || !newToken.access_token) {\n      throw new BadRequestException('刷新令牌后未能获取访问令牌');\n    }\n\n    return newToken.access_token;\n  }\n\n  /**\n   * 检查用户是否已授权Twitter\n   * @param accountId 账号ID\n   * @returns 是否已授权\n   */\n  async isAuthorized(accountId: string): Promise<boolean> {\n    try {\n      const accessToken = await this.getUserAccessToken(accountId);\n      return !!accessToken;\n    } catch (error) {\n      return false;\n    }\n  }","sourceCodeStart":477,"sourceCodeEnd":513,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/twitter/twitter.auth.service.ts#L477-L513","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify Redis connectivity and that all app instances share the same Redis instance/DB (check REDIS config)","Check the `twitter:accessToken:<accountId>` key directly in Redis after a refresh call (redis-cli GET / TTL) to confirm the write landed","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","Instead of re-reading Redis, refactor getUserAccessToken to use refreshResult's access_token directly, removing the Redis round-trip race"],"exampleFix":"// before\nconst refreshResult = await this.refreshAccessToken(accountTokenInfo.userId, accountTokenInfo.accountId, accountTokenInfo.refreshToken);\nconst newToken = await this.redisService.get(`twitter:accessToken:${accountId}`);\nif (!newToken || !newToken.access_token) {\n  throw new BadRequestException('刷新令牌后未能获取访问令牌');\n}\n// after: use the refresh result directly, fall back to Redis\nconst newToken = refreshResult?.access_token\n  ? refreshResult\n  : await this.redisService.get(`twitter:accessToken:${accountId}`);\nif (!newToken?.access_token) {\n  throw new BadRequestException('刷新令牌后未能获取访问令牌');\n}","handlingStrategy":"retry","validationCode":"// Confirm the Redis key exists and has a TTL before relying on the cache\nconst ttl = await redis.ttl(`twitter:accessToken:${accountId}`);\nif (ttl < 0) console.warn(`twitter:accessToken:${accountId} missing from Redis; a refresh will occur`);","typeGuard":"interface TwitterToken { access_token: string; refresh_token?: string; expires_in?: number }\nfunction isValidToken(t: unknown): t is TwitterToken {\n  return !!t && typeof t === 'object' && typeof (t as TwitterToken).access_token === 'string' && (t as TwitterToken).access_token.length > 0;\n}","tryCatchPattern":"try {\n  return await twitterAuthService.getUserAccessToken(accountId);\n} catch (e) {\n  if (e instanceof BadRequestException && e.message === '刷新令牌后未能获取访问令牌') {\n    await sleep(200); // brief retry to absorb Redis replication/visibility delay\n    return await twitterAuthService.getUserAccessToken(accountId);\n  }\n  throw e;\n}","preventionTips":["Point all app instances at the same shared Redis instance/DB","Avoid Redis eviction policies (allkeys-lru) that can drop token keys under memory pressure","Log the Twitter refresh HTTP response and alert when access_token is absent","Refactor to use the refresh call's return value directly instead of re-reading Redis"],"tags":["redis","oauth","twitter","token-refresh"],"backgroundTag":"token-refresh-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}