From 38786d190566d721714491d01d810a4930dd2028 Mon Sep 17 00:00:00 2001 From: Niler Barcelos Date: Tue, 17 Mar 2026 14:20:48 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20AssertionError=20when=20us?= =?UTF-8?q?ing=20TestClient=20directly=20with=20APIRouter=20(#14128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fastapi/routing.py | 5 +++ tests/test_router_direct_testclient.py | 62 ++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/test_router_direct_testclient.py diff --git a/fastapi/routing.py b/fastapi/routing.py index 36acb6b89d..173a6a5a0a 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -115,6 +115,11 @@ def request_response( response_awaited = False async with AsyncExitStack() as request_stack: scope["fastapi_inner_astack"] = request_stack + # Ensure fastapi_middleware_astack exists in scope even when + # the router is used directly without a FastAPI app (which + # normally sets it up via AsyncExitStackMiddleware). + if "fastapi_middleware_astack" not in scope: + scope["fastapi_middleware_astack"] = request_stack async with AsyncExitStack() as function_stack: scope["fastapi_function_astack"] = function_stack response = await f(request) diff --git a/tests/test_router_direct_testclient.py b/tests/test_router_direct_testclient.py new file mode 100644 index 0000000000..569f334992 --- /dev/null +++ b/tests/test_router_direct_testclient.py @@ -0,0 +1,62 @@ +"""Test for issue #14128: Router used directly with TestClient and yield dependencies.""" + +from fastapi import APIRouter, Depends +from fastapi.testclient import TestClient + + +def test_router_direct_testclient_with_yield_dependency(): + """TestClient(router) should work even without wrapping in FastAPI().""" + router = APIRouter() + + async def yield_dep(): + yield "value" + + @router.get("/test") + async def endpoint(dep: str = Depends(yield_dep)): + return {"dep": dep} + + client = TestClient(router) + response = client.get("/test") + assert response.status_code == 200 + assert response.json() == {"dep": "value"} + + +def test_router_direct_testclient_with_multiple_yield_dependencies(): + """Multiple yield dependencies should work with direct router TestClient.""" + router = APIRouter() + + async def dep_a(): + yield "a" + + async def dep_b(): + yield "b" + + @router.get("/test") + async def endpoint(a: str = Depends(dep_a), b: str = Depends(dep_b)): + return {"a": a, "b": b} + + client = TestClient(router) + response = client.get("/test") + assert response.status_code == 200 + assert response.json() == {"a": "a", "b": "b"} + + +def test_router_direct_testclient_yield_dependency_cleanup(): + """Yield dependency cleanup should run even with direct router TestClient.""" + cleaned_up = False + + router = APIRouter() + + async def yield_dep(): + nonlocal cleaned_up + yield "value" + cleaned_up = True + + @router.get("/test") + async def endpoint(dep: str = Depends(yield_dep)): + return {"dep": dep} + + client = TestClient(router) + response = client.get("/test") + assert response.status_code == 200 + assert cleaned_up