Browse Source
When a decorator uses `@wraps(func)` on an async wrapper around a sync function, FastAPI incorrectly treats the handler as sync because `is_coroutine_callable` uses `inspect.unwrap()` which follows `__wrapped__` back to the original sync function. This fix checks the actual callable first (not the unwrapped version) to correctly detect if the wrapper itself is async/generator, and only falls back to the unwrapped version for cases like `partial`. Fixes #14444pull/14445/head
2 changed files with 85 additions and 0 deletions
@ -0,0 +1,65 @@ |
|||
"""Test for async wrapper around sync function. |
|||
|
|||
This tests the issue reported in #14442 where using functools.wraps on an async |
|||
wrapper around a sync function causes FastAPI to incorrectly treat the handler |
|||
as sync, resulting in: ValueError: [TypeError("'coroutine' object is not iterable"), ...] |
|||
|
|||
The issue is that Dependant.is_coroutine_callable uses inspect.unwrap() which follows |
|||
__wrapped__ back to the original sync function, instead of checking if the actual |
|||
registered handler is async. |
|||
""" |
|||
|
|||
from functools import wraps |
|||
|
|||
import pytest |
|||
from fastapi import Depends, FastAPI |
|||
from fastapi.testclient import TestClient |
|||
|
|||
|
|||
def async_wrap(func): |
|||
"""A decorator that wraps a sync function with an async handler.""" |
|||
|
|||
@wraps(func) |
|||
async def wrapper(*args, **kwargs): |
|||
func(*args, **kwargs) |
|||
return "OK" |
|||
|
|||
return wrapper |
|||
|
|||
|
|||
app = FastAPI() |
|||
|
|||
|
|||
@app.get("/") |
|||
@async_wrap |
|||
def index(): |
|||
"""A simple sync page function wrapped with async handler.""" |
|||
print("Hello!") |
|||
|
|||
|
|||
def sync_dependency(): |
|||
return "dep_value" |
|||
|
|||
|
|||
@app.get("/with-dependency") |
|||
@async_wrap |
|||
def with_dependency(value: str = Depends(sync_dependency)): |
|||
"""A sync function with dependency, wrapped with async handler.""" |
|||
print(f"Got: {value}") |
|||
|
|||
|
|||
client = TestClient(app) |
|||
|
|||
|
|||
def test_async_wrapper_around_sync_function(): |
|||
"""Test that async wrapper around sync function works correctly.""" |
|||
response = client.get("/") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == "OK" |
|||
|
|||
|
|||
def test_async_wrapper_with_dependency(): |
|||
"""Test that async wrapper with dependency works correctly.""" |
|||
response = client.get("/with-dependency") |
|||
assert response.status_code == 200, response.text |
|||
assert response.json() == "OK" |
|||
Loading…
Reference in new issue