From 4a7b6d72ee412f9df023a83287836b1f73feeca8 Mon Sep 17 00:00:00 2001 From: fullstack-enginner Date: Wed, 1 Apr 2026 11:37:07 +0000 Subject: [PATCH] fix: support mounted sub-apps under APIRouter include --- fastapi/routing.py | 5 ++++ tests/test_router_mount.py | 48 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/test_router_mount.py diff --git a/fastapi/routing.py b/fastapi/routing.py index 36acb6b89d..44c5227413 100644 --- a/fastapi/routing.py +++ b/fastapi/routing.py @@ -1575,6 +1575,9 @@ class APIRouter(routing.Router): return decorator + def mount(self, path: str, app: ASGIApp, name: str | None = None) -> None: + super().mount(self.prefix + path, app=app, name=name) + def include_router( self, router: Annotated["APIRouter", Doc("The `APIRouter` to include.")], @@ -1803,6 +1806,8 @@ class APIRouter(routing.Router): include_in_schema=route.include_in_schema, name=route.name, ) + elif isinstance(route, routing.Mount): + self.mount(prefix + route.path, route.app, name=route.name) elif isinstance(route, APIWebSocketRoute): current_dependencies = [] if dependencies: diff --git a/tests/test_router_mount.py b/tests/test_router_mount.py new file mode 100644 index 0000000000..7dccdfc763 --- /dev/null +++ b/tests/test_router_mount.py @@ -0,0 +1,48 @@ +from fastapi import APIRouter, FastAPI +from fastapi.responses import JSONResponse +from fastapi.testclient import TestClient + + +def test_mount_subapp_under_router_prefix() -> None: + app = FastAPI() + api_router = APIRouter(prefix="/api") + + @api_router.get("/ping") + def ping() -> dict[str, str]: + return {"message": "pong"} + + sub_app = FastAPI() + + @sub_app.get("/sub") + def read_sub() -> dict[str, str]: + return {"message": "Hello World from sub API"} + + api_router.mount("/subapi", sub_app) + app.include_router(api_router) + + client = TestClient(app) + + response = client.get("/api/ping") + assert response.status_code == 200, response.text + assert response.json() == {"message": "pong"} + + mounted_response = client.get("/api/subapi/sub") + assert mounted_response.status_code == 200, mounted_response.text + assert mounted_response.json() == {"message": "Hello World from sub API"} + + +def test_mount_starlette_route_under_router_prefix() -> None: + app = FastAPI() + router = APIRouter(prefix="/v1") + + def plain_asgi_app(scope, receive, send): + response = JSONResponse({"ok": True}) + return response(scope, receive, send) + + router.mount("/internal", plain_asgi_app, name="internal") + app.include_router(router, prefix="/api") + + client = TestClient(app) + response = client.get("/api/v1/internal") + assert response.status_code == 200, response.text + assert response.json() == {"ok": True}