From dc8a90be977f6ea6b842d0c0f71f6aacf37b1508 Mon Sep 17 00:00:00 2001 From: kimchikingdom Date: Sat, 14 Feb 2026 20:46:59 +0900 Subject: [PATCH] fix: propagate Mount objects when using include_router MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- fastapi/routing.py | 2 + tests/test_router_mount.py | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 tests/test_router_mount.py diff --git a/fastapi/routing.py b/fastapi/routing.py index ea82ab14a..543804041 100644 --- a/fastapi/routing.py +++ b/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: diff --git a/tests/test_router_mount.py b/tests/test_router_mount.py new file mode 100644 index 000000000..43ebca210 --- /dev/null +++ b/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