Skip to content

Commit 772f7ef

Browse files
authored
Add Error Handler support for handling unexpected update handler exceptions (#285)
1 parent 13f08c4 commit 772f7ef

5 files changed

Lines changed: 225 additions & 7 deletions

File tree

pyrogram/dispatcher.py

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,16 @@
1717
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
1818

1919
import asyncio
20+
from collections import OrderedDict
2021
import inspect
2122
import logging
22-
from collections import OrderedDict
23+
from typing import Dict
2324

2425
import pyrogram
2526
from pyrogram import utils
2627
from pyrogram.handlers import (
27-
CallbackQueryHandler, MessageHandler, EditedMessageHandler, DeletedMessagesHandler,
28+
Handler,
29+
ErrorHandler, CallbackQueryHandler, MessageHandler, EditedMessageHandler, DeletedMessagesHandler,
2830
UserStatusHandler, RawUpdateHandler, InlineQueryHandler, PollHandler, PreCheckoutQueryHandler,
2931
ChosenInlineResultHandler, ChatMemberUpdatedHandler, ChatJoinRequestHandler, StoryHandler,
3032
ShippingQueryHandler, MessageReactionHandler, MessageReactionCountHandler, ChatBoostHandler,
@@ -294,7 +296,7 @@ async def stop(self, clear_handlers: bool = True):
294296

295297
log.info("Stopped %s HandlerTasks", self.client.workers)
296298

297-
def add_handler(self, handler, group: int):
299+
def add_handler(self, handler: Handler, group: int):
298300
async def fn():
299301
for lock in self.locks_list:
300302
await lock.acquire()
@@ -311,16 +313,21 @@ async def fn():
311313

312314
self.client.loop.create_task(fn())
313315

314-
def remove_handler(self, handler, group: int):
316+
def remove_handler(self, handler: Handler, group: int):
315317
async def fn():
316318
for lock in self.locks_list:
317319
await lock.acquire()
318320

319321
try:
320322
if group not in self.groups:
321-
raise ValueError(f"Group {group} does not exist. Handler was not removed.")
323+
raise ValueError(
324+
f"Group {group} does not exist. Handler was not removed."
325+
)
322326

323327
self.groups[group].remove(handler)
328+
329+
if not self.groups[group]:
330+
del self.groups[group]
324331
finally:
325332
for lock in self.locks_list:
326333
lock.release()
@@ -347,6 +354,9 @@ async def handler_worker(self, lock):
347354
async with lock:
348355
for group in self.groups.values():
349356
for handler in group:
357+
if isinstance(handler, ErrorHandler):
358+
continue
359+
350360
args = None
351361

352362
if isinstance(handler, handler_type):
@@ -382,11 +392,62 @@ async def handler_worker(self, lock):
382392
raise
383393
except pyrogram.ContinuePropagation:
384394
continue
385-
except Exception as e:
386-
log.exception(e)
395+
except Exception as exc:
396+
await self.handle_update_handler_exception(
397+
exc, handler, update, users, chats
398+
)
387399

388400
break
389401
except pyrogram.StopPropagation:
390402
pass
391403
except Exception as e:
392404
log.exception(e)
405+
406+
async def handle_update_handler_exception(
407+
self,
408+
exc: Exception,
409+
update_handler: Handler,
410+
update: "pyrogram.raw.base.Update",
411+
users: Dict[int, "pyrogram.raw.base.User"],
412+
chats: Dict[int, "pyrogram.raw.base.Chat"]
413+
) -> None:
414+
handled = False
415+
try:
416+
for group in self.groups.values():
417+
for handler in group:
418+
if not isinstance(handler, ErrorHandler):
419+
continue
420+
421+
if not isinstance(exc, handler.exceptions):
422+
continue
423+
424+
try:
425+
if inspect.iscoroutinefunction(handler.callback):
426+
await handler.callback(
427+
self.client, exc, update_handler, update, users, chats
428+
)
429+
else:
430+
await self.client.loop.run_in_executor(
431+
self.client.executor, handler.callback,
432+
self.client, exc, update_handler, update, users, chats
433+
)
434+
except pyrogram.StopPropagation:
435+
handled = True
436+
raise
437+
except pyrogram.ContinuePropagation:
438+
handled = True
439+
continue
440+
except Exception:
441+
log.exception("Error handler raised an exception:")
442+
else:
443+
handled = True
444+
445+
break
446+
except pyrogram.StopPropagation:
447+
pass
448+
finally:
449+
if not handled:
450+
log.error(
451+
f"Unexpected exception raised in {type(update_handler).__name__}:",
452+
exc_info=(type(exc), exc, exc.__traceback__)
453+
)

pyrogram/handlers/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
1818

1919
from .handler import Handler
20+
from .error_handler import ErrorHandler
2021
from .business_connection_handler import BusinessConnectionHandler
2122
from .business_message_handler import BusinessMessageHandler
2223
from .callback_query_handler import CallbackQueryHandler

pyrogram/handlers/error_handler.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from typing import Callable, Optional, Sequence, Union
20+
21+
import pyrogram
22+
from pyrogram.filters import Filter
23+
from pyrogram.types import Update
24+
25+
from .handler import Handler
26+
27+
28+
class ErrorHandler(Handler):
29+
"""The Error handler class. Used to handle unexpected errors.
30+
31+
It is intended to be used with :meth:`~pyrogram.Client.add_handler`.
32+
33+
For a more convenient way to register this handler, see the
34+
:meth:`~pyrogram.Client.on_error` decorator.
35+
36+
Parameters:
37+
callback (``Callable``):
38+
A function that will be called whenever an unexpected error is raised.
39+
It takes the following positional arguments: *(exception, handler, client, *args)*.
40+
41+
exceptions (``Exception`` | List of ``Exception``, *optional*):
42+
An exception type or a sequence of exception types that this handler should handle.
43+
If None, the handler will catch any exception that is a subclass of ``Exception``.
44+
45+
filters (:obj:`Filter`, *optional*):
46+
Pass one or more filters to allow only a subset of updates to be passed
47+
in your callback function.
48+
49+
Other parameters passed to the callback:
50+
client (:obj:`~pyrogram.Client`):
51+
The Client instance, useful when calling other API methods inside the error handler.
52+
53+
exception (``Exception``):
54+
The Exception instance that was raised.
55+
56+
handler (:obj:`~pyrogram.handlers.handler.Handler`):
57+
The Handler instance from which the exception was raised.
58+
59+
update (:obj:`~pyrogram.raw.base.Update`):
60+
The received update, which can be one of the many single Updates listed in the
61+
:obj:`~pyrogram.raw.base.Update` base type.
62+
63+
users (``dict``):
64+
Dictionary of all :obj:`~pyrogram.types.User` mentioned in the update.
65+
You can access extra info about the user (such as *first_name*, *last_name*, etc...) by using
66+
the IDs you find in the *update* argument (e.g.: *users[1768841572]*).
67+
68+
chats (``dict``):
69+
Dictionary of all :obj:`~pyrogram.types.Chat` and
70+
:obj:`~pyrogram.raw.types.Channel` mentioned in the update.
71+
You can access extra info about the chat (such as *title*, *participants_count*, etc...)
72+
by using the IDs you find in the *update* argument (e.g.: *chats[1701277281]*).
73+
74+
"""
75+
76+
def __init__(
77+
self,
78+
callback: Callable,
79+
exceptions: Optional[Union[Exception, Sequence[Exception]]] = None,
80+
filters: Optional[Filter] = None,
81+
):
82+
super().__init__(callback, filters)
83+
84+
if exceptions is None:
85+
self.exceptions = (Exception,)
86+
elif isinstance(exceptions, Sequence):
87+
self.exceptions = tuple(exceptions)
88+
else:
89+
self.exceptions = (exceptions,)

pyrogram/methods/decorators/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# You should have received a copy of the GNU Lesser General Public License
1717
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
1818

19+
from .on_error import OnError
1920
from .on_business_connection import OnBusinessConnection
2021
from .on_business_message import OnBusinessMessage
2122
from .on_callback_query import OnCallbackQuery
@@ -45,6 +46,7 @@
4546

4647

4748
class Decorators(
49+
OnError,
4850
OnBusinessConnection,
4951
OnBusinessMessage,
5052
OnMessage,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# Pyrogram - Telegram MTProto API Client Library for Python
2+
# Copyright (C) 2017-present Dan <https://github.com/delivrance>
3+
#
4+
# This file is part of Pyrogram.
5+
#
6+
# Pyrogram is free software: you can redistribute it and/or modify
7+
# it under the terms of the GNU Lesser General Public License as published
8+
# by the Free Software Foundation, either version 3 of the License, or
9+
# (at your option) any later version.
10+
#
11+
# Pyrogram is distributed in the hope that it will be useful,
12+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
# GNU Lesser General Public License for more details.
15+
#
16+
# You should have received a copy of the GNU Lesser General Public License
17+
# along with Pyrogram. If not, see <http://www.gnu.org/licenses/>.
18+
19+
from typing import Callable, Optional, Sequence, Union
20+
21+
import pyrogram
22+
from pyrogram.filters import Filter
23+
24+
25+
class OnError:
26+
def on_error(
27+
self: Optional[Union["OnError", Filter]] = None,
28+
exceptions: Optional[Union[Exception, Sequence[Exception]]] = None,
29+
filters: Optional[Filter] = None,
30+
group: int = 0,
31+
) -> Callable:
32+
"""Decorator for handling unexpected errors.
33+
34+
This does the same thing as :meth:`~pyrogram.Client.add_handler` using the
35+
:obj:`~pyrogram.handlers.ErrorHandler`.
36+
37+
.. include:: /_includes/usable-by/users-bots.rst
38+
39+
Parameters:
40+
exceptions (``Exception`` | List of ``Exception``, *optional*):
41+
An exception type or a sequence of exception types that this handler should handle.
42+
If None, the handler will catch any exception that is a subclass of ``Exception``.
43+
44+
filters (:obj:`~pyrogram.filters`, *optional*):
45+
Pass one or more filters to allow only a subset of messages to be passed
46+
in your function.
47+
48+
group (``int``, *optional*):
49+
The group identifier, defaults to 0.
50+
"""
51+
52+
def decorator(func: Callable) -> Callable:
53+
if isinstance(self, pyrogram.Client):
54+
self.add_handler(pyrogram.handlers.ErrorHandler(func, filters, exceptions), group)
55+
else:
56+
if not hasattr(func, "handlers"):
57+
func.handlers = []
58+
59+
func.handlers.append(
60+
(pyrogram.handlers.ErrorHandler(func, filters, exceptions), group)
61+
)
62+
63+
return func
64+
65+
return decorator

0 commit comments

Comments
 (0)