Browse Source

fix: propagate Mount objects when using include_router

When an APIRouter with mounted sub-applications is included into a
FastAPI app via include_router(), the Mount routes were silently
ignored. This is because include_router() only handled APIRoute,
Route, APIWebSocketRoute, and WebSocketRoute — never Mount.

Additionally, APIRouter.mount() (inherited from Starlette's Router)
does not apply self.prefix to the stored path, unlike add_api_route().
So the fix applies both the include_router prefix and the router's own
prefix when propagating Mount routes.

Fixes #10180
pull/14924/head
kimchikingdom 5 months ago
parent
commit
dc8a90be97
  1. 2
      fastapi/routing.py
  2. 106
      tests/test_router_mount.py

2
fastapi/routing.py

@ -1489,6 +1489,8 @@ class APIRouter(routing.Router):
self.add_websocket_route(
prefix + route.path, route.endpoint, name=route.name
)
elif isinstance(route, routing.Mount):
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:

106
tests/test_router_mount.py

@ -0,0 +1,106 @@
"""Tests for mounting sub-applications under APIRouter (issue #10180)."""
from fastapi import APIRouter, FastAPI
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
def test_mount_subapp_under_router_with_prefix() -> None:
"""APIRouter with prefix should propagate Mount to the main app."""
app = FastAPI()
api_router = APIRouter(prefix="/api")
@api_router.get("/app")
def read_main() -> dict:
return {"message": "Hello World from main app"}
subapi = FastAPI()
@subapi.get("/sub")
def read_sub() -> dict:
return {"message": "Hello World from sub API"}
api_router.mount("/subapi", subapi)
app.include_router(api_router)
client = TestClient(app)
# Regular route should still work
r = client.get("/api/app")
assert r.status_code == 200
assert r.json() == {"message": "Hello World from main app"}
# Mounted sub-app should be accessible under router prefix
r = client.get("/api/subapi/sub")
assert r.status_code == 200
assert r.json() == {"message": "Hello World from sub API"}
def test_mount_subapp_under_router_without_prefix() -> None:
"""APIRouter without prefix should propagate Mount correctly."""
app = FastAPI()
router = APIRouter()
subapi = FastAPI()
@subapi.get("/hello")
def hello() -> dict:
return {"msg": "hello"}
router.mount("/sub", subapi)
app.include_router(router)
client = TestClient(app)
r = client.get("/sub/hello")
assert r.status_code == 200
assert r.json() == {"msg": "hello"}
def test_mount_subapp_with_include_router_prefix() -> None:
"""include_router prefix should be combined with router prefix and mount path."""
app = FastAPI()
router = APIRouter(prefix="/api")
subapi = FastAPI()
@subapi.get("/test")
def test_endpoint() -> dict:
return {"msg": "test"}
router.mount("/sub", subapi)
app.include_router(router, prefix="/v1")
client = TestClient(app)
r = client.get("/v1/api/sub/test")
assert r.status_code == 200
assert r.json() == {"msg": "test"}
def test_mount_preserves_regular_routes() -> None:
"""Adding Mount support should not break existing APIRoute handling."""
app = FastAPI()
api_router = APIRouter(prefix="/api")
@api_router.get("/items")
def list_items() -> dict:
return {"items": [1, 2, 3]}
@api_router.post("/items")
def create_item() -> dict:
return {"created": True}
subapi = FastAPI()
@subapi.get("/status")
def status() -> dict:
return {"status": "ok"}
api_router.mount("/health", subapi)
app.include_router(api_router)
client = TestClient(app)
assert client.get("/api/items").status_code == 200
assert client.post("/api/items").status_code == 200
assert client.get("/api/health/status").status_code == 200
Loading…
Cancel
Save