Repository navigation
asyncio: default exception handler does not run in the task's context #159303
Description
Bug report
gh-96704 (PR #96756, 3.12) changed BaseEventLoop.call_exception_handler() to run the handler inside task.get_context() / handle.get_context(). The change was applied only to the branch where a custom handler is set with loop.set_exception_handler(). When no handler is set, default_exception_handler() is called directly, in whatever context is current at that moment.
cpython/Lib/asyncio/base_events.py
Lines 1942 to 1952 in 85bca2d
| if self._exception_handler is None: | |
| try: | |
| self.default_exception_handler(context) | |
| except (SystemExit, KeyboardInterrupt): | |
| raise | |
| except BaseException: | |
| # Second protection layer for unexpected errors | |
| # in the default implementation, as well as for subclassed | |
| # event loops with overloaded "default_exception_handler". | |
| logger.error('Exception in default exception handler', | |
| exc_info=True) |
import asyncio
import contextvars
import logging
import sys
request_id = contextvars.ContextVar("request_id", default=None)
class RequestIdFilter(logging.Filter):
def filter(self, record):
record.request_id = request_id.get()
return True
class NoTracebackFormatter(logging.Formatter):
def formatException(self, exc_info):
return ""
handler = logging.StreamHandler(sys.stdout)
handler.addFilter(RequestIdFilter())
handler.setFormatter(NoTracebackFormatter(" logged with request_id=%(request_id)s"))
logging.getLogger("asyncio").addHandler(handler)
logging.getLogger("asyncio").propagate = False
async def fail():
request_id.set("req-1")
raise ValueError
async def other_request():
request_id.set("req-2")
task = asyncio.ensure_future(fail())
await asyncio.sleep(0.01)
del task # "Task exception was never retrieved" is reported here
async def main(custom_handler):
if custom_handler:
asyncio.get_running_loop().set_exception_handler(
lambda loop, context: loop.default_exception_handler(context)
)
await asyncio.create_task(other_request())
print(sys.version)
print("default handler (expected req-1):")
asyncio.run(main(custom_handler=False))
print("custom handler calling default_exception_handler (expected req-1):")
asyncio.run(main(custom_handler=True))Output (identical on 3.12.7, 3.13.16, 3.14.8, 3.15.0):
default handler (expected req-1):
logged with request_id=req-2
custom handler calling default_exception_handler (expected req-1):
logged with request_id=req-1
Expected: the default handler runs in the failed task's (or handle's) context, as custom handlers have since gh-96704.
Workaround: loop.set_exception_handler(lambda loop, context: loop.default_exception_handler(context))
CPython versions tested on:
3.15
Operating systems tested on:
macOS
Activity