From 293a700b69f70e8fe51f354298e3fe4f37a607a5 Mon Sep 17 00:00:00 2001 From: Zaki Manian Date: Sun, 1 Mar 2026 16:36:26 -0800 Subject: [PATCH] fix: prevent Telegram 409 Conflict on webhook re-registration (#447) * fix: prevent Telegram 409 Conflict on webhook re-registration Delete any existing webhook before calling setWebhook in on_start(), matching the defensive cleanup that polling mode already does. As a safety net, register_webhook() now retries once on 409 after calling delete_webhook(). Closes #440 Co-Authored-By: Claude Opus 4.6 * refactor: deduplicate 409 retry logic in register_webhook Restructure the match block so the initial request and retry share a single response-handling code path. Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- channels-src/telegram/src/lib.rs | 82 +++++++++++++++++++++----------- 1 file changed, 55 insertions(+), 27 deletions(-) diff --git a/channels-src/telegram/src/lib.rs b/channels-src/telegram/src/lib.rs index 92c078bd..22f2facf 100644 --- a/channels-src/telegram/src/lib.rs +++ b/channels-src/telegram/src/lib.rs @@ -376,6 +376,9 @@ impl Guest for TelegramChannel { // Register webhook with Telegram API — propagate errors so a bad token // causes activation to fail rather than silently succeeding. if let Some(ref tunnel_url) = config.tunnel_url { + // Clear any stale webhook first to avoid 409 Conflict + let _ = delete_webhook(); + channel_host::log( channel_host::LogLevel::Info, &format!("Registering webhook: {}/webhook/telegram", tunnel_url), @@ -894,36 +897,61 @@ fn register_webhook(tunnel_url: &str, webhook_secret: Option<&str>) -> Result<() None, ); - match result { - Ok(response) => { - if response.status != 200 { - let body_str = String::from_utf8_lossy(&response.body); - return Err(format!("HTTP {}: {}", response.status, body_str)); - } + let mut response = match result { + Ok(response) => response, + Err(e) => return Err(format!("HTTP request failed: {}", e)), + }; - // Parse Telegram API response - let api_response: TelegramApiResponse = - serde_json::from_slice(&response.body) - .map_err(|e| format!("Failed to parse response: {}", e))?; + let mut retried = false; + if response.status == 409 { + channel_host::log( + channel_host::LogLevel::Warn, + "409 Conflict -- deleting existing webhook and retrying", + ); + let _ = delete_webhook(); + retried = true; - if !api_response.ok { - return Err(format!( - "Telegram API error: {}", - api_response - .description - .unwrap_or_else(|| "unknown".to_string()) - )); - } - - channel_host::log( - channel_host::LogLevel::Info, - &format!("Webhook registered successfully: {}", webhook_url), - ); - - Ok(()) - } - Err(e) => Err(format!("HTTP request failed: {}", e)), + response = match channel_host::http_request( + "POST", + "https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/setWebhook", + &headers.to_string(), + Some(&body_bytes), + None, + ) { + Ok(resp) => resp, + Err(e) => return Err(format!("HTTP request failed (after 409 retry): {}", e)), + }; } + + if response.status != 200 { + let body_str = String::from_utf8_lossy(&response.body); + let context = if retried { " (after 409 retry)" } else { "" }; + return Err(format!("HTTP {}{}: {}", response.status, context, body_str)); + } + + // Parse Telegram API response + let api_response: TelegramApiResponse = + serde_json::from_slice(&response.body) + .map_err(|e| format!("Failed to parse response: {}", e))?; + + if !api_response.ok { + let context = if retried { " (after 409 retry)" } else { "" }; + return Err(format!( + "Telegram API error{}: {}", + context, + api_response + .description + .unwrap_or_else(|| "unknown".to_string()) + )); + } + + let context = if retried { " (after retry)" } else { "" }; + channel_host::log( + channel_host::LogLevel::Info, + &format!("Webhook registered successfully{}: {}", context, webhook_url), + ); + + Ok(()) } // ============================================================================