diff --git a/fastapi/routing.py b/fastapi/routing.py index 36acb6b89d..cb977e216d 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -4,6 +4,7 @@ import functools import inspect import json import types +import warnings from collections.abc import ( AsyncIterator, Awaitable, @@ -679,6 +680,21 @@ def get_request_handler( if isinstance(raw_response, Response): if raw_response.background is None: raw_response.background = solved_result.background_tasks + elif solved_result.background_tasks is not None: + warnings.warn( + "FastAPI has injected BackgroundTasks via " + "dependency injection, but the endpoint returned " + "a Response that already has a `background` set. " + "The dependency-injected background tasks will be " + "discarded in favor of the response's own " + "`background`. To avoid this warning, either add " + "your tasks to the injected `BackgroundTasks` " + "instance instead of setting `background` on the " + "response, or do not inject `BackgroundTasks` at " + "all.", + UserWarning, + stacklevel=1, + ) response = raw_response else: response_args = _build_response_args( diff --git a/tests/test_warn_background_tasks_overwritten.py b/tests/test_warn_background_tasks_overwritten.py new file mode 100644 index 0000000000..f1116f14ef --- /dev/null +++ b/tests/test_warn_background_tasks_overwritten.py @@ -0,0 +1,90 @@ +"""Test that a warning is emitted when a returned Response already has a background +task set and the dependency-injected BackgroundTasks would be silently discarded. + +Ref: https://github.com/fastapi/fastapi/issues/11215 +""" + +import warnings + +from fastapi import BackgroundTasks, FastAPI +from fastapi.testclient import TestClient +from starlette.background import BackgroundTask +from starlette.responses import JSONResponse, Response + + +def test_warn_when_response_background_overwrites_injected_tasks(): + """When the endpoint returns a Response with its own `background`, + and the user also injected `BackgroundTasks`, a UserWarning should + be emitted so the silent data loss is visible.""" + app = FastAPI() + + @app.get("/") + async def endpoint(tasks: BackgroundTasks): + tasks.add_task(lambda: None) + return Response( + content="Custom response", + background=BackgroundTask(lambda: None), + ) + + client = TestClient(app, raise_server_exceptions=False) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + resp = client.get("/") + + assert resp.status_code == 200 + bg_warnings = [w for w in caught if "background" in str(w.message).lower()] + assert len(bg_warnings) == 1 + assert issubclass(bg_warnings[0].category, UserWarning) + + +def test_no_warn_when_response_has_no_background(): + """When the endpoint returns a Response without a `background`, + no warning should be emitted (the injected tasks are attached).""" + app = FastAPI() + executed = [] + + def bg_task(): + executed.append(True) + + @app.get("/") + async def endpoint(tasks: BackgroundTasks): + tasks.add_task(bg_task) + return JSONResponse(content={"ok": True}) + + client = TestClient(app) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + resp = client.get("/") + + assert resp.status_code == 200 + bg_warnings = [w for w in caught if "background" in str(w.message).lower()] + assert len(bg_warnings) == 0 + # The injected task should have run + assert executed == [True] + + +def test_no_warn_when_no_injected_background_tasks(): + """When the endpoint returns a Response with a `background` but did + NOT inject BackgroundTasks, no warning should be emitted.""" + app = FastAPI() + executed = [] + + @app.get("/") + async def endpoint(): + return Response( + content="ok", + background=BackgroundTask(lambda: executed.append(True)), + ) + + client = TestClient(app) + + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + resp = client.get("/") + + assert resp.status_code == 200 + bg_warnings = [w for w in caught if "background" in str(w.message).lower()] + assert len(bg_warnings) == 0 + assert executed == [True]