Skip to content

nest_asyncio.apply() breaks Python 3.14: asyncio task corruption → HTTP 500 on every static-asset request #2952

Description

@pidefrem

Bug

chainlit run fails on Python 3.14. The browser shows a white page because
every static-asset request (/assets/*.js, /public/*) returns HTTP 500:

anyio.NoEventLoopError: Not currently running on any asynchronous event loop.
Available async backends: asyncio, trio

Root cause

Corrected 2026-08-05. This section previously attributed the breakage to the loop=
keyword being removed from asyncio.ensure_future in 3.14
(bpo-39529). That attribution is incorrect: the
parameter is present and accepted on 3.14.1. The symptom, the failure chain and the fix
are unchanged.

chainlit/cli/__init__.py calls nest_asyncio.apply() at module import time. apply()
rebinds the global task and future classes to their pure-Python implementations, while
asyncio.current_task stays bound to the C accelerator, which reads a registry the
pure-Python Task never populates.

Verified on 3.14.1 with nest_asyncio==1.6.0:

before apply: asyncio.Task is <class '_asyncio.Task'>
after  apply: asyncio.Task is <class 'asyncio.tasks.Task'>

_py_current_task(): <Task pending name='Task-1' …>
_c_current_task():  None
asyncio.current_task() is _c_current_task: True

asyncio.current_task() therefore returns None inside a running coroutine. anyio
indexes _task_states[host_task] — a WeakKeyDictionary — with that None:

TypeError: cannot create weak reference to 'NoneType' object

which surfaces as anyio.NoEventLoopError.

The failure chain:

  1. chainlit runimport chainlit.clinest_asyncio.apply() at module level
  2. apply() rebinds asyncio.Task/asyncio.Future to the pure-Python classes, while
    current_task() keeps reading the C registry → returns None
  3. Browser requests /assets/index-*.js
  4. FileResponse.__call__anyio.to_thread.run_sync(os.stat, path)
  5. anyiosniffio.current_async_library()asyncio.current_task()None
  6. anyio.NoEventLoopError → HTTP 500 → no JS → white page

The breakage is specific to 3.14 because nest_asyncio suspends the current task by
clearing asyncio.tasks._current_tasks, and 3.14 moved current-task tracking into the
thread state, making that suspension a no-op.

asyncio.ensure_future(..., loop=...) is not implicated. On 3.14.1 the signature is still
(coro_or_future, *, loop=None), the call succeeds, and nest_asyncio.apply() does not
raise:

ensure_future signature: (coro_or_future, *, loop=None)
ensure_future(loop=...) accepted: True
nest_asyncio.apply() raised: False

No release of nest_asyncio can fix this: the class rebind is the library's mechanism, not
a bug in it. The library has also had no release since 2023.

Fix

Remove nest_asyncio, and eliminate the reentrancy it was compensating for.

asyncio.run(start()) at the bottom of run_chainlit() is a top-level entry point, does
not run inside another event loop, and never needed a reentrant loop, so the apply() call
is dropped there.

Two call sites did depend on reentrancy:

  • WebsocketSession.get_config() called loop.run_until_complete() from inside the
    running loop. It becomes an async def resolve_config() awaited from the async
    connect() handler.
  • cl.run_sync() on the main thread while the loop is running drives the loop through a
    new chainlit/_reentrant_loop.py, suspending the calling task around the nested run
    rather than patching anything. asyncio.Task and asyncio.Future keep their C
    implementations, so current_task() stays correct and anyio is unaffected.

Fix, updated pyproject.toml, regenerated lockfile and regression tests:
#2953

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendPertains to the Python backend.bugSomething isn't workingneeds-triage

    Type

    Projects

    Status
    Todo

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions