moeru-ai/airi · error

Failed to post tweet: ${errorToMessage(error)}

Error message

Failed to post tweet: ${errorToMessage(error)}

What it means

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.

Source

Thrown at integrations/twitter-services/src/core/services/tweet.ts:263

        }

        // If we couldn't get the ID from the toast, check the current URL
        if (!tweetId) {
          const url = await page.url()
          const match = url.match(/\/status\/(\d+)/)
          tweetId = match?.[1] || ''
        }
      }
      catch {
        // If we fail to get the ID, generate a temporary one
        tweetId = `temp-${Date.now()}`
      }

      return tweetId
    }
    catch (error: unknown) {
      console.error('Error posting tweet:', error)
      throw new Error(`Failed to post tweet: ${errorToMessage(error)}`)
    }
  }

  /**
   * Gets detailed information about a specific tweet
   */
  async function getTweetDetails(tweetId: string): Promise<TweetDetail> {
    try {
      const page = ctx.page
      await page.goto(`${TWITTER_BASE_URL}/i/status/${tweetId}`)
      await page.waitForSelector(SELECTORS.TIMELINE.TWEET)

      // Get the main tweet element
      const tweetElement = await page.$(SELECTORS.TIMELINE.TWEET)
      if (!tweetElement) {
        throw new Error('Tweet element not found')
      }

View on GitHub (pinned to 27111382b4)

Solutions

  1. Validate the content length against Twitter's current limit before calling postTweet.
  2. Confirm the compose and submit selectors still resolve on the live site.
  3. Check for an active session and that no rate-limit/challenge banner is present before composing.
  4. Log the underlying error (already done via console.error) and inspect whether it is a TimeoutError vs navigation error to target the fix.

Example fix

// before
await page.goto(TWITTER_HOME_URL)
// ... compose and post, rely on inner regex only

// after
await page.goto(TWITTER_HOME_URL)
await page.waitForSelector(SELECTORS.COMPOSE_BOX, { timeout: 10000 })
if (content.length > MAX_TWEET_LENGTH) {
  throw new Error(`Content exceeds ${MAX_TWEET_LENGTH} characters`)
}
Defensive patterns

Strategy: validation

Validate before calling

const MAX_LEN = 280
function validateTweetContent(content: string) {
  if (!content || content.length === 0) throw new Error('Content is empty')
  if (content.length > MAX_LEN) throw new Error(`Content is ${content.length} chars; max ${MAX_LEN}`)
}
// call before postTweet

Try / catch

try {
  return await postTweet(content, options)
}
catch (err) {
  if (/limit|too many/i.test((err as Error).message)) {
    // back off; do not retry immediately
    throw new Error('Rate limited; retry later')
  }
  throw err
}

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/50c9b47461da75b5. Report an issue: GitHub.