{"record":{"id":"e70fd76511d7d6e4","repo":"yikart/AiToEarn","slug":"error-response-data-error-error-m-e70fd7","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":370,"sourceCode":"   */\n  async unlikeTweet(accessToken: string, userId: string, accountId: string, tweetId: string) {\n    try {\n      // Twitter API V2 取消点赞端点\n      const url = `${TWITTER_API_V2_URL}/users/${accountId}/likes/${tweetId}`;\n\n      const response = await lastValueFrom(\n        this.httpService.delete(url, {\n          headers: {\n            'Authorization': `Bearer ${accessToken}`\n          }\n        })\n      );\n\n      this.logger.log(`成功取消点赞推文: userId=${userId}, accountId=${accountId}, tweetId=${tweetId}`);\n      return response.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","sourceCodeStart":352,"sourceCodeEnd":375,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-electron/server/src/modules/plat/twitter/twitter.service.ts#L352-L375","documentation":"unlikeTweet calls DELETE /2/users/:userId/likes/:tweetId with a Bearer token. Any Twitter API v2 error returned via Axios is rethrown as a BadRequestException using error.response.data.error or error.message.","triggerScenarios":"Tweet not previously liked (404 'user has not liked tweet'), tweet deleted (404), expired/revoked token (401), or path userId not matching the token user (403).","commonSituations":"Bulk unlike jobs running against likes already removed; account re-authorization losing tweet.write scope; stale cached tokens; wrong userId argument order when calling the service.","solutions":["Treat 404 'not liked' as success for idempotent unlike flows.","Refresh the user access token on 401 before retrying.","Confirm the path userId matches the authenticated token owner.","Ensure the token was granted tweet.write scope at authorization time.","Log full error.response.data.errors to distinguish rate limit (429) from permission (403) failures."],"exampleFix":"// before\nawait twitterService.unlikeTweet(accessToken, userId, otherUserId, tweetId)\n// after\ntry {\n  await twitterService.unlikeTweet(accessToken, userId, userId, String(tweetId))\n} catch (e) {\n  if (/has not liked|not found/i.test(e.message)) return // already unliked\n  throw e\n}","handlingStrategy":"try-catch","validationCode":"if (!accessToken || userId !== tokenOwnerId) throw new Error('unlikeTweet requires token-owner userId')","typeGuard":"const isNotLiked = (msg: string): boolean => /has not liked|not found/i.test(msg)","tryCatchPattern":"try {\n  await twitterService.unlikeTweet(accessToken, userId, userId, String(tweetId))\n} catch (e) {\n  const msg = e?.response?.data?.error || e.message\n  if (isNotLiked(msg)) return // already unliked: idempotent\n  if (/401/.test(msg)) return retryAfterRefresh()\n  throw e\n}","preventionTips":["Design unlike flows to tolerate 'not liked' 404 responses","Match the path userId to the authenticated user, never a different account","Refresh tokens before bulk unlike jobs","Log error.response.data.errors for precise Twitter error codes"],"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"}