diff --git a/fastapi/applications.py b/fastapi/applications.py index 41d86143e..ab9176c14 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1133,6 +1133,9 @@ 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 07a63e883..5354cfa3a 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -994,6 +994,26 @@ 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 at the given path. + + Raises `FastAPIError` if `app` is not an instance of `StaticFiles`, + since mounting arbitrary ASGI sub-applications under an `APIRouter` + is not supported. Sub-applications should be mounted directly on + the root `FastAPI` instance instead. + """ + if not isinstance(app, StaticFiles): + raise FastAPIError( + "APIRouter does not support mounting ASGI applications other than " + "StaticFiles. Mount sub-applications directly on the root FastAPI " + "instance instead." + ) + 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, @@ -1492,7 +1512,7 @@ class APIRouter(routing.Router): ) elif isinstance(route, routing.Mount): if isinstance(route.app, StaticFiles): - self.mount(prefix + router.prefix + route.path, route.app, name=route.name) + self._mount(prefix + router.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_router_mount.py b/tests/test_router_mount.py index c8fdcfdb9..e0b19d193 100644 --- a/tests/test_router_mount.py +++ b/tests/test_router_mount.py @@ -3,6 +3,7 @@ from pathlib import Path import pytest from fastapi import APIRouter, FastAPI +from fastapi.exceptions import FastAPIError from fastapi.staticfiles import StaticFiles from fastapi.testclient import TestClient @@ -68,27 +69,13 @@ def test_mount_staticfiles_with_include_router_prefix(tmp_path: Path) -> None: assert r.text == "combined prefix" -def test_mount_non_staticfiles_app_is_ignored() -> None: - """Mounting a non-StaticFiles ASGI app on APIRouter should be silently ignored - (not propagated to the main app) to preserve existing behavior.""" - app = FastAPI() - api_router = APIRouter(prefix="/api") - - subapi = FastAPI() - - @subapi.get("/sub") - def read_sub() -> dict: - return {"message": "sub"} - - api_router.mount("/subapi", subapi) - app.include_router(api_router) - - client = TestClient(app) - - # Non-StaticFiles sub-app mount is not propagated — returns 404 - r = client.get("/api/subapi/sub") - assert r.status_code == 404 +def test_mount_non_staticfiles_app_raises_error() -> None: + """Mounting a non-StaticFiles ASGI app on APIRouter should raise FastAPIError.""" + router = APIRouter() + sub_app = FastAPI() - # Regular routes are unaffected - # (no routes defined here, but include_router itself should not crash) - assert app is not None + with pytest.raises( + FastAPIError, + match="APIRouter does not support mounting ASGI applications other than StaticFiles", + ): + router.mount("/sub", sub_app)