Skip to content

Don't re-await main_task in run_app finally when it's already done#12493

Open
AndrewKarelin wants to merge 3 commits intoaio-libs:masterfrom
AndrewKarelin:fix-run_app-traceback
Open

Don't re-await main_task in run_app finally when it's already done#12493
AndrewKarelin wants to merge 3 commits intoaio-libs:masterfrom
AndrewKarelin:fix-run_app-traceback

Conversation

@AndrewKarelin
Copy link
Copy Markdown

What

In web.run_app's finally block, only call main_task.cancel() and re-run loop.run_until_complete(main_task) when the task is still running.

 finally:
     try:
-        main_task.cancel()
-        with suppress(asyncio.CancelledError):
-            loop.run_until_complete(main_task)
+        if not main_task.done():
+            main_task.cancel()
+            with suppress(asyncio.CancelledError):
+                loop.run_until_complete(main_task)
     finally:
         _cancel_tasks(asyncio.all_tasks(loop), loop)
         loop.run_until_complete(loop.shutdown_asyncgens())
         loop.close()

Why

The cancel/re-await branch is intended for the GracefulExit / KeyboardInterrupt path, where the first run_until_complete returned without draining main_task. On the startup-failure path, main_task is already done() with the exception, and re-running it strips most of the traceback before it reaches the caller.

Future.result() on an already-finished task does raise self._exception.with_traceback(self._exception_tb). The with_traceback call resets exc.__traceback__ to the earlier-saved _exception_tb, which by this point reflects only the outermost frames. The deep frames the user actually needs (the cleanup_ctx/on_startup body, the user code that raised) are gone.

Skipping the second run_until_complete when the task is already done preserves the full traceback in the startup-failure case and is a no-op in the graceful-shutdown case (the task is still running there).

Versions affected

  • Last good: 3.10.5
  • First bad: 3.10.6 (regression)
  • Still bad: 3.13.5 and current master

Reproducer

import traceback, sys
from aiohttp import web

async def bad_ctx(app):
    raise RuntimeError("boom from ctx startup")
    yield

app = web.Application()
app.cleanup_ctx.append(bad_ctx)
try:
    web.run_app(app, host='127.0.0.1', port=0, print=None)
except Exception:
    traceback.print_exception(*sys.exc_info())

Before this PR (3.10.6..3.13.5): 3 frames, no bad_ctx shown. Traceback stops at loop.run_until_complete.

After this PR: 9 frames, including:

File ".../aiohttp/web_app.py", line 600, in _on_startup
    await it.__anext__()
File "repro.py", line 5, in bad_ctx
    raise RuntimeError("boom from ctx startup")
RuntimeError: boom from ctx startup

The same chain you get today via asyncio.run(runner.setup()) — the bug is strictly in web.run_app's finally.

Test

Added test_run_app_preserves_startup_traceback in tests/test_run_app.py: registers a failing cleanup_ctx, asserts that the user function's frame appears in the traceback walked from the propagated exception.

Compatibility

  • No public API change.
  • GracefulExit/KeyboardInterrupt path: identical behavior (task is not done at that point, so the inner block runs as before).
  • Startup-failure path: traceback now contains the actual cause, instead of stopping at loop.run_until_complete.
  • Normal shutdown path: main_task is done, second run_until_complete is skipped — same observable behavior, slightly cheaper.

CHANGES file will be added once the PR number is assigned.

The cancel/re-await branch in run_app's finally is meant for the
GracefulExit / KeyboardInterrupt path, where main_task is still running.
On the startup-failure path, main_task is already done with the
exception, and re-running loop.run_until_complete(main_task) calls
Future.result(), which executes
``raise self._exception.with_traceback(self._exception_tb)``. That
with_traceback call resets exc.__traceback__ to the saved
_exception_tb, which by this point reflects only the outermost frames.
The deep frames (cleanup_ctx / on_startup body, the user code that
raised) are lost before the exception reaches the caller.

Skip the second run_until_complete when main_task is already done.
This preserves the full traceback in the startup-failure case and is
a no-op for the graceful-shutdown path.

Regression since 3.10.6.
@AndrewKarelin AndrewKarelin requested a review from asvetlov as a code owner May 8, 2026 14:58
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided There is a change note present in this PR label May 8, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.92%. Comparing base (7d534ab) to head (85aba50).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##           master   #12493   +/-   ##
=======================================
  Coverage   98.92%   98.92%           
=======================================
  Files         134      134           
  Lines       46750    46761   +11     
  Branches     2429     2430    +1     
=======================================
+ Hits        46248    46259   +11     
  Misses        373      373           
  Partials      129      129           
Flag Coverage Δ
CI-GHA 98.98% <100.00%> (+<0.01%) ⬆️
OS-Linux 98.72% <100.00%> (-0.01%) ⬇️
OS-Windows 96.98% <100.00%> (+<0.01%) ⬆️
OS-macOS 97.89% <100.00%> (+<0.01%) ⬆️
Py-3.10.11 97.38% <100.00%> (-0.01%) ⬇️
Py-3.10.20 97.86% <100.00%> (+<0.01%) ⬆️
Py-3.11.15 98.12% <100.00%> (+<0.01%) ⬆️
Py-3.11.9 97.65% <100.00%> (+<0.01%) ⬆️
Py-3.12.10 97.73% <100.00%> (+<0.01%) ⬆️
Py-3.12.13 98.21% <100.00%> (+<0.01%) ⬆️
Py-3.13.13 98.44% <100.00%> (-0.01%) ⬇️
Py-3.14.4 98.50% <100.00%> (+<0.01%) ⬆️
Py-3.14.4t 97.52% <100.00%> (-0.01%) ⬇️
Py-pypy3.11.15-7.3.21 97.35% <92.85%> (-0.01%) ⬇️
VM-macos 97.89% <100.00%> (+<0.01%) ⬆️
VM-ubuntu 98.72% <100.00%> (-0.01%) ⬇️
VM-windows 96.98% <100.00%> (+<0.01%) ⬆️
cython-coverage 38.06% <0.00%> (-0.02%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@codspeed-hq
Copy link
Copy Markdown

codspeed-hq Bot commented May 8, 2026

Merging this PR will not alter performance

✅ 67 untouched benchmarks
⏩ 4 skipped benchmarks1


Comparing AndrewKarelin:fix-run_app-traceback (85aba50) with master (7d534ab)

Open in CodSpeed

Footnotes

  1. 4 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bot:chronographer:provided There is a change note present in this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant