zeroclaw-labs/zeroclaw · error · anyhow::Error
{method} failed: status={status}, body={body}
Error message
{method} failed: status={status}, body={body} What it means
The voice-note upload path builds a multipart POST where method/field/mime come from telegram_audio_send_spec("opus") (sendVoice with OGG/Opus) and bails on any non-2xx, embedding the HTTP status and Telegram response body. The body's `description` field names the exact Bot API rejection.
Source
Thrown at crates/zeroclaw-channels/src/telegram.rs:1503
let mut form = reqwest::multipart::Form::new()
.text("chat_id", chat_id.to_string())
.part(
field,
reqwest::multipart::Part::bytes(audio_bytes)
.file_name(filename)
.mime_str(mime)?,
);
if let Some(tid) = thread_id {
form = form.text("message_thread_id", tid.to_string());
}
let resp = client.post(&url).multipart(form).send().await?;
if !resp.status().is_success() {
let status = resp.status();
let body = resp.text().await.unwrap_or_default();
anyhow::bail!("{method} failed: status={status}, body={body}");
}
::zeroclaw_log::record!(
INFO,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
.with_attrs(::serde_json::json!({"audio_len": audio_len})),
"sent voice note ( bytes)"
);
Ok(())
}
async fn classify_edit_message_response(resp: reqwest::Response) -> EditMessageResult {
if resp.status().is_success() {
return EditMessageResult::Success;
}
let status = resp.status();
let body = resp.text().await.unwrap_or_default();View on GitHub (pinned to 88bb9c8533)
Solutions
- Read the body= field first — Telegram's `description` names the concrete problem.
- On 401/404, verify the bot token with getMe; the token is wrong or revoked.
- Confirm the upload really is OGG/Opus (the spec assumes synthesize_opus output); re-check the ffmpeg transcode step.
- On 429, honor retry_after in the body and resend the same multipart request once after the delay.
Example fix
// before
let resp = client.post(&url).multipart(form).send().await?;
// after
let resp = client.post(&url).multipart(form).send().await?;
if resp.status().as_u16() == 429 {
let body: serde_json::Value = resp.json().await?;
let wait = body["parameters"]["retry_after"].as_u64().unwrap_or(1);
tokio::time::sleep(Duration::from_secs(wait)).await;
// resend once
} Defensive patterns
Strategy: try-catch
Validate before calling
const VOICE_LIMIT: u64 = 50 * 1024 * 1024;
let meta = tokio::fs::metadata(&ogg_path).await?;
if meta.len() > VOICE_LIMIT {
anyhow::bail!("voice file {}MB exceeds 50MB", meta.len() / 1024 / 1024);
} Type guard
fn telegram_retry_after(err: &anyhow::Error) -> Option<u64> {
err.to_string().contains("429")
.then(|| err.to_string().split("retry_after").nth(1)?.parse().ok())
.flatten()
} Try / catch
if let Err(e) = send_voice_note(&chat, &path).await {
if let Some(secs) = telegram_retry_after(&e) {
tokio::time::sleep(Duration::from_secs(secs)).await;
send_voice_note(&chat, &path).await?;
} else {
return Err(e);
}
} Prevention
- Always route voice bytes through the same OGG/Opus transcode so mime and format match telegram_audio_send_spec.
- Honor retry_after on 429 and pace outbound voice messages.
- Verify the bot token with getMe at startup so 401s fail fast.
When it happens
Trigger: 400 'wrong type of the current file' / duration errors when bytes are not valid OGG/Opus; 401 Unauthorized from a wrong bot_token; 413 when audio exceeds Telegram's voice size limit; 429 flood control (retry_after in body); 400 chat not found or 403 bot blocked by the user.
Common situations: ffmpeg producing a non-Opus container after a codec change; token belonging to a different bot than the chat; bursts of voice replies hitting per-chat rate limits; long replies exceeding voice-duration expectations.
Related errors
- Telegram sendVoice failed: {err}
- Telegram file download failed: {}
- Telegram sendMessage failed (markdown {}: {}; plain {}: {})
- {method} by URL failed: {err}
- Telegram sendDocument failed: {err}
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/cfc6de84a2ece116.
Report an issue: GitHub.