Summary
Hypercorn 0.18.0 does not work with Python 3.14. Several asyncio APIs that Hypercorn uses (asyncio.TaskGroup, asyncio.timeout, asyncio.wait_for) now require a proper task context in 3.14, but Hypercorn's asyncio.Runner doesn't establish one.
Environment
- Hypercorn 0.18.0
- Python 3.14.4 (Alpine Linux, GCC 15.2.0)
- Running with
--workers 2 --worker-class asyncio
Errors
On startup, Hypercorn crashes with errors like:
RuntimeError: asyncio.TaskGroup requires a running event loop with a current task
and
RuntimeError: timeout() requires a running event loop with a current task
These come from worker_serve() in hypercorn/asyncio/run.py and the lifespan handling in hypercorn/asyncio/lifespan.py.
Root cause
Hypercorn's _run() function uses asyncio.Runner.run() which runs the coroutine directly via loop.run_until_complete(). Python 3.14 now enforces that TaskGroup, timeout(), and wait_for() have a proper asyncio.current_task() context. Since Runner doesn't wrap work in an explicit task the way asyncio.run() does internally, these APIs fail.
Additionally, on Python 3.14 (Alpine/GCC build specifically), asyncio.current_task() returns None in daemon threads even after explicit loop.create_task() + loop.run_until_complete(). This appears to be a CPython bug with the C-level task registry, but it means any code in Hypercorn workers that spawns daemon threads with their own event loops (e.g., background task executors using httpx.AsyncClient) will also break, since anyio and sniffio depend on current_task() being non-None.
Workaround
We are currently monkey-patching three things to run Hypercorn 0.18.0 on Python 3.14:
_run - replaced with a version that wraps work in asyncio.run() instead of using Runner directly, establishing a proper task context
worker_serve - replaced with a version that avoids TaskGroup and asyncio.timeout, using asyncio.wait() and asyncio.ensure_future() instead
asyncio.wait_for - globally replaced with a compatible implementation using asyncio.wait() + asyncio.ensure_future()
The full patch is ~300 lines. Happy to share it or submit a PR if that would be helpful.
Suggested fix
The minimal fix would be to ensure _run() wraps the main coroutine in an explicit asyncio.Task (e.g., via asyncio.run() instead of Runner.run()), so that asyncio.current_task() returns a valid task throughout Hypercorn's lifecycle.
Summary
Hypercorn 0.18.0 does not work with Python 3.14. Several asyncio APIs that Hypercorn uses (
asyncio.TaskGroup,asyncio.timeout,asyncio.wait_for) now require a proper task context in 3.14, but Hypercorn'sasyncio.Runnerdoesn't establish one.Environment
--workers 2 --worker-class asyncioErrors
On startup, Hypercorn crashes with errors like:
and
These come from
worker_serve()inhypercorn/asyncio/run.pyand the lifespan handling inhypercorn/asyncio/lifespan.py.Root cause
Hypercorn's
_run()function usesasyncio.Runner.run()which runs the coroutine directly vialoop.run_until_complete(). Python 3.14 now enforces thatTaskGroup,timeout(), andwait_for()have a properasyncio.current_task()context. SinceRunnerdoesn't wrap work in an explicit task the wayasyncio.run()does internally, these APIs fail.Additionally, on Python 3.14 (Alpine/GCC build specifically),
asyncio.current_task()returnsNonein daemon threads even after explicitloop.create_task()+loop.run_until_complete(). This appears to be a CPython bug with the C-level task registry, but it means any code in Hypercorn workers that spawns daemon threads with their own event loops (e.g., background task executors usinghttpx.AsyncClient) will also break, sinceanyioandsniffiodepend oncurrent_task()being non-None.Workaround
We are currently monkey-patching three things to run Hypercorn 0.18.0 on Python 3.14:
_run- replaced with a version that wraps work inasyncio.run()instead of usingRunnerdirectly, establishing a proper task contextworker_serve- replaced with a version that avoidsTaskGroupandasyncio.timeout, usingasyncio.wait()andasyncio.ensure_future()insteadasyncio.wait_for- globally replaced with a compatible implementation usingasyncio.wait()+asyncio.ensure_future()The full patch is ~300 lines. Happy to share it or submit a PR if that would be helpful.
Suggested fix
The minimal fix would be to ensure
_run()wraps the main coroutine in an explicitasyncio.Task(e.g., viaasyncio.run()instead ofRunner.run()), so thatasyncio.current_task()returns a valid task throughout Hypercorn's lifecycle.