Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 100 additions & 2 deletions gateway/src/adapters/telegram.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ pub async fn handle_reply(
"gateway → telegram"
);
let url = format!("{TELEGRAM_API_BASE}/bot{bot_token}/sendMessage");
let _ = client
let retry_plain = match client
.post(&url)
.json(&serde_json::json!({
"chat_id": reply.channel.id,
Expand All @@ -342,7 +342,68 @@ pub async fn handle_reply(
}))
.send()
.await
.map_err(|e| error!("telegram send error: {e}"));
{
Ok(resp) => {
match resp.json::<serde_json::Value>().await {
Ok(body) if body["ok"].as_bool() != Some(true) => {
let desc = body["description"].as_str().unwrap_or("unknown");
let code = body["error_code"].as_u64().unwrap_or(0);
if should_retry_plain_text_fallback(code, desc) {
warn!(code, desc = %desc, "telegram Markdown parse failed, retrying as plain text");
true
} else {
error!(code, desc = %desc, "telegram send failed");
false
}
}
Ok(_) => false,
Err(e) => {
warn!("telegram response not valid JSON: {e}");
false
}
}
}
Err(e) => {
error!("telegram send error: {e}");
false
}
};

if retry_plain {
match client
.post(&url)
.json(&serde_json::json!({
"chat_id": reply.channel.id,
"text": reply.content.text,
"message_thread_id": reply.channel.thread_id,
}))
.send()
.await
{
Ok(resp) => {
match resp.json::<serde_json::Value>().await {
Ok(body) if body["ok"].as_bool() != Some(true) => {
error!(
desc = %body["description"].as_str().unwrap_or("unknown"),
"telegram plain text fallback also failed"
);
}
Err(e) => warn!("telegram plain text fallback response not valid JSON: {e}"),
_ => {}
}
}
Err(e) => error!("telegram plain text send error: {e}"),
}
}
}

fn should_retry_plain_text_fallback(code: u64, description: &str) -> bool {
if code != 400 {
return false;
}

let description = description.to_ascii_lowercase();
description.contains("parse") || description.contains("entities")
}

/// Download media from Telegram via getFile → store to filesystem (colocate mode).
Expand Down Expand Up @@ -483,3 +544,40 @@ async fn download_telegram_document(
path: Some(path),
})
}

#[cfg(test)]
mod tests {
use super::should_retry_plain_text_fallback;

#[test]
fn retries_markdown_parse_errors() {
assert!(should_retry_plain_text_fallback(
400,
"Bad Request: can't parse entities: Can't find end of the entity"
));
}

#[test]
fn retries_entity_errors() {
assert!(should_retry_plain_text_fallback(
400,
"Bad Request: unsupported start tag in entities"
));
}

#[test]
fn does_not_retry_unrelated_bad_requests() {
assert!(!should_retry_plain_text_fallback(
400,
"Bad Request: message text is empty"
));
}

#[test]
fn does_not_retry_non_bad_request_errors() {
assert!(!should_retry_plain_text_fallback(
429,
"Too Many Requests: retry after 10"
));
}
}
Loading