Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move error_handler error handing into Dispatcher.dispatch_error #2660

Merged
merged 5 commits into from Sep 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
119 changes: 53 additions & 66 deletions telegram/ext/dispatcher.py
Expand Up @@ -62,7 +62,8 @@

class DispatcherHandlerStop(Exception):
"""
Raise this in handler to prevent execution of any other handler (even in different group).
Raise this in a handler or an error handler to prevent execution of any other handler (even in
different group).

In order to use this exception in a :class:`telegram.ext.ConversationHandler`, pass the
optional ``state`` parameter instead of returning the next state:
Expand All @@ -73,6 +74,9 @@ def callback(update, context):
...
raise DispatcherHandlerStop(next_state)

Note:
Has no effect, if the handler or error handler is run asynchronously.

Attributes:
state (:obj:`object`): Optional. The next state of the conversation.

Expand Down Expand Up @@ -320,15 +324,16 @@ def _pooled(self) -> None:

# Avoid infinite recursion of error handlers.
if promise.pooled_function in self.error_handlers:
self.logger.error('An uncaught error was raised while handling the error.')
self.logger.exception(
'An error was raised and an uncaught error was raised while '
'handling the error with an error_handler.',
exc_info=promise.exception,
)
continue

# If we arrive here, an exception happened in the promise and was neither
# DispatcherHandlerStop nor raised by an error handler. So we can and must handle it
try:
self.dispatch_error(promise.update, promise.exception, promise=promise)
except Exception:
self.logger.exception('An uncaught error was raised while handling the error.')
self.dispatch_error(promise.update, promise.exception, promise=promise)

def run_async(
self, func: Callable[..., object], *args: object, update: object = None, **kwargs: object
Expand Down Expand Up @@ -452,10 +457,7 @@ def process_update(self, update: object) -> None:
"""
# An error happened while polling
if isinstance(update, TelegramError):
try:
self.dispatch_error(None, update)
except Exception:
self.logger.exception('An uncaught error was raised while handling the error.')
self.dispatch_error(None, update)
return

context = None
Expand Down Expand Up @@ -483,14 +485,9 @@ def process_update(self, update: object) -> None:

# Dispatch any error.
except Exception as exc:
try:
self.dispatch_error(update, exc)
except DispatcherHandlerStop:
self.logger.debug('Error handler stopped further handlers')
if self.dispatch_error(update, exc):
self.logger.debug('Error handler stopped further handlers.')
break
# Errors should not stop the thread.
except Exception:
self.logger.exception('An uncaught error was raised while handling the error.')

# Update persistence, if handled
handled_only_async = all(sync_modes)
Expand Down Expand Up @@ -606,72 +603,37 @@ def __update_persistence(self, update: object = None) -> None:
self.bot.callback_data_cache.persistence_data
)
except Exception as exc:
try:
self.dispatch_error(update, exc)
except Exception:
message = (
'Saving callback data raised an error and an '
'uncaught error was raised while handling '
'the error with an error_handler'
)
self.logger.exception(message)
self.dispatch_error(update, exc)
if self.persistence.store_data.bot_data:
try:
self.persistence.update_bot_data(self.bot_data)
except Exception as exc:
try:
self.dispatch_error(update, exc)
except Exception:
message = (
'Saving bot data raised an error and an '
'uncaught error was raised while handling '
'the error with an error_handler'
)
self.logger.exception(message)
self.dispatch_error(update, exc)
if self.persistence.store_data.chat_data:
for chat_id in chat_ids:
try:
self.persistence.update_chat_data(chat_id, self.chat_data[chat_id])
except Exception as exc:
try:
self.dispatch_error(update, exc)
except Exception:
message = (
'Saving chat data raised an error and an '
'uncaught error was raised while handling '
'the error with an error_handler'
)
self.logger.exception(message)
self.dispatch_error(update, exc)
if self.persistence.store_data.user_data:
for user_id in user_ids:
try:
self.persistence.update_user_data(user_id, self.user_data[user_id])
except Exception as exc:
try:
self.dispatch_error(update, exc)
except Exception:
message = (
'Saving user data raised an error and an '
'uncaught error was raised while handling '
'the error with an error_handler'
)
self.logger.exception(message)
self.dispatch_error(update, exc)

