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:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user