8000 Handle Exceptions in building `CallbackContext` (#4222) · gtkacz/python-telegram-bot@912fe45 · GitHub
[go: up one dir, main page]

Skip to content

Commit 912fe45

Browse files
authored
Handle Exceptions in building CallbackContext (python-telegram-bot#4222)
1 parent 805b7bf commit 912fe45

File tree

4 files changed

+186
-29
lines changed

4 files changed

+186
-29
lines changed

telegram/ext/_application.py

Lines changed: 53 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,30 +1251,43 @@ async def process_update(self, update: object) -> None:
12511251
try:
12521252
for handler in handlers:
12531253
check = handler.check_update(update) # Should the handler handle this update?
1254-
if not (check is None or check is False): # if yes,
1255-
if not context: # build a context if not already built
1254+
if check is None or check is False:
1255+
continue
1256+
1257+
if not context: # build a context if not already built
1258+
try:
12561259
context = self.context_types.context.from_update(update, self)
1257-
await context.refresh_data()
1258-
coroutine: Coroutine = handler.handle_update(update, self, check, context)
1259-
1260-
if not handler.block or ( # if handler is running with block=False,
1261-
handler.block is DEFAULT_TRUE
1262-
and isinstance(self.bot, ExtBot)
1263-
and self.bot.defaults
1264-
and not self.bot.defaults.block
1265-
):
1266-
self.create_task(
1267-
coroutine,
1268-
update=update,
1269-
name=(
1270-
f"Application:{self.bot.id}:process_update_non_blocking"
1271-
f":{handler}"
1260+
except Exception as exc:
1261+
_LOGGER.critical(
1262+
(
1263+
"Error while building CallbackContext for update %s. "
1264+
"Update will not be processed."
12721265
),
1266+
update,
1267+
exc_info=exc,
12731268
)
1274-
else:
1275-
any_blocking = True
1276-
await coroutine
1277-
break # Only a max of 1 handler per group is handled
1269+
return
1270+
await context.refresh_data()
1271+
coroutine: Coroutine = handler.handle_update(update, self, check, context)
1272+
1273+
if not handler.block or ( # if handler is running with block=False,
1274+
handler.block is DEFAULT_TRUE
1275+
and isinstance(self.bot, ExtBot)
1276+
and self.bot.defaults
1277+
and not self.bot.defaults.block
1278+
):
1279+
self.create_task(
1280+
coroutine,
1281+
update=update,
1282+
name=(
1283+
f"Application:{self.bot.id}:process_update_non_blocking"
1284+
f":{handler}"
1285+
),
1286+
)
1287+
else:
1288+
any_blocking = True
1289+
await coroutine
1290+
break # Only a max of 1 handler per group is handled
12781291

12791292
# Stop processing with any other handler.
12801293
except ApplicationHandlerStop:
@@ -1808,13 +1821,25 @@ async def process_error(
18081821
callback,
18091822
block,
18101823
) in self.error_handlers.items():
1811-
context = self.context_types.context.from_error(
1812-
update=update,
1813-
error=error,
1814-
application=self,
1815-
job=job,
1816-
coroutine=coroutine,
1817-
)
1824+
try:
1825+
context = self.context_types.context.from_error(
1826+
update=update,
1827+
error=error,
1828+
application=self,
1829+
job=job,
1830+
coroutine=coroutine,
1831+
)
1832+
except Exception as exc:
1833+
_LOGGER.critical(
1834+
(
1835+
"Error while building CallbackContext for exception %s. "
1836+
"Exception will not be processed by error handlers."
1837+
),
1838+
error,
1839+
exc_info=exc,
1840+
)
1841+
return False
1842+
18181843
if not block or ( # If error handler has `block=False`, create a Task to run cb
18191844
block is DEFAULT_TRUE
18201845
and isinstance(self.bot, ExtBot)

telegram/ext/_jobqueue.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
except ImportError:
3232
APS_AVAILABLE = False
3333

34+
from telegram._utils.logging import get_logger
3435
from telegram._utils.repr import build_repr_with_selected_attrs
3536
from telegram._utils.types import JSONDict
3637
from telegram.ext._extbot import ExtBot
@@ -44,6 +45,7 @@
4445

4546

4647
_ALL_DAYS = tuple(range(7))
48+
_LOGGER = get_logger(__name__, class_name="JobQueue")
4749

4850

4951
class JobQueue(Generic[CCT]):
@@ -953,7 +955,16 @@ async def _run(
953955
self, application: "Application[Any, CCT, Any, Any, Any, JobQueue[CCT]]"
954956
) -> None:
955957
try:
956-
context = application.context_types.context.from_job(self, application)
958+
try:
959+
context = application.context_types.context.from_job(self, application)
960+
except Exception as exc:
961+
_LOGGER.critical(
962+
"Error while building CallbackContext for job %s. Job will not be run.",
963+
self._job,
964+
exc_info=exc,
965+
)
966+
return
967+
957968
await context.refresh_data()
958969
await self.callback(context)
959970
except Exception as exc:

tests/ext/test_application.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,3 +2421,83 @@ async def callback(update, context):
24212421
assert len(assertions) == 5
24222422
for key, value in assertions.items():
24232423
assert value, f"assertion '{key}' failed!"
2424+
2425+
async def test_process_update_exception_in_building_context(self, monkeypatch, caplog, app):
2426+
# Makes sure that exceptions in building the context don't stop the application
2427+
exception = ValueError("TestException")
2428+
original_from_update = CallbackContext.from_update
2429+
2430+
def raise_exception(update, application):
2431+
if update == 1:
2432+
raise exception
2433+
return original_from_update(update, application)
2434+
2435+
monkeypatch.setattr(CallbackContext, "from_update", raise_exception)
2436+
2437+
received_updates = set()
2438+
2439+
async def callback(update, context):
2440+
received_updates.add(update)
2441+
2442+
app.add_handler(TypeHandler(int, callback))
2443+
2444+
async with app:
2445+
with caplog.at_level(logging.CRITICAL):
2446+
await app.process_update(1)
2447+
2448+
assert received_updates == set()
2449+
assert len(caplog.records) == 1
2450+
record = caplog.records[0]
2451+
assert record.name == "telegram.ext.Application"
2452+
assert record.getMessage().startswith(
2453+
"Error while building CallbackContext for update 1"
2454+
)
2455+
assert record.levelno == logging.CRITICAL
2456+
10000
2457+
# Let's also check that no critical log is produced when the exception is not raised
2458+
caplog.clear()
2459+
with caplog.at_level(logging.CRITICAL):
2460+
await app.process_update(2)
2461+
2462+
assert received_updates == {2}
2463+
assert len(caplog.records) == 0
2464+
2465+
async def test_process_error_exception_in_building_context(self, monkeypatch, caplog, app):
2466+
# Makes sure that exceptions in building the context don't stop the application
2467+
exception = ValueError("TestException")
2468+
original_from_error = CallbackContext.from_error
2469+
2470+
def raise_exception(update, error, application, *args, **kwargs):
2471+
if error == 1:
2472+
raise exception
2473+
return original_from_error(update, error, application, *args, **kwargs)
2474+
2475+
monkeypatch.setattr(CallbackContext, "from_error", raise_exception)
2476+
2477+
received_errors = set()
2478+
2479+
async def callback(update, context):
2480+
received_errors.add(context.error)
2481+
2482+
app.add_error_handler(callback)
2483+
2484+
async with app:
2485+
with caplog.at_level(logging.CRITICAL):
2486+
await app.process_error(update=None, error=1)
2487+
2488+
assert received_errors == set()
2489+
assert len(caplog.records) == 1
2490+
record = caplog.records[0]
2491+
assert record.name == "telegram.ext.Application"
2492+
assert record.getMessage().startswith(
2493+
"Error while building CallbackContext for exception 1"
2494+
)
2495+
assert record.levelno == logging.CRITICAL
2496+
2497+
# Let's also check that no critical log is produced when the exception is not raised
2498+
caplog.clear()
2499+
with caplog.at_level(logging.CRITICAL):
2500+
await app.process_error(update=None, error=2)
2501+
2502+
assert received_errors == {2}
2503+
assert len(caplog.records) == 0

tests/ext/test_jobqueue.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -646,3 +646,44 @@ async def test_from_aps_job_missing_reference(self, job_queue):
646646
tg_job = Job.from_aps_job(aps_job)
647647
assert tg_job is job
648648
assert tg_job.job is aps_job
649+
650+
async def test_run_job_exception_in_building_context(
651+
self, monkeypatch, job_queue, caplog, app
652+
):
653+
# Makes sure that exceptions in building the context don't stop the application
654+
exception = ValueError("TestException")
655+
original_from_job = CallbackContext.from_job
656+
657+
def raise_exception(job, application):
658+
if job.data == 1:
659+
raise exception
660+
return original_from_job(job, application)
661+
662+
monkeypatch.setattr(CallbackContext, "from_job", raise_exception)
663+
664+
received_jobs = set()
665+
666+
async def job_callback(context):
667+
received_jobs.add(context.job.data)
668+
669+
with caplog.at_level(logging.CRITICAL):
670+
job_queue.run_once(job_callback, 0.1, data=1)
671+
await asyncio.sleep(0.2)
672+
673+
assert received_jobs == set()
674+
assert len(caplog.records) == 1
675+
record = caplog.records[0]
676+
assert record.name == "telegram.ext.JobQueue"
677+
assert record.getMessage().startswith(
678+
"Error while building CallbackContext for job job_callback"
679+
)
680+
assert record.levelno == logging.CRITICAL
681+
682+
# Let's also check that no critical log is produced when the exception is not raised
683+
caplog.clear()
684+
with caplog.at_level(logging.CRITICAL):
685+
job_queue.run_once(job_callback, 0.1, data=2)
686+
await asyncio.sleep(0.2)
687+
688+
assert received_jobs == {2}
689+
assert len(caplog.records) == 0

0 commit comments

Comments
 (0)
0