Skip to content

Conversation

@tiangolo
Copy link
Member

@tiangolo tiangolo commented Dec 2, 2025

♻️ Refactor internals, update is_coroutine check to reuse internal supported variants (unwrap, check class)

@tiangolo tiangolo enabled auto-merge (squash) December 2, 2025 13:38
@tiangolo tiangolo merged commit 247ef32 into master Dec 2, 2025
32 checks passed
@tiangolo tiangolo deleted the dependant-is_coroutine branch December 2, 2025 13:43
@potiuk

This comment was marked as resolved.

@potiuk
Copy link

potiuk commented Dec 3, 2025

Ok. False alarm. Sorry for bothering you - but you might be interested and maybe also chime in. Seems to be cadwyn issue that it badly wraps sync calls and the calls are recognized as coroutines zmievsa/cadwyn#309

@tirkarthi
Copy link

After some debugging, this also seems to break below code without cadwyn dependency which is similar to the pattern in Airflow + cadwyn does. The below code works in 0.123.4 and breaks in 0.123.5.

from functools import wraps

from fastapi import FastAPI
from pydantic import BaseModel


class SampleModel(BaseModel):
    name: str
    age: int

app = FastAPI()

def auth_required(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@app.get("/")
@auth_required # Custom decorator
def root():
    return {"message": "Hello World"}

0.123.5

      INFO   127.0.0.1:39848 - "GET / HTTP/1.1" 500
     ERROR   Exception in ASGI application
Traceback (most recent call last):
  File "/tmp/env/lib/python3.11/site-packages/fastapi/encoders.py", line 337, in jsonable_encoder
    data = dict(obj)
           ^^^^^^^^^
TypeError: 'coroutine' object is not iterable

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/tmp/env/lib/python3.11/site-packages/fastapi/encoders.py", line 342, in jsonable_encoder
    data = vars(obj)
           ^^^^^^^^^
TypeError: vars() argument must have __dict__ attribute

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/tmp/env/lib/python3.11/site-packages/uvicorn/protocols/http/httptools_impl.py", line 409, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/env/lib/python3.11/site-packages/uvicorn/middleware/proxy_headers.py", line 60, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/env/lib/python3.11/site-packages/fastapi/applications.py", line 1139, in __call__
    await super().__call__(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/applications.py", line 107, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/middleware/errors.py", line 186, in __call__
    raise exc
  File "/tmp/env/lib/python3.11/site-packages/starlette/middleware/errors.py", line 164, in __call__
    await self.app(scope, receive, _send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/middleware/exceptions.py", line 63, in __call__
    await wrap_app_handling_exceptions(self.app, conn)(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File "/tmp/env/lib/python3.11/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File "/tmp/env/lib/python3.11/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/routing.py", line 716, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/routing.py", line 736, in app
    await route.handle(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/routing.py", line 290, in handle
    await self.app(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/fastapi/routing.py", line 119, in app
    await wrap_app_handling_exceptions(app, request)(scope, receive, send)
  File "/tmp/env/lib/python3.11/site-packages/starlette/_exception_handler.py", line 53, in wrapped_app
    raise exc
  File "/tmp/env/lib/python3.11/site-packages/starlette/_exception_handler.py", line 42, in wrapped_app
    await app(scope, receive, sender)
  File "/tmp/env/lib/python3.11/site-packages/fastapi/routing.py", line 105, in app
    response = await f(request)
               ^^^^^^^^^^^^^^^^
  File "/tmp/env/lib/python3.11/site-packages/fastapi/routing.py", line 407, in app
    content = await serialize_response(
              ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/env/lib/python3.11/site-packages/fastapi/routing.py", line 273, in serialize_response
    return jsonable_encoder(response_content)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/tmp/env/lib/python3.11/site-packages/fastapi/encoders.py", line 345, in jsonable_encoder
    raise ValueError(errors) from e
ValueError: [TypeError("'coroutine' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')]

@dolfinus
Copy link
Contributor

dolfinus commented Dec 3, 2025

But why def root(): is sync in the first place?

@tirkarthi
Copy link

@dolfinus This was during debugging the issue to remove cadwyn dependency and to use only fastapi to see if there are any issues. routes use def instead of async def since Airflow performs blocking operations inside the routes.

apache/airflow#43718 (comment)
apache/airflow#43797

@dolfinus
Copy link
Contributor

dolfinus commented Dec 3, 2025

routes use def instead of async def since Airflow performs blocking operations inside the routes.

And wrapping them with:

async def wrapper(*args, **kwargs)
  return func(*args, **kwargs)

will make FastAPI run the syncronous code inside event loop, blocking it. It's only then wrapper method is def then FastAPI can detect that function should be run in thread pool.

@YuriiMotov
Copy link
Member

I think the issue is valid.
We may want to wrap sync function by async function to perform some async actions before\after executing sync function.
To avoid blocking event loop we can use await run_in_threadpool(func, ...)

@potiuk
Copy link

potiuk commented Dec 3, 2025

Ah cool. So that might mean our CI helped to find an issue :) .. Goood :)

@dolfinus
Copy link
Contributor

dolfinus commented Dec 3, 2025

We may want to wrap sync function by async function to perform some async actions before\after executing sync function.

That should be done very, very carefully, to avoid blocking event loop.

@tiangolo
Copy link
Member Author

tiangolo commented Dec 3, 2025

Thanks for the report folks! Let me take a look into it with your reproduction example (that helps a lot!).

@tiangolo
Copy link
Member Author

tiangolo commented Dec 3, 2025

Wait, I'm actually confused...

def auth_required(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

This means wrapper would be called on the main thread, async, but it is wrapping a potentially sync function func, and calling it directly, potentially blocking the event loop. But that is done on the non-FastAPI side...

Not sure how that actually works on Cadwyn and what would be expected to be done there 🤔


I think @YuriiMotov found a way to replicate it, and still FastAPI errors out when it shouldn't, let me check with that.

@tirkarthi
Copy link

From my understanding cadwyn just assumes everything will be async and wraps it with async. In Airflow's case blocking operations are performed inside the routes like sqlalchemy db calls which are blocking using sync db driver and session. The routes used to be async but were made sync to be migrated later once sqlalchemy and all other operations become async. The issue seems to be that earlier the iscoroutinefunction was made on wrapper and now occurs on func where wrapper and func might differ in their synchronous nature but func is ultimately the function executed.

@tiangolo
Copy link
Member Author

tiangolo commented Dec 3, 2025

Yep, I'm here now:

import inspect
import sys
from functools import wraps

from fastapi import FastAPI
from fastapi.concurrency import run_in_threadpool
from pydantic import BaseModel

if sys.version_info >= (3, 13):  # pragma: no cover
    from inspect import iscoroutinefunction
else:  # pragma: no cover
    from asyncio import iscoroutinefunction


class SampleModel(BaseModel):
    name: str
    age: int


app = FastAPI()


def wrap(func):
    @wraps(func)
    async def wrapper(*args, **kwargs):
        if inspect.isroutine(func) and iscoroutinefunction(func):
            return await func(*args, **kwargs)
        if inspect.isclass(func):
            return await run_in_threadpool(func, *args, **kwargs)
        dunder_call = getattr(func, "__call__", None)  # noqa: B004
        if iscoroutinefunction(dunder_call):
            return await dunder_call(*args, **kwargs)
        return await run_in_threadpool(func, *args, **kwargs)

    return wrapper


@app.get("/")
@wrap
def root():
    return {"message": "Hello World"}

That more or less simulates what Cadwyn would be doing, and still fails. We're on it. @YuriiMotov will help me write a set of tests with combinations of sync/async wrapper scenarios (for path operations and dependencies), and we'll continue from there.

@falkoschindler
Copy link

We're also experiencing problems with synchronous NiceGUI page functions (see zauberzeug/nicegui#5535).

Therefore I investigated the problem and came up with this MRE:

from functools import wraps
from fastapi import FastAPI

app = FastAPI()

def my_decorator(func):
    @wraps(func)
    async def wrapper():
        func()
        return 'Ok'
    return wrapper

@app.get('/')
@my_decorator
def index():
    print('Hello!')

@YuriiMotov
Copy link
Member

Maybe better move this conversation to discussion: #14442

@tiangolo
Copy link
Member Author

tiangolo commented Dec 4, 2025

This should be fixed by #14448

It's now released in FastAPI 0.123.6 🎉

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants