{"record":{"id":"b8b982f61bf2352f","repo":"yikart/AiToEarn","slug":"error-response-data-error-error-m","errorCode":null,"errorMessage":"获取推文详情失败: ${error.response?.data?.error || error.message}","messagePattern":"获取推文详情失败: (.+?)","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"project/aitoearn-electron/server/src/modules/plat/twitter/twitter.service.ts","lineNumber":239,"sourceCode":"      const params = {\n        'tweet.fields': 'created_at,public_metrics,text,source',\n        'expansions': 'attachments.media_keys,author_id',\n        'media.fields': 'url,preview_image_url,type'\n      };\n\n      const response = await lastValueFrom(\n        this.httpService.get(url, {\n          params,\n          headers: {\n            'Authorization': `Bearer ${accessToken}`\n          }\n        })\n      );\n\n      return response.data.data\n    } catch (error) {\n      this.logger.error(`获取推文详情失败: ${error.message}`, error.stack);\n      throw new BadRequestException(`获取推文详情失败: ${error.response?.data?.error || error.message}`);\n    }\n  }\n\n  /**\n   * 获取推文的统计数据\n   * @param userId 用户ID\n   * @param accountId Twitter账号ID\n   * @param tweetId 推文ID\n   * @returns 推文统计数据\n   */\n  async getTweetMetrics(accessToken, userId: string, accountId: string, tweetId: string) {\n    try {\n\n      const url = `${TWITTER_API_V2_URL}/tweets/${tweetId}`;\n      const params = {\n        'tweet.fields': 'public_metrics,non_public_metrics,organic_metrics', // 注意：某些指标需要高级API访问权限\n      };\n","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/twitter/twitter.service.ts#L221-L257","documentation":"getTweetDetail calls GET /2/tweets/:tweetId with tweet.field/expansion query params using a Bearer token. Axios errors from Twitter API v2 are wrapped in a BadRequestException whose message prefers Twitter's error.response.data.error detail over error.message.","triggerScenarios":"Requesting a nonexistent or deleted tweetId (404), expired token (401), tweet not visible to the authorizing account (403 protected/private tweet), or malformed tweetId.","commonSituations":"Deleted tweets being re-fetched for analytics; private/protected accounts whose tweets the app can't read; API tier lacking access; expired cached access token; id passed as a float losing precision in JavaScript.","solutions":["Refresh the user access token when the message contains a 401/Unauthorized detail.","Keep tweet IDs as strings end-to-end to avoid precision loss.","Check tweet existence first with GET /2/tweets/:id and handle 404 gracefully.","Confirm the token's scopes (tweet.read) and that the account can view the tweet.","Read the full error.response.data.errors array for Twitter's exact error title/parameters."],"exampleFix":"// before\nconst detail = await twitterService.getTweetDetail(accessToken, userId, accountId, tweetId)\n// after\nif (!/^[0-9]+$/.test(String(tweetId))) throw new Error('invalid tweetId')\nconst detail = await twitterService.getTweetDetail(accessToken, userId, accountId, String(tweetId)).catch(e => null)\nif (!detail) return null // tweet deleted or inaccessible","handlingStrategy":"try-catch","validationCode":"if (!/^[0-9]+$/.test(String(tweetId))) throw new Error('tweetId must be a numeric string')","typeGuard":"const isAxiosLike = (e: unknown): e is { response?: { status?: number; data?: { error?: string } } } =>\n  typeof e === 'object' && e !== null && 'response' in e","tryCatchPattern":"try {\n  return await twitterService.getTweetDetail(accessToken, userId, accountId, tweetId)\n} catch (e) {\n  if (isAxiosLike(e) && e.response?.status === 404) return null\n  if (isAxiosLike(e) && e.response?.status === 401) return retryWithFreshToken()\n  throw e\n}","preventionTips":["Handle 404 as 'tweet deleted' rather than an application error","Refresh tokens proactively based on expires_in tracking","Verify read scope (tweet.read) is granted","Never convert tweet IDs through Number()"],"tags":["twitter-api","http-4xx","oauth","nestjs"],"backgroundTag":"twitter-api-request-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}