]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
🐛 Fix support for background tasks and headers from dependencies in `app.frontend...
authorSebastián Ramírez <tiangolo@gmail.com>
Wed, 29 Jul 2026 17:04:36 +0000 (19:04 +0200)
committerGitHub <noreply@github.com>
Wed, 29 Jul 2026 17:04:36 +0000 (19:04 +0200)
docs/en/docs/tutorial/frontend.md
fastapi/routing.py
tests/test_frontend.py

index be1bdba1852ce5aa3b35a0e49b06faeea1833c1e..37a9d31d9369ecd759b4a3b5a97428fce15d4fbc 100644 (file)
@@ -136,6 +136,8 @@ Frontend responses run inside the normal **FastAPI** application, so HTTP middle
 
 Dependencies from the app, from an `APIRouter`, and from `include_router()` also apply to frontend responses. This can be useful for protecting a frontend with cookie authentication or similar.
 
+Dependencies can also modify response headers and add background tasks, as with normal *path operations*.
+
 ## Static Build Output Only { #static-build-output-only }
 
 `app.frontend()` serves files already generated by your frontend build.
index 189dc15163534b46325e849270dbf244d356175f..dc48a33d63ed9aae3c1b4a5d8abd2ab4f724c908 100644 (file)
@@ -56,6 +56,7 @@ from fastapi.dependencies.models import (
     _is_gen_callable,
 )
 from fastapi.dependencies.utils import (
+    SolvedDependency,
     _get_body_field,
     _get_flat_body_params,
     _should_embed_body_fields,
@@ -1936,6 +1937,12 @@ class _FrontendStaticFiles(StaticFiles):
         assert isinstance(path, str)
         return os.path.normpath(os.path.join(*path.split("/")))
 
+    async def get_response_for_scope(self, scope: Scope) -> Response:
+        if not self.config_checked:
+            await self.check_config()
+            self.config_checked = True
+        return await self.get_response(self.get_path(scope), scope)
+
     async def get_response(self, path: str, scope: Scope) -> Response:
         if scope["method"] not in ("GET", "HEAD"):
             if await self._lookup_static_resource(path) is not None:
@@ -2086,7 +2093,8 @@ class _FrontendRoute(BaseRoute):
         return None
 
     async def handle(self, scope: Scope, receive: Receive, send: Send) -> None:
-        await self.app(scope, receive, send)
+        response = await self.app.get_response_for_scope(scope)
+        await response(scope, receive, send)
 
     def url_path_for(self, name: str, /, **path_params: Any) -> URLPath:
         raise NoMatchFound(name, path_params)
@@ -2189,8 +2197,12 @@ class _FrontendRouteGroup(BaseRoute):
                 dependant=dependant,
                 dependency_overrides_provider=dependency_overrides_provider,
                 embed_body_fields=embed_body_fields,
-            ):
-                await route.handle(scope, receive, send)
+            ) as solved_result:
+                response = await route.app.get_response_for_scope(scope)
+                if response.background is None:
+                    response.background = solved_result.background_tasks
+                response.headers.raw.extend(solved_result.response.headers.raw)
+                await response(scope, receive, send)
             return
         await route.handle(scope, receive, send)
 
@@ -2210,7 +2222,7 @@ class _FrontendRouteGroup(BaseRoute):
         dependant: Dependant,
         dependency_overrides_provider: Any | None,
         embed_body_fields: bool,
-    ) -> AsyncIterator[None]:
+    ) -> AsyncIterator[SolvedDependency]:
         request = Request(scope, receive, send)
         previous_inner_astack = scope.get("fastapi_inner_astack", _SCOPE_MISSING)
         previous_function_astack = scope.get("fastapi_function_astack", _SCOPE_MISSING)
@@ -2228,7 +2240,7 @@ class _FrontendRouteGroup(BaseRoute):
                     )
                     if solved_result.errors:
                         raise RequestValidationError(solved_result.errors)
-                    yield
+                    yield solved_result
         finally:
             if previous_inner_astack is _SCOPE_MISSING:
                 scope.pop("fastapi_inner_astack", None)
index 0fd899b94c5b34a3107b06adeaa70c33c766959e..f587faafbcae545dcfea05a5a8847f0d373790c8 100644 (file)
@@ -7,7 +7,15 @@ from typing import Literal
 
 import anyio
 import pytest
-from fastapi import APIRouter, Depends, FastAPI, HTTPException, Request, WebSocket
+from fastapi import (
+    APIRouter,
+    BackgroundTasks,
+    Depends,
+    FastAPI,
+    HTTPException,
+    Request,
+    WebSocket,
+)
 from fastapi.testclient import TestClient
 from starlette.exceptions import HTTPException as StarletteHTTPException
 from starlette.responses import PlainTextResponse, Response
@@ -491,6 +499,30 @@ def test_app_middleware_still_runs_for_frontend_dependencies(tmp_path: Path):
     assert calls == ["middleware-before", "dependency", "middleware-after"]
 
 
+def test_frontend_dependency_response_headers_and_background_tasks(tmp_path: Path):
+    calls: list[str] = []
+
+    def frontend_dependency(
+        response: Response, background_tasks: BackgroundTasks
+    ) -> None:
+        response.headers["X-Frontend-Dependency"] = "applied"
+        response.set_cookie("frontend", "dependency")
+        background_tasks.add_task(calls.append, "background")
+
+    dist = tmp_path / "dist"
+    write_file(dist / "index.html", "app")
+    app = FastAPI(dependencies=[Depends(frontend_dependency)])
+    app.frontend("/", directory=dist)
+
+    response = TestClient(app).get("/")
+
+    assert response.status_code == 200
+    assert response.text == "app"
+    assert response.headers["X-Frontend-Dependency"] == "applied"
+    assert response.cookies["frontend"] == "dependency"
+    assert calls == ["background"]
+
+
 def test_frontend_dependency_validation_errors_return_422(tmp_path: Path):
     def require_token(token: str) -> None:
         pass  # pragma: no cover