From eb4bc1513f0dabe72d80723185907d33d48a96cc Mon Sep 17 00:00:00 2001 From: kshitij <82637225+kshitijk4poor@users.noreply.github.com> Date: Mon, 17 Aug 2026 16:15:07 +0530 Subject: [PATCH] fix(telegram): log first-choice IPv4 stick as info, not warning Healthy IPv4-first connect is the new default path, so two transports were warning on every successful initialize. Keep warning only when a literal actually failed first. Also restates the transport docstring and docs to match IPv4-first, hostname last. --- .../platforms/telegram/telegram_network.py | 14 ++++--- tests/gateway/test_telegram_network.py | 38 +++++++++++++++++++ website/docs/user-guide/messaging/telegram.md | 2 +- 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/plugins/platforms/telegram/telegram_network.py b/plugins/platforms/telegram/telegram_network.py index bdec305ff6..9ba5ebc005 100644 --- a/plugins/platforms/telegram/telegram_network.py +++ b/plugins/platforms/telegram/telegram_network.py @@ -53,12 +53,13 @@ def _resolve_proxy_url(target_hosts=None) -> str | None: class TelegramFallbackTransport(httpx.AsyncBaseTransport): - """Retry Telegram Bot API requests via fallback IPs while preserving TLS/SNI. + """Reach Telegram Bot API via known IPv4 literals first, hostname last. - Requests continue to target https://api.telegram.org/... logically, but on - connect failures the underlying TCP connection is retried against a known - reachable IP. This is effectively the programmatic equivalent of - ``curl --resolve api.telegram.org:443:``. + Requests still target https://api.telegram.org/... logically (Host + SNI + stay on the hostname). TCP connects to a known A-record IP first so a + blackholed IPv6 AAAA cannot pin initialize(). Equivalent to + ``curl --resolve api.telegram.org:443:``. The dual-stack hostname + is last resort for IPv6-only networks. """ # Bound every pool. httpx defaults to 100 connections per pool, so a wedged @@ -159,7 +160,8 @@ class TelegramFallbackTransport(httpx.AsyncBaseTransport): if self._sticky_ip is _UNSET or self._sticky_ip != ip: self._sticky_ip = ip if ip is not None: - logger.warning( + log = logger.warning if last_error is not None else logger.info + log( "[Telegram] Using sticky IPv4 Telegram API path %s " "(dual-stack hostname tried last — #87015)", ip, diff --git a/tests/gateway/test_telegram_network.py b/tests/gateway/test_telegram_network.py index cc599f6d77..e23cc04609 100644 --- a/tests/gateway/test_telegram_network.py +++ b/tests/gateway/test_telegram_network.py @@ -138,6 +138,44 @@ class TestFallbackTransport: assert resp2.status_code == 200 assert calls[0]["url_host"] == "149.154.167.220" + @pytest.mark.asyncio + async def test_first_choice_ipv4_success_logs_info_not_warning(self, monkeypatch, caplog): + """Healthy IPv4-first connect is info; warning only after a skipped dead path.""" + import logging + + calls = [] + monkeypatch.setattr( + tnet.httpx, + "AsyncHTTPTransport", + _fake_transport_factory(calls, {"149.154.167.220": "ok"}), + ) + transport = tnet.TelegramFallbackTransport(["149.154.167.220"]) + with caplog.at_level(logging.INFO, logger="plugins.platforms.telegram.telegram_network"): + await transport.handle_async_request(_telegram_request()) + records = [r for r in caplog.records if "sticky IPv4 Telegram API path" in r.getMessage()] + assert len(records) == 1 + assert records[0].levelno == logging.INFO + + @pytest.mark.asyncio + async def test_sticky_after_failed_literal_logs_warning(self, monkeypatch, caplog): + import logging + + calls = [] + monkeypatch.setattr( + tnet.httpx, + "AsyncHTTPTransport", + _fake_transport_factory( + calls, {"149.154.167.220": "timeout", "149.154.167.221": "ok"} + ), + ) + transport = tnet.TelegramFallbackTransport(["149.154.167.220", "149.154.167.221"]) + with caplog.at_level(logging.INFO, logger="plugins.platforms.telegram.telegram_network"): + await transport.handle_async_request(_telegram_request()) + records = [r for r in caplog.records if "sticky IPv4 Telegram API path" in r.getMessage()] + assert len(records) == 1 + assert records[0].levelno == logging.WARNING + assert "149.154.167.221" in records[0].getMessage() + @pytest.mark.asyncio async def test_sticky_ip_tried_first_but_falls_through_if_stale(self, monkeypatch): diff --git a/website/docs/user-guide/messaging/telegram.md b/website/docs/user-guide/messaging/telegram.md index 5e94c3a245..4b4b58feef 100644 --- a/website/docs/user-guide/messaging/telegram.md +++ b/website/docs/user-guide/messaging/telegram.md @@ -1138,7 +1138,7 @@ In some restricted networks, `api.telegram.org` may resolve to an IP that is unr 1. If `TELEGRAM_FALLBACK_IPS` is set, those IPs are used directly. 2. Otherwise, the adapter automatically queries **Google DNS** and **Cloudflare DNS** via DNS-over-HTTPS (DoH) to discover alternative IPs for `api.telegram.org`. 3. Known IPv4 Telegram API IPs are tried **before** the dual-stack `api.telegram.org` hostname. A blackholed IPv6 path can sit in `connect()` without erroring, which used to pin the event loop so the 30s init deadline never fired. -4. If DoH is also blocked or times out, a hardcoded IPv4 seed list (`149.154.166.110`, `149.154.167.220`) is used instead of falling through to the hostname. +4. If DoH is also blocked or times out, a hardcoded IPv4 seed list (`149.154.166.110`, `149.154.167.220`) is used as that IPv4-first list. The hostname remains last resort. 5. Once a path succeeds, it becomes "sticky" — subsequent requests use it directly. The hostname is kept as a last resort for IPv6-only networks. ### Configuration