From 19982a6ee232110664add682d93250c6cb1e91e9 Mon Sep 17 00:00:00 2001 From: infinitycrew39 Date: Sun, 21 Jun 2026 08:31:46 +0700 Subject: [PATCH 1/2] test(telegram): cover thread fallback when badrequest is non-network --- .../gateway/test_telegram_thread_fallback.py | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/gateway/test_telegram_thread_fallback.py b/tests/gateway/test_telegram_thread_fallback.py index 036d27e771bf..b4219e4f6e3a 100644 --- a/tests/gateway/test_telegram_thread_fallback.py +++ b/tests/gateway/test_telegram_thread_fallback.py @@ -48,6 +48,11 @@ def __init__(self, seconds): self.retry_after = seconds +class FakeStandaloneBadRequest(Exception): + """BadRequest variant that is not a NetworkError subclass.""" + pass + + # Build a fake telegram module tree so the adapter's internal imports work class _FakeInlineKeyboardButton: def __init__(self, text, callback_data=None, **kwargs): @@ -1412,3 +1417,31 @@ async def mock_send_message(**kwargs): assert result.success is True assert result.message_id == "300" assert attempt[0] == 2 + + +@pytest.mark.asyncio +async def test_send_thread_not_found_falls_back_when_badrequest_not_network(monkeypatch): + """Thread fallback should not depend on BadRequest inheriting NetworkError.""" + adapter = _make_adapter() + call_log = [] + + monkeypatch.setattr(_fake_telegram_error, "BadRequest", FakeStandaloneBadRequest) + + async def mock_send_message(**kwargs): + call_log.append(dict(kwargs)) + if kwargs.get("message_thread_id") is not None: + raise FakeStandaloneBadRequest("Message thread not found") + return SimpleNamespace(message_id=777) + + adapter._bot = SimpleNamespace(send_message=mock_send_message) + + result = await adapter.send( + chat_id="-100123", + content="test message", + metadata={"thread_id": "99999"}, + ) + + assert result.success is True + assert result.message_id == "777" + assert call_log[0].get("message_thread_id") == 99999 + assert call_log[-1].get("message_thread_id") is None From 099de1b81753244837c86fc33f436f09823a6db4 Mon Sep 17 00:00:00 2001 From: infinitycrew39 Date: Sun, 21 Jun 2026 08:31:46 +0700 Subject: [PATCH 2/2] fix(telegram): handle badrequest thread fallback regardless of hierarchy --- gateway/platforms/telegram.py | 47 ++++++++++++++++------------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/gateway/platforms/telegram.py b/gateway/platforms/telegram.py index aed7b71af9b5..30bc850e11b0 100644 --- a/gateway/platforms/telegram.py +++ b/gateway/platforms/telegram.py @@ -2378,11 +2378,6 @@ async def send( except ImportError: _NetErr = OSError # type: ignore[misc,assignment] - try: - from telegram.error import BadRequest as _BadReq - except ImportError: - _BadReq = None # type: ignore[assignment,misc] - try: from telegram.error import TimedOut as _TimedOut except (ImportError, AttributeError): @@ -2462,12 +2457,12 @@ async def send( else: raise break # success - except _NetErr as send_err: - # BadRequest is a subclass of NetworkError in - # python-telegram-bot but represents permanent errors - # (not transient network issues). Detect and handle - # specific cases instead of blindly retrying. - if _BadReq and isinstance(send_err, _BadReq): + except Exception as send_err: + # Handle BadRequest semantics first. Different + # python-telegram-bot versions/packages may expose + # BadRequest with different inheritance, so this + # branch must not depend on it being a NetworkError. + if self._is_bad_request_error(send_err): if self._is_thread_not_found_error(send_err) and effective_thread_id is not None: if private_dm_topic_send or (metadata and metadata.get("telegram_dm_topic_created_for_send")): return SendResult( @@ -2531,6 +2526,21 @@ async def send( continue # Other BadRequest errors are permanent — don't retry raise + retry_after = getattr(send_err, "retry_after", None) + if retry_after is not None or "retry after" in str(send_err).lower(): + if _send_attempt < 2: + wait = float(retry_after) if retry_after is not None else 1.0 + logger.warning( + "[%s] Telegram flood control on send (attempt %d/3), retrying in %.1fs: %s", + self.name, + _send_attempt + 1, + wait, + send_err, + ) + await asyncio.sleep(wait) + continue + if not isinstance(send_err, _NetErr): + raise # TimedOut is also a subclass of NetworkError. A # generic timeout may have reached Telegram, so don't # retry; a wrapped ConnectTimeout means no connection @@ -2552,21 +2562,6 @@ async def send( await asyncio.sleep(wait) else: raise - except Exception as send_err: - retry_after = getattr(send_err, "retry_after", None) - if retry_after is not None or "retry after" in str(send_err).lower(): - if _send_attempt < 2: - wait = float(retry_after) if retry_after is not None else 1.0 - logger.warning( - "[%s] Telegram flood control on send (attempt %d/3), retrying in %.1fs: %s", - self.name, - _send_attempt + 1, - wait, - send_err, - ) - await asyncio.sleep(wait) - continue - raise message_ids.append(str(msg.message_id)) # Re-trigger typing indicator after sending a message.