Compare commits

..

3 commits

Author SHA1 Message Date
MHSanaei
5b796672e9
Improve telego client robustness and retries
Some checks failed
Release 3X-UI / build (386) (push) Has been cancelled
Release 3X-UI / build (amd64) (push) Has been cancelled
Release 3X-UI / build (arm64) (push) Has been cancelled
Release 3X-UI / build (armv5) (push) Has been cancelled
Release 3X-UI / build (armv6) (push) Has been cancelled
Release 3X-UI / build (armv7) (push) Has been cancelled
Release 3X-UI / build (s390x) (push) Has been cancelled
Release 3X-UI / Build for Windows (push) Has been cancelled
Add a createRobustFastHTTPClient helper to configure fasthttp.Client with better timeouts, connection limits, retries and optional SOCKS5 proxy dialing. Validate and sanitize proxy and API server URLs instead of returning early on invalid values, and build telego.Bot options dynamically. Reduce long-polling timeout to detect connection issues faster and adjust update retrieval comments. Implement exponential-backoff retry logic for SendMessage calls to handle transient connection/timeouts and improve delivery reliability; also reduce inter-message delay for better throughput.
2026-02-14 22:49:19 +01:00
MHSanaei
3fa0da38c9
Add timeouts and delays to backup sends
Add rate-limit friendly delays and context timeouts when sending backups via Telegram. Iterate admin IDs with index to sleep 1s between sends; add 30s context.WithTimeout for each SendDocument call and defer file.Close() for opened files; insert a 500ms pause between sending DB and config files. These changes improve resource cleanup and reduce chance of Telegram rate-limit/timeout failures.
2026-02-14 22:31:41 +01:00
MHSanaei
8eb1225734
translate bug fix #3789 2026-02-14 21:41:20 +01:00
16 changed files with 207 additions and 57 deletions

View file

