diff --git a/fastapi/applications.py b/fastapi/applications.py index 41d86143e..a2fc33f0e 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1133,6 +1133,11 @@ class FastAPI(Starlette): scope["root_path"] = self.root_path await super().__call__(scope, receive, send) + + def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: + self.router._mount(path, app=app, name=name) + + def add_api_route( self, path: str, diff --git a/fastapi/routing.py b/fastapi/routing.py index ea82ab14a..f5db323cb 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -74,6 +74,7 @@ from starlette.routing import ( get_name, ) from starlette.routing import Mount as Mount # noqa +from starlette.staticfiles import StaticFiles from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send from starlette.websockets import WebSocket from typing_extensions import deprecated @@ -993,6 +994,20 @@ class APIRouter(routing.Router): self.default_response_class = default_response_class self.generate_unique_id_function = generate_unique_id_function + def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: + """ + Mount a StaticFiles instance. + Will raise a FastAPIError exception if app is not an instance of StaticFiles. + """ + if not isinstance(app, StaticFiles): + raise FastAPIError( + "APIRouter does not support mounting ASGI applications other than StaticFiles." + ) + self._mount(path=path, app=app, name=name) + + def _mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: + super().mount(path=path, app=app, name=name) + def route( self, path: str, @@ -1489,6 +1504,11 @@ class APIRouter(routing.Router): self.add_websocket_route( prefix + route.path, route.endpoint, name=route.name ) + elif ( # fmt: skip + isinstance(route, routing.Mount) and isinstance(route.app, StaticFiles) + ): + self.mount(prefix + route.path, route.app, name=route.name) + for handler in router.on_startup: self.add_event_handler("startup", handler) for handler in router.on_shutdown: diff --git a/tests/test_apirouter_mount.py b/tests/test_apirouter_mount.py new file mode 100644 index 000000000..a873cfa49 --- /dev/null +++ b/tests/test_apirouter_mount.py @@ -0,0 +1,32 @@ +import pytest +from fastapi import APIRouter, FastAPI +from fastapi.exceptions import FastAPIError +from fastapi.staticfiles import StaticFiles +from fastapi.testclient import TestClient + + +def test_mount_static_files_to_apirouter(tmp_path): + static_asset = tmp_path / "index.html" + static_asset.write_text("Hello, World!") + + router = APIRouter() + router.mount("/static", StaticFiles(directory=tmp_path), name="static") + + app = FastAPI() + app.include_router(router) + + client = TestClient(app) + response = client.get("/static/index.html") + assert response.status_code == 200 + assert response.text == "Hello, World!" + + +def test_mount_app_to_apirouter_raises(): + router = APIRouter() + sub_app = FastAPI() + + with pytest.raises( + FastAPIError, + match="APIRouter does not support mounting ASGI applications other than StaticFiles.", + ): + router.mount("/sub", sub_app)