Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 21 additions & 26 deletions gateway/platforms/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
33 changes: 33 additions & 0 deletions tests/gateway/test_telegram_thread_fallback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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