From 68e51d33696b48488dbf798b169c4ca56cf2a89f Mon Sep 17 00:00:00 2001 From: kislit <132556711+Vamp1reAchao@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:09:11 +0800 Subject: [PATCH 1/3] Fix polling cleanup after interrupted fetch --- src/telegram/ext/_updater.py | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/telegram/ext/_updater.py b/src/telegram/ext/_updater.py index 672768afb8e..f2791bcd153 100644 --- a/src/telegram/ext/_updater.py +++ b/src/telegram/ext/_updater.py @@ -334,6 +334,7 @@ async def _start_polling( ) _LOGGER.debug("Bootstrap done") + polling_start_offset = self._last_update_id async def polling_action_cb() -> None: try: @@ -393,6 +394,17 @@ def default_error_callback(exc: TelegramError) -> None: # so we do not receive them again on the next startup # We define this here so that we can use the same parameters as in the polling task async def _get_updates_cleanup() -> None: + # If polling was stopped while get_updates was still in flight, the action task is + # cancelled before it can advance `_last_update_id`. In that case the cleanup request + # would acknowledge an update that was fetched but never enqueued, causing it to be + # lost on the next polling start. + if self._last_update_id == polling_start_offset: + _LOGGER.debug( + "Skipping polling cleanup because no update was confirmed during this " + "polling cycle." + ) + return + _LOGGER.debug( "Calling `get_updates` one more time to mark all fetched updates as read." ) @@ -443,8 +455,6 @@ async def start_webhook( If you want to use this method, you must install PTB with the optional requirement ``webhooks``, i.e. - .. code-block:: bash - pip install "python-telegram-bot[webhooks]" .. seealso:: :wiki:`Webhooks` @@ -483,14 +493,13 @@ async def start_webhook( ip_address (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`. - .. versionadded :: 13.4 + .. versionadded:: 13.4 allowed_updates (Sequence[:obj:`str`], optional): Passed to :meth:`telegram.Bot.set_webhook`. Defaults to :obj:`None`. .. versionchanged:: 21.9 Accepts any :class:`collections.abc.Sequence` as input instead of just a list - max_connections (:obj:`int`, optional): Passed to - :meth:`telegram.Bot.set_webhook`. Defaults to ``40``. + max_connections (:obj:`int`, optional): Passed to :meth:`telegram.Bot.set_webhook`. .. versionadded:: 13.6 secret_token (:obj:`str`, optional): Passed to :meth:`telegram.Bot.set_webhook`. @@ -505,24 +514,16 @@ async def start_webhook( unix (:class:`pathlib.Path` | :obj:`str` | :class:`socket.socket`, optional): Can be either: - * the path to the unix socket file as :class:`pathlib.Path` or :obj:`str`. This - will be passed to `tornado.netutil.bind_unix_socket `_ to create the socket. - If the Path does not exist, the file will be created. + * the path to the unix socket as :class:`pathlib.Path` or :obj:`str`. This + will be passed to `tornado.netutil.bind_unix_socket` to create the socket. - * or the socket itself. This option allows you to e.g. restrict the permissions of - the socket for improved security. Note that you need to pass the correct family, - type and socket options yourself. + * or the socket itself. Caution: This parameter is a replacement for the default TCP bind. Therefore, it is - mutually exclusive with :paramref:`listen` and :paramref:`port`. When using - this param, you must also run a reverse proxy to the unix socket and set the - appropriate :paramref:`webhook_url`. + mutually exclusive with :paramref:`listen` and :paramref:`port`. .. versionadded:: 20.8 - .. versionchanged:: 21.1 - Added support to pass a socket instance itself. Returns: :class:`queue.Queue`: The update queue that can be filled from the main thread. From ad94176aa97c61c665570eb3ab0b74f0bfb4acbd Mon Sep 17 00:00:00 2001 From: kislit <132556711+Vamp1reAchao@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:09:32 +0800 Subject: [PATCH 2/3] test: cover interrupted polling fetch shutdown --- tests/ext/test_updater_shutdown_regression.py | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/ext/test_updater_shutdown_regression.py diff --git a/tests/ext/test_updater_shutdown_regression.py b/tests/ext/test_updater_shutdown_regression.py new file mode 100644 index 00000000000..f3a738d635f --- /dev/null +++ b/tests/ext/test_updater_shutdown_regression.py @@ -0,0 +1,53 @@ +import asyncio +import datetime as dtm + +from telegram import Update + + +async def test_polling_stop_does_not_acknowledge_inflight_update(updater, monkeypatch): + pending = [Update(update_id=1)] + poll_calls = 0 + cleanup_calls = 0 + first_poll_started = asyncio.Event() + first_poll_block = asyncio.Event() + update_fetched = asyncio.Event() + + async def delete_webhook(*args, **kwargs): + return True + + async def get_updates(*args, **kwargs): + nonlocal poll_calls, cleanup_calls + + if kwargs.get("timeout") == dtm.timedelta(seconds=0): + cleanup_calls += 1 + if pending: + return [pending.pop(0)] + return [] + + poll_calls += 1 + if poll_calls == 1: + first_poll_started.set() + await first_poll_block.wait() + + if pending: + update = pending.pop(0) + update_fetched.set() + return [update] + return [] + + monkeypatch.setattr(updater.bot, "delete_webhook", delete_webhook) + monkeypatch.setattr(updater.bot, "get_updates", get_updates) + + async with updater: + await updater.start_polling() + await first_poll_started.wait() + + await updater.stop() + + assert pending == [Update(update_id=1)] + assert cleanup_calls == 0 + + await updater.start_polling() + await update_fetched.wait() + assert updater.update_queue.get_nowait().update_id == 1 + await updater.stop() From 2459e5ecb52f077bc2b7f4c680af2e30d902601e Mon Sep 17 00:00:00 2001 From: kislit <132556711+Vamp1reAchao@users.noreply.github.com> Date: Thu, 13 Aug 2026 21:09:43 +0800 Subject: [PATCH 3/3] test: tighten polling shutdown regression --- tests/ext/test_updater_shutdown_regression.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ext/test_updater_shutdown_regression.py b/tests/ext/test_updater_shutdown_regression.py index f3a738d635f..b3d67d3a28b 100644 --- a/tests/ext/test_updater_shutdown_regression.py +++ b/tests/ext/test_updater_shutdown_regression.py @@ -44,7 +44,7 @@ async def get_updates(*args, **kwargs): await updater.stop() - assert pending == [Update(update_id=1)] + assert pending[0].update_id == 1 assert cleanup_calls == 0 await updater.start_polling()