Browse Source

fix: support mounting StaticFiles to APIRouter, raise error for other apps

- Override APIRouter.mount() to only allow StaticFiles; raise FastAPIError
  for any other ASGI app with a clear message directing users to mount
  sub-applications on the root FastAPI instance instead
- Add APIRouter._mount() as internal bypass used by include_router()
  and FastAPI.mount()
- Override FastAPI.mount() to call self.router._mount() directly,
  bypassing the StaticFiles-only restriction on APIRouter
- Propagate StaticFiles mounts in include_router() via _mount()

Fixes #10180
pull/14924/head
kimchikingdom 5 months ago
parent
commit
fbcdc4ad28
  1. 3
      fastapi/applications.py
  2. 22
      fastapi/routing.py
  3. 33
      tests/test_router_mount.py

3
fastapi/applications.py

@ -1133,6 +1133,9 @@ class FastAPI(Starlette):
scope["root_path"] = self.root_path scope["root_path"] = self.root_path
await super().__call__(scope, receive, send) 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( def add_api_route(
self, self,
path: str, path: str,

22
fastapi/routing.py

@ -994,6 +994,26 @@ class APIRouter(routing.Router):
self.default_response_class = default_response_class self.default_response_class = default_response_class
self.generate_unique_id_function = generate_unique_id_function 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( def route(
self, self,
path: str, path: str,
@ -1492,7 +1512,7 @@ class APIRouter(routing.Router):
) )
elif isinstance(route, routing.Mount): elif isinstance(route, routing.Mount):
if isinstance(route.app, StaticFiles): 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: for handler in router.on_startup:
self.add_event_handler("startup", handler) self.add_event_handler("startup", handler)
for handler in router.on_shutdown: for handler in router.on_shutdown:

33
tests/test_router_mount.py

@ -3,6 +3,7 @@ from pathlib import Path
import pytest import pytest
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
from fastapi.exceptions import FastAPIError
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.testclient import TestClient 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" assert r.text == "combined prefix"
def test_mount_non_staticfiles_app_is_ignored() -> None: def test_mount_non_staticfiles_app_raises_error() -> None:
"""Mounting a non-StaticFiles ASGI app on APIRouter should be silently ignored """Mounting a non-StaticFiles ASGI app on APIRouter should raise FastAPIError."""
(not propagated to the main app) to preserve existing behavior.""" router = APIRouter()
app = FastAPI() sub_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
# Regular routes are unaffected with pytest.raises(
# (no routes defined here, but include_router itself should not crash) FastAPIError,
assert app is not None match="APIRouter does not support mounting ASGI applications other than StaticFiles",
):
router.mount("/sub", sub_app)

Loading…
Cancel
Save