@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"net" "net"
"reflect" "reflect"
"strconv" "strconv"
"strings" "strings"
@ -719,25 +719,25 @@ func (s *SettingService) GetDefaultXrayConfig() (any, error) {
} }
func extractHostname(host string) string { func extractHostname(host string) string {
h, _, err := net.SplitHostPort(host) h, _, err := net.SplitHostPort(host)
// Err is not nil means host does not contain port // Err is not nil means host does not contain port
if err != nil { if err != nil {
h = host h = host
} }
ip := net.ParseIP(h) ip := net.ParseIP(h)
// If it's not an IP, return as is // If it's not an IP, return as is
if ip == nil { if ip == nil {
return h return h
} }
// If it's an IPv4, return as is // If it's an IPv4, return as is
if ip.To4() != nil { if ip.To4() != nil {
return h return h
} }
// IPv6 needs bracketing // IPv6 needs bracketing
return "[" + h + "]" return "[" + h + "]"
} }
func (s *SettingService) GetDefaultSettings(host string) (any, error) { func (s *SettingService) GetDefaultSettings(host string) (any, error) {

View file

@ -272,41 +272,78 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
return nil return nil
} }
// createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
client := &fasthttp.Client{
// Connection timeouts
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxIdleConnDuration: 60 * time.Second,
MaxConnDuration: 0, // unlimited, but controlled by MaxIdleConnDuration
MaxIdemponentCallAttempts: 3,
ReadBufferSize: 4096,
WriteBufferSize: 4096,
MaxConnsPerHost: 100,
MaxConnWaitTimeout: 10 * time.Second,
DisableHeaderNamesNormalizing: false,
DisablePathNormalizing: false,
// Retry on connection errors
RetryIf: func(request *fasthttp.Request) bool {
// Retry on connection errors for GET requests
return string(request.Header.Method()) == "GET" || string(request.Header.Method()) == "POST"
},
}
// Set proxy if provided
if proxyUrl != "" {
client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)
}
return client
}
// NewBot creates a new Telegram bot instance with optional proxy and API server settings. // NewBot creates a new Telegram bot instance with optional proxy and API server settings.
func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) { func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
if proxyUrl == "" && apiServerUrl == "" { // Validate proxy URL if provided
return telego.NewBot(token)
}
if proxyUrl != "" { if proxyUrl != "" {
if !strings.HasPrefix(proxyUrl, "socks5://") { if !strings.HasPrefix(proxyUrl, "socks5://") {
logger.Warning("Invalid socks5 URL, using default") logger.Warning("Invalid socks5 URL, ignoring proxy")
return telego.NewBot(token) proxyUrl = "" // Clear invalid proxy
} else {
_, err := url.Parse(proxyUrl)
if err != nil {
logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err)
proxyUrl = ""
}
} }
}
_, err := url.Parse(proxyUrl) // Validate API server URL if provided
if err != nil { if apiServerUrl != "" {
logger.Warningf("Can't parse proxy URL, using default instance for tgbot: %v", err) if !strings.HasPrefix(apiServerUrl, "http") {
return telego.NewBot(token) logger.Warning("Invalid http(s) URL for API server, using default")
apiServerUrl = ""
} else {
_, err := url.Parse(apiServerUrl)
if err != nil {
logger.Warningf("Can't parse API server URL, using default: %v", err)
apiServerUrl = ""
}
} }
return telego.NewBot(token, telego.WithFastHTTPClient(&fasthttp.Client{
Dial: fasthttpproxy.FasthttpSocksDialer(proxyUrl),
}))
} }
if !strings.HasPrefix(apiServerUrl, "http") { // Create robust fasthttp client
logger.Warning("Invalid http(s) URL, using default") client := t.createRobustFastHTTPClient(proxyUrl)
return telego.NewBot(token)
// Build bot options
var options []telego.BotOption
options = append(options, telego.WithFastHTTPClient(client))
if apiServerUrl != "" {
options = append(options, telego.WithAPIServer(apiServerUrl))
} }
_, err := url.Parse(apiServerUrl) return telego.NewBot(token, options...)
if err != nil {
logger.Warningf("Can't parse API server URL, using default instance for tgbot: %v", err)
return telego.NewBot(token)
}
return telego.NewBot(token, telego.WithAPIServer(apiServerUrl))
} }
// IsRunning checks if the Telegram bot is currently running. // IsRunning checks if the Telegram bot is currently running.
@ -390,7 +427,7 @@ func (t *Tgbot) decodeQuery(query string) (string, error) {
// OnReceive starts the message receiving loop for the Telegram bot. // OnReceive starts the message receiving loop for the Telegram bot.
func (t *Tgbot) OnReceive() { func (t *Tgbot) OnReceive() {
params := telego.GetUpdatesParams{ params := telego.GetUpdatesParams{
Timeout: 30, // Increased timeout to reduce API calls Timeout: 20, // Reduced timeout to detect connection issues faster
} }
// Strict singleton: never start a second long-polling loop. // Strict singleton: never start a second long-polling loop.
tgBotMutex.Lock() tgBotMutex.Lock()
@ -408,7 +445,7 @@ func (t *Tgbot) OnReceive() {
botWG.Add(1) botWG.Add(1)
tgBotMutex.Unlock() tgBotMutex.Unlock()
// Get updates channel using the context. // Get updates channel using the context with shorter timeout for better error recovery
updates, _ := bot.UpdatesViaLongPolling(ctx, &params) updates, _ := bot.UpdatesViaLongPolling(ctx, &params)
go func() { go func() {
defer botWG.Done() defer botWG.Done()
@ -2247,10 +2284,36 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if len(replyMarkup) > 0 && n == (len(allMessages)-1) { if len(replyMarkup) > 0 && n == (len(allMessages)-1) {
params.ReplyMarkup = replyMarkup[0] params.ReplyMarkup = replyMarkup[0]
} }
_, err := bot.SendMessage(context.Background(), &params)
if err != nil { // Retry logic with exponential backoff for connection errors
logger.Warning("Error sending telegram message :", err) maxRetries := 3
for attempt := range maxRetries {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
_, err := bot.SendMessage(ctx, &params)
cancel()
if err == nil {
break // Success
}
// Check if error is a connection error
errStr := err.Error()
isConnectionError := strings.Contains(errStr, "connection") ||
strings.Contains(errStr, "timeout") ||
strings.Contains(errStr, "closed")
if isConnectionError && attempt < maxRetries-1 {
// Exponential backoff: 1s, 2s, 4s
backoff := time.Duration(1<<uint(attempt)) * time.Second
logger.Warningf("Connection error sending telegram message (attempt %d/%d), retrying in %v: %v",
attempt+1, maxRetries, backoff, err)
time.Sleep(backoff)
} else {
logger.Warning("Error sending telegram message:", err)
break
}
} }
// Reduced delay to improve performance (only needed for rate limiting) // Reduced delay to improve performance (only needed for rate limiting)
if n < len(allMessages)-1 { // Only delay between messages, not after the last one if n < len(allMessages)-1 { // Only delay between messages, not after the last one
time.Sleep(100 * time.Millisecond) time.Sleep(100 * time.Millisecond)
@ -2585,8 +2648,12 @@ func (t *Tgbot) SendBackupToAdmins() {
if !t.IsRunning() { if !t.IsRunning() {
return return
} }
for _, adminId := range adminIds { for i, adminId := range adminIds {
t.sendBackup(int64(adminId)) t.sendBackup(int64(adminId))
// Add delay between sends to avoid Telegram rate limits
if i < len(adminIds)-1 {
time.Sleep(1 * time.Second)
}
} }
} }
@ -3596,13 +3663,17 @@ func (t *Tgbot) sendBackup(chatId int64) {
logger.Error("Error in trigger a checkpoint operation: ", err) logger.Error("Error in trigger a checkpoint operation: ", err)
} }
// Send database backup
file, err := os.Open(config.GetDBPath()) file, err := os.Open(config.GetDBPath())
if err == nil { if err == nil {
defer file.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
document := tu.Document( document := tu.Document(
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(context.Background(), document) _, err = bot.SendDocument(ctx, document)
if err != nil { if err != nil {
logger.Error("Error in uploading backup: ", err) logger.Error("Error in uploading backup: ", err)
} }
@ -3610,13 +3681,20 @@ func (t *Tgbot) sendBackup(chatId int64) {
logger.Error("Error in opening db file for backup: ", err) logger.Error("Error in opening db file for backup: ", err)
} }
// Small delay between file sends
time.Sleep(500 * time.Millisecond)
// Send config.json backup
file, err = os.Open(xray.GetConfigPath()) file, err = os.Open(xray.GetConfigPath())
if err == nil { if err == nil {
defer file.Close()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
document := tu.Document( document := tu.Document(
tu.ID(chatId), tu.ID(chatId),
tu.File(file), tu.File(file),
) )
_, err = bot.SendDocument(context.Background(), document) _, err = bot.SendDocument(ctx, document)
if err != nil { if err != nil {
logger.Error("Error in uploading config.json: ", err) logger.Error("Error in uploading config.json: ", err)
} }

View file

@ -525,6 +525,12 @@
"accountInfo" = "معلومات الحساب" "accountInfo" = "معلومات الحساب"
"outboundStatus" = "حالة المخرج" "outboundStatus" = "حالة المخرج"
"sendThrough" = "أرسل من خلال" "sendThrough" = "أرسل من خلال"
"test" = "اختبار"
"testResult" = "نتيجة الاختبار"
"testing" = "جاري اختبار الاتصال..."
"testSuccess" = "الاختبار ناجح"
"testFailed" = "فشل الاختبار"
"testError" = "فشل اختبار المخرج"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "أضف موازن تحميل" "addBalancer" = "أضف موازن تحميل"

View file

@ -525,6 +525,12 @@
"accountInfo" = "Información de la Cuenta" "accountInfo" = "Información de la Cuenta"
"outboundStatus" = "Estado de Salida" "outboundStatus" = "Estado de Salida"
"sendThrough" = "Enviar a través de" "sendThrough" = "Enviar a través de"
"test" = "Probar"
"testResult" = "Resultado de la prueba"
"testing" = "Probando conexión..."
"testSuccess" = "Prueba exitosa"
"testFailed" = "Prueba fallida"
"testError" = "Error al probar la salida"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Agregar equilibrador" "addBalancer" = "Agregar equilibrador"

View file

@ -525,6 +525,12 @@
"accountInfo" = "اطلاعات حساب" "accountInfo" = "اطلاعات حساب"
"outboundStatus" = "وضعیت خروجی" "outboundStatus" = "وضعیت خروجی"
"sendThrough" = "ارسال با" "sendThrough" = "ارسال با"
"test" = "تست"
"testResult" = "نتیجه تست"
"testing" = "در حال تست اتصال..."
"testSuccess" = "تست موفقیت‌آمیز"
"testFailed" = "تست ناموفق"
"testError" = "خطا در تست خروجی"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "افزودن بالانسر" "addBalancer" = "افزودن بالانسر"

View file

@ -525,6 +525,12 @@
"accountInfo" = "Informasi Akun" "accountInfo" = "Informasi Akun"
"outboundStatus" = "Status Keluar" "outboundStatus" = "Status Keluar"
"sendThrough" = "Kirim Melalui" "sendThrough" = "Kirim Melalui"
"test" = "Tes"
"testResult" = "Hasil Tes"
"testing" = "Menguji koneksi..."
"testSuccess" = "Tes berhasil"
"testFailed" = "Tes gagal"
"testError" = "Gagal menguji outbound"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Tambahkan Penyeimbang" "addBalancer" = "Tambahkan Penyeimbang"

View file

@ -525,6 +525,12 @@
"accountInfo" = "アカウント情報" "accountInfo" = "アカウント情報"
"outboundStatus" = "アウトバウンドステータス" "outboundStatus" = "アウトバウンドステータス"
"sendThrough" = "送信経路" "sendThrough" = "送信経路"
"test" = "テスト"
"testResult" = "テスト結果"
"testing" = "接続をテスト中..."
"testSuccess" = "テスト成功"
"testFailed" = "テスト失敗"
"testError" = "アウトバウンドのテストに失敗しました"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "負荷分散追加" "addBalancer" = "負荷分散追加"

View file

@ -525,6 +525,12 @@
"accountInfo" = "Informações da Conta" "accountInfo" = "Informações da Conta"
"outboundStatus" = "Status de Saída" "outboundStatus" = "Status de Saída"
"sendThrough" = "Enviar Através de" "sendThrough" = "Enviar Através de"
"test" = "Testar"
"testResult" = "Resultado do teste"
"testing" = "Testando conexão..."
"testSuccess" = "Teste bem-sucedido"
"testFailed" = "Teste falhou"
"testError" = "Falha ao testar saída"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Adicionar Balanceador" "addBalancer" = "Adicionar Balanceador"

View file

@ -525,6 +525,12 @@
"accountInfo" = "Информация об учетной записи" "accountInfo" = "Информация об учетной записи"
"outboundStatus" = "Статус исходящего подключения" "outboundStatus" = "Статус исходящего подключения"
"sendThrough" = "Отправить через" "sendThrough" = "Отправить через"
"test" = "Тест"
"testResult" = "Результат теста"
"testing" = "Тестирование соединения..."
"testSuccess" = "Тест успешен"
"testFailed" = "Тест не пройден"
"testError" = "Не удалось протестировать исходящее подключение"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Создать балансировщик" "addBalancer" = "Создать балансировщик"

View file

@ -525,6 +525,12 @@
"accountInfo" = "Hesap Bilgileri" "accountInfo" = "Hesap Bilgileri"
"outboundStatus" = "Giden Durumu" "outboundStatus" = "Giden Durumu"
"sendThrough" = "Üzerinden Gönder" "sendThrough" = "Üzerinden Gönder"
"test" = "Test"
"testResult" = "Test Sonucu"
"testing" = "Bağlantı test ediliyor..."
"testSuccess" = "Test başarılı"
"testFailed" = "Test başarısız"
"testError" = "Giden test edilemedi"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Dengeleyici Ekle" "addBalancer" = "Dengeleyici Ekle"

View file

@ -525,6 +525,12 @@
"accountInfo" = "Інформація про обліковий запис" "accountInfo" = "Інформація про обліковий запис"
"outboundStatus" = "Статус виходу" "outboundStatus" = "Статус виходу"
"sendThrough" = "Надіслати через" "sendThrough" = "Надіслати через"
"test" = "Тест"
"testResult" = "Результат тесту"
"testing" = "Тестування з'єднання..."
"testSuccess" = "Тест успішний"
"testFailed" = "Тест не пройдено"
"testError" = "Не вдалося протестувати вихідне з'єднання"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Додати балансир" "addBalancer" = "Додати балансир"

View file

@ -525,6 +525,12 @@
"accountInfo" = "Thông tin tài khoản" "accountInfo" = "Thông tin tài khoản"
"outboundStatus" = "Trạng thái đầu ra" "outboundStatus" = "Trạng thái đầu ra"
"sendThrough" = "Gửi qua" "sendThrough" = "Gửi qua"
"test" = "Kiểm tra"
"testResult" = "Kết quả kiểm tra"
"testing" = "Đang kiểm tra kết nối..."
"testSuccess" = "Kiểm tra thành công"
"testFailed" = "Kiểm tra thất bại"
"testError" = "Không thể kiểm tra đầu ra"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "Thêm cân bằng" "addBalancer" = "Thêm cân bằng"

View file

@ -525,6 +525,12 @@
"accountInfo" = "帐户信息" "accountInfo" = "帐户信息"
"outboundStatus" = "出站状态" "outboundStatus" = "出站状态"
"sendThrough" = "发送通过" "sendThrough" = "发送通过"
"test" = "测试"
"testResult" = "测试结果"
"testing" = "正在测试连接..."
"testSuccess" = "测试成功"
"testFailed" = "测试失败"
"testError" = "测试出站失败"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "添加负载均衡" "addBalancer" = "添加负载均衡"

View file

@ -525,6 +525,12 @@
"accountInfo" = "帳戶資訊" "accountInfo" = "帳戶資訊"
"outboundStatus" = "出站狀態" "outboundStatus" = "出站狀態"
"sendThrough" = "傳送通過" "sendThrough" = "傳送通過"
"test" = "測試"
"testResult" = "測試結果"
"testing" = "正在測試連接..."
"testSuccess" = "測試成功"
"testFailed" = "測試失敗"
"testError" = "測試出站失敗"
[pages.xray.balancer] [pages.xray.balancer]
"addBalancer" = "新增負載均衡" "addBalancer" = "新增負載均衡"