Describe the bug
If you use an async class method as a dependency, a ValueError is thrown. It doesn't happen for a non-async method.
Complete error: ValueError: [KeyError(<class 'coroutine'>), TypeError("'coroutine' object is not iterable"), TypeError('vars() argument must have __dict__ attribute')] (at fastapi/encoders.py:106)
To Reproduce
from fastapi import Depends, FastAPI
from starlette.requests import Request
class DependencyClass:
async def async_dep(self, request: Request):
return True
def sync_dep(self, request: Request):
return True
app = FastAPI()
dependency = DependencyClass()
# Error
@app.get('/async-dep')
def async_dep(r=Depends(dependency.async_dep)):
return r
# Everything is fine
@app.get('/sync-dep')
def sync_dep(r=Depends(dependency.sync_dep)):
return r
Expected behavior
The async class method dependency should be called and its return value injected.
Environment:
Additional context
I believe the issue comes from here:
Indeed, inspect.isfunction(call) will return False in case of a class method. Hence, it is sent to run_in_threadpool, which never awaits the coroutine, and we end up trying to serialize it instead of its result (hence the ValueError).
Changing the check by:
if inspect.isfunction(call) or inspect.ismethod(call):
solves the issue. I can make a PR with the fix and unit tests if it helps.
not sure what the "right" behavior should be but in your async_dep (wich itself is not async) you return a coroutine r without awaiting it so it's kind of expected that it doesn't return it's value, is it ?
somehting like this "works" even if I fail to see the use case
import asyncio
import uvicorn
from fastapi import Depends, FastAPI
from starlette.requests import Request
class DependencyClass:
async def async_dep(self, request: Request):
await asyncio.sleep(0)
return False
def sync_dep(self, request: Request):
return True
app = FastAPI()
dependency = DependencyClass()
# Error
@app.get('/async-dep')
async def authenticate(r=Depends(dependency.async_dep)):
s = await r
return s
# Everything is fine
@app.get('/sync-dep')
def authenticate(r=Depends(dependency.sync_dep)):
return r
if __name__ == '__main__':
uvicorn.run("679_async_dep_class:app", reload= True)
Well yes, for the sake of simplicity in the example, my async method don't do anything async, but still is a coroutine that needs to be awaited.
The use case is inspired from the async class dependencies, like OAuth2PasswordBearer:
__call__ is async here and, when you inject this dependency, we do get param, we don't need to await it in the controller function.
So, I think we should be able to do something similar with class methods. For example:
class Authentication:
def __init__(self, params):
self.params = params
async def get_user(self, request: Request) -> User:
return await self._authenticate(request)
async def get_active_user(self, request: Request) -> User:
user = await self._authenticate(request)
if not user.is_active:
raise HTTPException()
return user
async def _authenticate(self, request: Request) -> User:
# Do some authentication logic
return user
authentication = Authentication(params)
@app.get('/active-user')
def active_user(user=Depends(authentication.get_active_user)):
return user
This is useful to share common logic between all the dependencies while providing some specialized behaviour.
IMO, this is clearly a bug as it works flawlessly with async __call__ and non-async methods.
makes lot of sense indeed, was stuck on your version before edit
This makes sense to me; @frankie567 I think it's worth a PR.
The fix is merged and released in 0.44.0. Let's close it 馃憤Thanks @tiangolo!
Thanks for the help here everyone! :clap: :bow:
Thanks for reporting back and closing the issue @frankie567 :+1: And thanks for the PR/fix! :tada: