mirror of
https://github.com/kennethreitz/responder.git
synced 2026-07-21 18:39:29 +00:00
9c08451904
## Summary Follow-up to #610 from the same audit. Two remaining spots invoked **synchronous** handlers directly on the event loop, so a blocking call in any of them stalled the whole server: - Sync `startup`/`shutdown` event handlers — `Router.trigger_event` (`routes.py`) - Sync exception handlers — `_wrap_exc_handler`'s adapter (`api.py`) Both now route through `run_in_threadpool`, matching how the rest of the framework dispatches sync callables (views, hooks, auth, dependency providers). ## Tests New `tests/test_sync_handlers_offloop.py` proves each path runs off the loop, using the fact that `asyncio.get_running_loop()` succeeds on the loop thread but raises `RuntimeError` inside a threadpool worker: - sync startup handler runs off-loop - sync shutdown handler runs off-loop - sync exception handler runs off-loop (and still produces its response) Full suite: 743 passed, 1 skipped (php not installed); ruff clean; mypy clean. ## Notes Changelog entry added under `[Unreleased]` — no version bump in this PR; cut the release when you're ready. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
66 lines
1.6 KiB
Python
66 lines
1.6 KiB
Python
"""Sync lifecycle and exception handlers must run off the event loop.
|
|
|
|
A blocking sync handler executed directly on the loop stalls the whole
|
|
server. Both paths now route through ``run_in_threadpool``. We detect this
|
|
by calling ``asyncio.get_running_loop()`` inside the handler: it succeeds on
|
|
the loop thread but raises ``RuntimeError`` inside a threadpool worker.
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
import responder
|
|
|
|
|
|
def _ran_off_loop():
|
|
try:
|
|
asyncio.get_running_loop()
|
|
return False
|
|
except RuntimeError:
|
|
return True
|
|
|
|
|
|
def test_sync_startup_handler_runs_off_event_loop():
|
|
api = responder.API(allowed_hosts=[";"], session_https_only=False)
|
|
seen = {}
|
|
|
|
@api.on_event("startup")
|
|
def startup():
|
|
seen["off_loop"] = _ran_off_loop()
|
|
|
|
with api.requests:
|
|
pass
|
|
|
|
assert seen["off_loop"] is True
|
|
|
|
|
|
def test_sync_shutdown_handler_runs_off_event_loop():
|
|
api = responder.API(allowed_hosts=[";"], session_https_only=False)
|
|
seen = {}
|
|
|
|
@api.on_event("shutdown")
|
|
def shutdown():
|
|
seen["off_loop"] = _ran_off_loop()
|
|
|
|
with api.requests:
|
|
pass
|
|
|
|
assert seen["off_loop"] is True
|
|
|
|
|
|
def test_sync_exception_handler_runs_off_event_loop():
|
|
api = responder.API(allowed_hosts=[";"], session_https_only=False)
|
|
seen = {}
|
|
|
|
@api.exception_handler(ValueError)
|
|
def handle(req, resp, exc):
|
|
seen["off_loop"] = _ran_off_loop()
|
|
resp.media = {"error": "handled"}
|
|
|
|
@api.route("/boom")
|
|
def boom(req, resp):
|
|
raise ValueError("nope")
|
|
|
|
r = api.requests.get("/boom")
|
|
assert seen["off_loop"] is True
|
|
assert r.json() == {"error": "handled"}
|