From 97fb9e1f629c633e1c60d4b64ba490e7c3a61e60 Mon Sep 17 00:00:00 2001 From: liuhao1024 Date: Fri, 10 Jul 2026 18:56:14 +0530 Subject: [PATCH] fix(telegram): classify PTB heartbeat transport errors --- plugins/platforms/telegram/adapter.py | 49 +++++++++++++++++---------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index fd5fc0177eba..5d4c095e3fbe 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -1175,12 +1175,23 @@ class TelegramAdapter(BasePlatformAdapter): @staticmethod def _looks_like_network_error(error: Exception) -> bool: - """Return True for transient network errors that warrant a reconnect attempt.""" + """Return True for transient transport failures that warrant reconnect.""" name = error.__class__.__name__.lower() + if name in {"badrequest", "invalidtoken", "forbidden", "retryafter"}: + return False if name in {"networkerror", "timedout", "connectionerror"}: return True try: - from telegram.error import NetworkError, TimedOut + from telegram.error import ( + BadRequest, + Forbidden, + InvalidToken, + NetworkError, + RetryAfter, + TimedOut, + ) + if isinstance(error, (BadRequest, InvalidToken, Forbidden, RetryAfter)): + return False if isinstance(error, (NetworkError, TimedOut)): return True except ImportError: @@ -2204,17 +2215,11 @@ class TelegramAdapter(BasePlatformAdapter): except asyncio.CancelledError: return except (asyncio.TimeoutError, OSError) as probe_err: - logger.warning( - "[%s] Polling heartbeat probe failed (%s); triggering reconnect", - self.name, probe_err, - ) - if self._polling_error_task and not self._polling_error_task.done(): - continue # reconnect already in progress - loop = asyncio.get_running_loop() - self._polling_error_task = loop.create_task( - self._handle_polling_network_error(probe_err) - ) - except Exception: + self._schedule_polling_recovery(probe_err, reason="heartbeat probe") + except Exception as probe_err: + if self._looks_like_network_error(probe_err): + self._schedule_polling_recovery(probe_err, reason="heartbeat probe") + continue # Non-connectivity errors (e.g. TelegramError 401) are not # CLOSE-WAIT symptoms — let PTB's own handlers surface them. pass @@ -3192,10 +3197,6 @@ class TelegramAdapter(BasePlatformAdapter): # Start polling — retry initialize() for transient TLS resets. # Each attempt is capped by _init_timeout so a single unreachable # fallback-IP chain can't block startup indefinitely. - try: - from telegram.error import NetworkError, TimedOut - except ImportError: - NetworkError = TimedOut = OSError # type: ignore[misc,assignment] _max_connect = 8 _init_timeout = _env_float("HERMES_TELEGRAM_INIT_TIMEOUT", 30.0) for _attempt in range(_max_connect): @@ -3230,7 +3231,19 @@ class TelegramAdapter(BasePlatformAdapter): f"({_init_timeout:.0f}s each). Check network connectivity to api.telegram.org " f"or set HERMES_TELEGRAM_HTTP_CONNECT_TIMEOUT to a lower value." ) - except (NetworkError, TimedOut, OSError) as init_err: + except OSError as init_err: + if _attempt < _max_connect - 1: + wait = min(2 ** _attempt, 15) + logger.warning( + "[%s] Connect attempt %d/%d failed: %s — retrying in %ds", + self.name, _attempt + 1, _max_connect, init_err, wait, + ) + await asyncio.sleep(wait) + else: + raise + except Exception as init_err: + if not self._looks_like_network_error(init_err): + raise if _attempt < _max_connect - 1: wait = min(2 ** _attempt, 15) logger.warning(