zeroclaw-labs/zeroclaw · error · anyhow::Error

Twitter create tweet failed ({status}): {err}

Error message

Twitter create tweet failed ({status}): {err}

What it means

Raised by TwitterChannel::create_tweet when POST /2/tweets answers non-2xx. send() splits content into 280-char chunks and calls create_tweet per chunk in a reply thread, so a mid-thread failure surfaces this error after earlier chunks already posted.

Source

Thrown at crates/zeroclaw-channels/src/twitter.rs:137

    ) -> anyhow::Result<String> {
        let mut body = json!({ "text": text });

        if let Some(reply_id) = reply_tweet_id {
            body["reply"] = json!({ "in_reply_to_tweet_id": reply_id });
        }

        let resp = self
            .http_client()
            .post(format!("{TWITTER_API_BASE}/tweets"))
            .bearer_auth(&self.bearer_token)
            .json(&body)
            .send()
            .await?;

        if !resp.status().is_success() {
            let status = resp.status();
            let err = resp.text().await.unwrap_or_default();
            anyhow::bail!("Twitter create tweet failed ({status}): {err}");
        }

        let data: serde_json::Value = resp.json().await?;
        let tweet_id = data
            .get("data")
            .and_then(|d| d.get("id"))
            .and_then(|id| id.as_str())
            .unwrap_or("")
            .to_string();

        Ok(tweet_id)
    }

    /// Send a DM to a user.
    async fn send_dm(&self, recipient_id: &str, text: &str) -> anyhow::Result<()> {
        let body = json!({
            "text": text,
        });

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. For 403 scope errors, regenerate the token with tweet.write and retry.
  2. For 429, back off to the next rate window (minutes-scale for posting caps) before re-sending.
  3. For duplicate-content 403s, vary the text or drop the redundant send.
  4. For reply failures, confirm the parent tweet id still exists and the account is not protected.

Example fix

# before — read-only token; GET /users/me works, POST /2/tweets returns 403
[channels.twitter.bot]
bearer_token = "AAAA%read-only"

# after — token with tweet.read + tweet.write (and dm.write for DMs)
[channels.twitter.bot]
bearer_token = "AAAA%read-write"
Defensive patterns

Strategy: retry

Validate before calling

// Client-side guard matching the channel's own 280-char chunking.
fn tweet_chunks_ok(content: &str) -> bool {
    // send() already chunks at 280; just avoid pathological inputs up front
    !content.trim().is_empty()
}

Try / catch

match twitter.send(&msg).await {
    Ok(()) => Ok(()),
    Err(err) => {
        let msg_text = err.to_string();
        if msg_text.contains("Twitter create tweet failed (429)") {
            tokio::time::sleep(Duration::from_secs(15 * 60)).await; // posting window
            twitter.send(&msg).await
        } else if msg_text.contains("(403)") {
            Err(err.context("token lacks tweet.write, duplicate content, or protected/deleted parent"))
        } else {
            Err(err)
        }
    }
}

Prevention

When it happens

Trigger: 403 missing the tweet.write scope or posting against a write-protected/deactivated account; 429 posting rate limit (per-user and app-level caps); 403 duplicate-content rejection when re-posting identical text; 404 when the reply.in_reply_to_tweet_id target was deleted.

Common situations: The app token has read scopes only, so reads work but every send fails with 403. Bots re-posting identical content hit the duplicate rule. Long agent replies that thread multiple chunks hit the per-window post cap mid-thread.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/457c7db37e076a3b. Report an issue: GitHub.