def add_error_handler(
self,
callback: Callable[[object, CCT], None],
run_async: Union[bool, DefaultValue] = DEFAULT_FALSE, # pylint: disable=W0621
) -> None:
"""Registers an error handler in the Dispatcher. This handler will receive every error
which happens in your bot.
which happens in your bot. See the docs of :meth:`dispatch_error` for more details on how
errors are handled.

Note:
Attempts to add the same callback multiple times will be ignored.

Warning:
The errors handled within these handlers won't show up in the logger, so you
need to make sure that you reraise the error.

Args:
callback (:obj:`callable`): The callback function for this error handler. Will be
called when an error is raised.
Expand Down Expand Up @@ -700,16 +662,31 @@ def remove_error_handler(self, callback: Callable[[object, CCT], None]) -> None:
self.error_handlers.pop(callback, None)

def dispatch_error(
self, update: Optional[object], error: Exception, promise: Promise = None
) -> None:
"""Dispatches an error.
self,
update: Optional[object],
error: Exception,
promise: Promise = None,
) -> bool:
"""Dispatches an error by passing it to all error handlers registered with
:meth:`add_error_handler`. If one of the error handlers raises
:class:`telegram.ext.DispatcherHandlerStop`, the update will not be handled by other error
handlers or handlers (even in other groups). All other exceptions raised by an error
handler will just be logged.

.. versionchanged:: 14.0
* Exceptions raised by error handlers are now properly logged.
* :class:`telegram.ext.DispatcherHandlerStop` is no longer reraised but converted into
the return value.

Args:
update (:obj:`object` | :class:`telegram.Update`): The update that caused the error.
error (:obj:`Exception`): The error that was raised.
promise (:class:`telegram.utils.Promise`, optional): The promise whose pooled function
raised the error.

Returns:
:obj:`bool`: :obj:`True` if one of the error handlers raised
:class:`telegram.ext.DispatcherHandlerStop`. :obj:`False`, otherwise.
"""
async_args = None if not promise else promise.args
async_kwargs = None if not promise else promise.kwargs
Expand All @@ -722,9 +699,19 @@ def dispatch_error(
if run_async:
self.run_async(callback, update, context, update=update)
else:
callback(update, context)
try:
callback(update, context)
except DispatcherHandlerStop:
return True
except Exception as exc:
self.logger.exception(
'An error was raised and an uncaught error was raised while '
'handling the error with an error_handler.',
exc_info=exc,
)
return False

else:
self.logger.exception(
'No error handlers are registered, logging exception.', exc_info=error
)
self.logger.exception(
'No error handlers are registered, logging exception.', exc_info=error
)
return False
20 changes: 2 additions & 18 deletions telegram/ext/jobqueue.py
Expand Up @@ -73,15 +73,7 @@ def _update_persistence(self, _: JobEvent) -> None:
self._dispatcher.update_persistence()

def _dispatch_error(self, event: JobEvent) -> None:
try:
self._dispatcher.dispatch_error(None, event.exception)
# Errors should not stop the thread.
except Exception:
self.logger.exception(
'An error was raised while processing the job and an '
'uncaught error was raised while handling the error '
'with an error_handler.'
)
self._dispatcher.dispatch_error(None, event.exception)

@overload
def _parse_time_input(self, time: None, shift_day: bool = False) -> None:
Expand Down Expand Up @@ -524,15 +516,7 @@ def run(self, dispatcher: 'Dispatcher') -> None:
try:
self.callback(dispatcher.context_types.context.from_job(self, dispatcher))
except Exception as exc:
try:
dispatcher.dispatch_error(None, exc)
# Errors should not stop the thread.
except Exception:
dispatcher.logger.exception(
'An error was raised while processing the job and an '
'uncaught error was raised while handling the error '
'with an error_handler.'
)
dispatcher.dispatch_error(None, exc)

def schedule_removal(self) -> None:
"""
Expand Down
29 changes: 18 additions & 11 deletions tests/test_dispatcher.py
Expand Up @@ -298,7 +298,9 @@ def test_async_handler_error_handler_that_raises_error(self, dp, caplog):
dp.update_queue.put(self.message_update)
sleep(0.1)
assert len(caplog.records) == 1
assert caplog.records[-1].getMessage().startswith('An uncaught error was raised')
assert (
caplog.records[-1].getMessage().startswith('An error was raised and an uncaught')
)

# Make sure that the main loop still runs
dp.remove_handler(handler)
Expand All @@ -316,7 +318,9 @@ def test_async_handler_async_error_handler_that_raises_error(self, dp, caplog):
dp.update_queue.put(self.message_update)
sleep(0.1)
assert len(caplog.records) == 1
assert caplog.records[-1].getMessage().startswith('An uncaught error was raised')
assert (
caplog.records[-1].getMessage().startswith('An error was raised and an uncaught')
)

# Make sure that the main loop still runs
dp.remove_handler(handler)
Expand Down Expand Up @@ -631,7 +635,7 @@ def test_sensible_worker_thread_names(self, dp2):
for thread_name in thread_names:
assert thread_name.startswith(f"Bot:{dp2.bot.id}:worker:")

def test_error_while_persisting(self, dp, monkeypatch):
def test_error_while_persisting(self, dp, caplog):
class OwnPersistence(BasePersistence):
def update(self, data):
raise Exception('PersistenceError')
Expand Down Expand Up @@ -681,27 +685,30 @@ def flush(self):
def callback(update, context):
pass

test_flag = False
test_flag = []

def error(update, context):
nonlocal test_flag
test_flag = str(context.error) == 'PersistenceError'
test_flag.append(str(context.error) == 'PersistenceError')
raise Exception('ErrorHandlingError')

def logger(message):
assert 'uncaught error was raised while handling' in message

update = Update(
1, message=Message(1, None, Chat(1, ''), from_user=User(1, '', False), text='Text')
)
handler = MessageHandler(Filters.all, callback)
dp.add_handler(handler)
dp.add_error_handler(error)
monkeypatch.setattr(dp.logger, 'exception', logger)

dp.persistence = OwnPersistence()
dp.process_update(update)
assert test_flag

with caplog.at_level(logging.ERROR):
dp.process_update(update)

assert test_flag == [True, True, True, True]
assert len(caplog.records) == 4
for record in caplog.records:
message = record.getMessage()
assert message.startswith('An error was raised and an uncaught')

def test_persisting_no_user_no_chat(self, dp):
class OwnPersistence(BasePersistence):
Expand Down
4 changes: 1 addition & 3 deletions tests/test_jobqueue.py
Expand Up @@ -449,15 +449,13 @@ def test_dispatch_error_that_raises_errors(self, job_queue, dp, caplog):
sleep(0.1)
assert len(caplog.records) == 1
rec = caplog.records[-1]
assert 'processing the job' in rec.getMessage()
assert 'uncaught error was raised while handling' in rec.getMessage()
assert 'An error was raised and an uncaught' in rec.getMessage()
caplog.clear()

with caplog.at_level(logging.ERROR):
job.run(dp)
assert len(caplog.records) == 1
rec = caplog.records[-1]
assert 'processing the job' in rec.getMessage()
assert 'uncaught error was raised while handling' in rec.getMessage()
caplog.clear()

Expand Down