{"record":{"id":"50c9b47461da75b5","repo":"moeru-ai/airi","slug":"failed-to-post-tweet-errortomessage-error","errorCode":null,"errorMessage":"Failed to post tweet: ${errorToMessage(error)}","messagePattern":"Failed to post tweet: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"integrations/twitter-services/src/core/services/tweet.ts","lineNumber":263,"sourceCode":"        }\n\n        // If we couldn't get the ID from the toast, check the current URL\n        if (!tweetId) {\n          const url = await page.url()\n          const match = url.match(/\\/status\\/(\\d+)/)\n          tweetId = match?.[1] || ''\n        }\n      }\n      catch {\n        // If we fail to get the ID, generate a temporary one\n        tweetId = `temp-${Date.now()}`\n      }\n\n      return tweetId\n    }\n    catch (error: unknown) {\n      console.error('Error posting tweet:', error)\n      throw new Error(`Failed to post tweet: ${errorToMessage(error)}`)\n    }\n  }\n\n  /**\n   * Gets detailed information about a specific tweet\n   */\n  async function getTweetDetails(tweetId: string): Promise<TweetDetail> {\n    try {\n      const page = ctx.page\n      await page.goto(`${TWITTER_BASE_URL}/i/status/${tweetId}`)\n      await page.waitForSelector(SELECTORS.TIMELINE.TWEET)\n\n      // Get the main tweet element\n      const tweetElement = await page.$(SELECTORS.TIMELINE.TWEET)\n      if (!tweetElement) {\n        throw new Error('Tweet element not found')\n      }\n","sourceCodeStart":245,"sourceCodeEnd":281,"githubUrl":"https://github.com/moeru-ai/airi/blob/27111382b4a79a7e983289d6e983a06af185ed0f/integrations/twitter-services/src/core/services/tweet.ts#L245-L281","documentation":"This is the top-level catch for postTweet, which composes and submits a tweet via browser automation against Twitter's web compose UI. Any failure in navigation, selector interaction, or post-URL scraping is normalized through errorToMessage and rethrown. The inner block already tolerates a missing tweet ID by falling back to a temp-<timestamp> id, so this throw means something earlier in the flow failed.","triggerScenarios":"The compose textbox or post button selector changed so page.type/click throws; the tweet content exceeds Twitter's length limit and the post button stays disabled; a rate-limit or challenge interstitial appears after clicking post; the regex extracting the tweet id from the resulting URL does not match the new URL shape (caught, but a navigation error before that point is not).","commonSituations":"Twitter A/B tests the compose UI; bot hits the daily post limit; media attachment flow diverges; CI runs without a valid logged-in profile so the compose box never appears.","solutions":["Validate the content length against Twitter's current limit before calling postTweet.","Confirm the compose and submit selectors still resolve on the live site.","Check for an active session and that no rate-limit/challenge banner is present before composing.","Log the underlying error (already done via console.error) and inspect whether it is a TimeoutError vs navigation error to target the fix."],"exampleFix":"// before\nawait page.goto(TWITTER_HOME_URL)\n// ... compose and post, rely on inner regex only\n\n// after\nawait page.goto(TWITTER_HOME_URL)\nawait page.waitForSelector(SELECTORS.COMPOSE_BOX, { timeout: 10000 })\nif (content.length > MAX_TWEET_LENGTH) {\n  throw new Error(`Content exceeds ${MAX_TWEET_LENGTH} characters`)\n}","handlingStrategy":"validation","validationCode":"const MAX_LEN = 280\nfunction validateTweetContent(content: string) {\n  if (!content || content.length === 0) throw new Error('Content is empty')\n  if (content.length > MAX_LEN) throw new Error(`Content is ${content.length} chars; max ${MAX_LEN}`)\n}\n// call before postTweet","typeGuard":null,"tryCatchPattern":"try {\n  return await postTweet(content, options)\n}\ncatch (err) {\n  if (/limit|too many/i.test((err as Error).message)) {\n    // back off; do not retry immediately\n    throw new Error('Rate limited; retry later')\n  }\n  throw err\n}","preventionTips":["Pre-validate content length and media against current Twitter limits.","Confirm the compose-box selectors before posting in CI.","Log the underlying cause so a selector vs rate-limit failure is distinguishable."],"tags":["browser-automation","twitter","posting","rate-limit","dom-selector"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}