diff --git a/plugins/platforms/telegram/adapter.py b/plugins/platforms/telegram/adapter.py index c673062d7b4e..b60728acf862 100644 --- a/plugins/platforms/telegram/adapter.py +++ b/plugins/platforms/telegram/adapter.py @@ -3049,12 +3049,31 @@ def _with_limits(httpx_kwargs: Optional[dict] = None) -> dict: proxy_targets = ["api.telegram.org", *fallback_ips] proxy_url = resolve_proxy_url("TELEGRAM_PROXY", target_hosts=proxy_targets) - if fallback_ips and not proxy_url and not disable_fallback: - logger.info( - "[%s] Telegram fallback IPs active: %s", - self.name, - ", ".join(fallback_ips), - ) + if fallback_ips and not disable_fallback: + # The fallback-IP transport pins the TCP connection to a + # known-reachable Telegram IP while preserving the + # api.telegram.org SNI/Host. It also threads TELEGRAM_PROXY + # through internally, so this single branch handles three + # cases: fallback-only, proxy-only-with-pinned-IPs, and + # proxy+fallback together. Combining a proxy with pinned IPs is + # required when the proxy's upstream egress can only reach a + # subset of the api.telegram.org anycast block (e.g. a + # DPI-bypass router): plain httpx DNS through the proxy would + # pick an unreachable IP, but the transport falls through to + # the reachable one. + if proxy_url: + logger.info( + "[%s] Telegram fallback IPs active via proxy %s: %s", + self.name, + proxy_url, + ", ".join(fallback_ips), + ) + else: + logger.info( + "[%s] Telegram fallback IPs active: %s", + self.name, + ", ".join(fallback_ips), + ) # Keep request/update pools separate to reduce contention during # polling reconnect + bot API bootstrap/delete_webhook calls. # httpx ignores the client-level `limits` kwarg when a custom diff --git a/plugins/platforms/telegram/telegram_network.py b/plugins/platforms/telegram/telegram_network.py index 319f4d2accf8..83eddec04bf6 100644 --- a/plugins/platforms/telegram/telegram_network.py +++ b/plugins/platforms/telegram/telegram_network.py @@ -256,4 +256,13 @@ def _rewrite_request_for_ip(request: httpx.Request, ip: str) -> httpx.Request: def _is_retryable_connect_error(exc: Exception) -> bool: - return isinstance(exc, (httpx.ConnectTimeout, httpx.ConnectError)) + # httpx.ProxyError is NOT a subclass of ConnectError. When TELEGRAM_PROXY + # points at a proxy that can reach some Telegram IPs but not the one the + # primary DNS path resolved to (e.g. a DPI-bypass router whose upstream + # egress only routes a subset of the api.telegram.org anycast block), the + # primary attempt raises ProxyError "Host unreachable". Treat that as + # retryable so the transport falls through to the reachable fallback IPs + # instead of failing the whole request. + return isinstance( + exc, (httpx.ConnectTimeout, httpx.ConnectError, httpx.ProxyError) + )