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
- For 403 scope errors, regenerate the token with tweet.write and retry.
- For 429, back off to the next rate window (minutes-scale for posting caps) before re-sending.
- For duplicate-content 403s, vary the text or drop the redundant send.
- 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
- Issue tokens with tweet.write from day one; read-only tokens fail every send with 403.
- Because threaded replies post chunk-by-chunk, a mid-thread failure leaves partial posts — prefer idempotent reply flows and check thread state before retrying.
- Rate-limit outbound posting in your own scheduler (token bucket) instead of relying on 429 feedback.
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
- Twitter users/me failed ({status}): {err}
- Twitter DM send failed ({status}): {err}
- bot info request failed: status={status}, body={body}
- Embedding API error {status}: {text}
- Composio v3 API error: {err}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/457c7db37e076a3b.
Report an issue: GitHub.