Skip to content

Cello v1.2.4 — Critical Async Handler Fix

Release Date: June 14, 2026 License: MIT Python: 3.12+


Overview

Cello v1.2.4 fixes a critical regression introduced in v1.2.1 where all async def route handlers silently returned HTTP 500 and emitted:

RuntimeWarning: coroutine 'handler_name' was never awaited

Drop-in upgrade from v1.2.3. No API changes.


Root Cause

In v1.2.1 the server startup was changed from pyo3_asyncio::tokio::run to py.allow_threads + tokio::block_on to fix port binding on Python 3.12+. That fix was correct, but it had an unintended side effect: pyo3_asyncio was no longer initialised, so pyo3_asyncio::tokio::into_future (used in handler.rs to drive async handlers) failed silently on every request — returning 500 and dropping the unawaited coroutine.

v1.2.1 change (lib.rs):
  - pyo3_asyncio::tokio::run(py, server_future)   ← initialises pyo3_asyncio
  + py.allow_threads(|| rt.block_on(server_future))  ← does NOT initialise it

handler.rs (unchanged):
  pyo3_asyncio::tokio::into_future(coro)  ← fails: pyo3_asyncio not ready → 500

Fix

handler.rs Phase 2 now drives coroutines via tokio::task::spawn_blocking + asyncio.run() instead of pyo3_asyncio::tokio::into_future:

// Before (broken since v1.2.1):
let future = Python::with_gil(|py| {
    pyo3_asyncio::tokio::into_future(raw_result.as_ref(py))  // ← failed silently
})?;
future.await?

// After (v1.2.4):
let (tx, rx) = tokio::sync::oneshot::channel();
let coro = raw_result;
tokio::task::spawn_blocking(move || {
    let result = Python::with_gil(|py| {
        let asyncio = py.import("asyncio")?;
        asyncio.call_method1("run", (coro.as_ref(py),))
            .map(|r| r.into_py(py))
    });
    let _ = tx.send(result);
});
rx.await??

The Tokio thread is released during spawn_blocking, so other requests can be served while the coroutine runs. asyncio.run() creates a fresh event loop per call (~0.1 ms overhead).


Files Changed

File Change
src/handler.rs Replace pyo3_asyncio::tokio::into_future with spawn_blocking + asyncio.run()
tests/verify_async_client.py New test — all HTTP methods via local echo server
Cargo.toml, pyproject.toml, python/cello/__init__.py Version → 1.2.4

Upgrade

pip install --upgrade cello-framework==1.2.4

No code changes required — all async def handlers that were silently returning 500 will now work correctly after upgrading.


Impact

Any application using async def handlers on Cello v1.2.1, v1.2.2, or v1.2.3 was affected. Sync (def) handlers were not impacted.