yikart/AiToEarn · error · BadRequestException

删除推文失败

Error message

删除推文失败: ${error.response?.data?.error || error.message}

What it means

deleteTweet calls DELETE https://api.twitter.com/2/tweets/:tweetId with a Bearer user token. Any Axios error from Twitter API v2 (HTTP 4xx/5xx) is caught and rethrown as a NestJS BadRequestException with the message taken from error.response.data.error (the Twitter error detail) or error.message. It is a wrapped upstream Twitter API failure, not a local validation error.

Solutions

  1. Refresh the user's Twitter access token via TwitterAuthService before retrying (401 means expired).
  2. Verify the tweetId exists and belongs to accountId by calling GET /2/tweets/:id first.
  3. Ensure the OAuth2 token was issued with tweet.delete (tweet.write) scope and is a user-context token, not app-only Bearer.
  4. Inspect error.response.data (Twitter returns details/errors arrays) in the logged stack for the exact reason.
  5. Retry with exponential backoff only for 429/5xx; treat 403/404 as permanent.

Example fix

// before
await twitterService.deleteTweet(accessToken, userId, accountId, tweetId)
// after
try {
  const freshToken = await twitterAuthService.ensureFreshToken(accountId)
  await twitterService.deleteTweet(freshToken, userId, accountId, String(tweetId))
} catch (e) {
  if (e.getStatus?.() === 400 && /not found/i.test(e.message)) return // already deleted
  throw e
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (!accessToken || !/^[0-9]+$/.test(String(tweetId))) throw new Error('invalid deleteTweet arguments')

Type guard

function hasTwitterErrorDetail(e: unknown): e is { response: { data: { error: string } } } {
  return typeof e === 'object' && e !== null && 'response' in e &&
    typeof (e as any).response?.data?.error === 'string'
}

Try / catch

try {
  await twitterService.deleteTweet(accessToken, userId, accountId, tweetId)
} catch (e) {
  const detail = e?.response?.data?.error || e.message
  if (/401|unauthorized/i.test(detail)) await refreshTokenAndRetry()
  else if (/not found/i.test(detail)) return // treat as deleted
  else throw new Error(`删除推文失败: ${detail}`)
}

Prevention

When it happens

Trigger: Calling deleteTweet with an expired/revoked OAuth2 user access token (401), a tweetId that does not exist or was already deleted (404 'Not found Tweet'), or an account whose app lacks tweet.delete scope / does not own the tweet (403 'Client not allowed to access this resource' or authorization error).

Common situations: Stale tokens after user re-authorizes the app; deleting a tweet that was already removed manually; using an app-only Bearer token instead of the user context token; incorrect tweetId passed from a frontend (e.g. truncated or non-numeric id); free-tier API access lacking write endpoints.

Related errors


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

Appendix: source

Thrown at project/aitoearn-electron/server/src/modules/plat/twitter/twitter.service.ts:206

  async deleteTweet(accessToken, userId: string, accountId: string, tweetId: string) {
    try {

      const url = `${TWITTER_API_V2_URL}/tweets/${tweetId}`;

      await lastValueFrom(
        this.httpService.delete(url, {
          headers: {
            'Authorization': `Bearer ${accessToken}`
          }
        })
      );

      this.logger.log(`成功删除推文: tweetId=${tweetId}, accountId=${accountId}`);

      return true
    } catch (error) {
      this.logger.error(`删除推文失败: ${error.message}`, error.stack);
      throw new BadRequestException(`删除推文失败: ${error.response?.data?.error || error.message}`);
    }
  }

  /**
   * 获取推文详情
   * @param userId 用户ID
   * @param accountId Twitter账号ID
   * @param tweetId 推文ID
   * @returns 推文详情
   */
  async getTweetDetail(accessToken, userId: string, accountId: string, tweetId: string) {
    try {

      const url = `${TWITTER_API_V2_URL}/tweets/${tweetId}`;
      const params = {
        'tweet.fields': 'created_at,public_metrics,text,source',
        'expansions': 'attachments.media_keys,author_id',
        'media.fields': 'url,preview_image_url,type'

View on GitHub (pinned to d3aa8bea5b)