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.
This commit is contained in:
kshitij
2026-08-17 16:15:07 +05:30
parent 55e2dffe16
commit eb4bc1513f
3 changed files with 47 additions and 7 deletions
@@ -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:<ip>``.
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:<ip>``. 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,
+38
View File
@@ -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):
@@ -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