From: Sebastián Ramírez Date: Wed, 29 Jul 2026 17:04:36 +0000 (+0200) Subject: 🐛 Fix support for background tasks and headers from dependencies in `app.frontend... X-Git-Tag: 0.141.1~2 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=d62354434b2e508fe89024213b220ca8e67dea5e;p=thirdparty%2Ffastapi%2Ffastapi.git 🐛 Fix support for background tasks and headers from dependencies in `app.frontend()` (#16105) --- diff --git a/docs/en/docs/tutorial/frontend.md b/docs/en/docs/tutorial/frontend.md index be1bdba185..37a9d31d93 100644 --- a/docs/en/docs/tutorial/frontend.md +++ b/docs/en/docs/tutorial/frontend.md @@ -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. diff --git a/fastapi/routing.py b/fastapi/routing.py index 189dc15163..dc48a33d63 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -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) diff --git a/tests/test_frontend.py b/tests/test_frontend.py index 0fd899b94c..f587faafbc 100644 --- a/tests/test_frontend.py +++ b/tests/test_frontend.py @@ -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