Browse Source
Added support for mounting sub-applications on APIRouter with proper
prefix handling:
1. Override mount() in APIRouter to automatically apply the router's
prefix to the mount path
2. Handle Mount routes in include_router() to apply the include prefix
This allows patterns like:
router = APIRouter(prefix='/api')
router.mount('/subapp', FastAPI())
app.include_router(router)
Which correctly routes requests to /api/subapp/...
Note: Mounts must be added before include_router() is called, as
include_router copies routes at call time. This is consistent with
how other routes work.
Fixes #10180
pull/14793/head
2 changed files with 150 additions and 0 deletions
@ -0,0 +1,133 @@ |
|||||
|
"""Test mounting sub-applications under APIRouter.""" |
||||
|
|
||||
|
from fastapi import APIRouter, FastAPI |
||||
|
from fastapi.testclient import TestClient |
||||
|
|
||||
|
|
||||
|
def test_router_mount_basic(): |
||||
|
"""Sub-applications mounted on a router should work when included in the app.""" |
||||
|
app = FastAPI() |
||||
|
api_router = APIRouter(prefix="/api") |
||||
|
|
||||
|
@api_router.get("/main") |
||||
|
def read_main(): |
||||
|
return {"message": "Hello from main"} |
||||
|
|
||||
|
subapi = FastAPI() |
||||
|
|
||||
|
@subapi.get("/sub") |
||||
|
def read_sub(): |
||||
|
return {"message": "Hello from sub"} |
||||
|
|
||||
|
# Mount BEFORE include_router |
||||
|
api_router.mount("/subapi", subapi) |
||||
|
app.include_router(api_router) |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
# Main route should work |
||||
|
response = client.get("/api/main") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"message": "Hello from main"} |
||||
|
|
||||
|
# Sub-application route should also work |
||||
|
response = client.get("/api/subapi/sub") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"message": "Hello from sub"} |
||||
|
|
||||
|
|
||||
|
def test_router_mount_with_include_prefix(): |
||||
|
"""Mount path should combine with both router prefix and include_router prefix.""" |
||||
|
app = FastAPI() |
||||
|
api_router = APIRouter(prefix="/v1") |
||||
|
|
||||
|
subapi = FastAPI() |
||||
|
|
||||
|
@subapi.get("/endpoint") |
||||
|
def sub_endpoint(): |
||||
|
return {"version": "1"} |
||||
|
|
||||
|
api_router.mount("/mounted", subapi) |
||||
|
app.include_router(api_router, prefix="/api") |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
# Full path: /api + /v1 + /mounted + /endpoint |
||||
|
response = client.get("/api/v1/mounted/endpoint") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"version": "1"} |
||||
|
|
||||
|
|
||||
|
def test_router_mount_without_prefix(): |
||||
|
"""Mount should work on router without prefix.""" |
||||
|
app = FastAPI() |
||||
|
api_router = APIRouter() # No prefix |
||||
|
|
||||
|
subapi = FastAPI() |
||||
|
|
||||
|
@subapi.get("/hello") |
||||
|
def hello(): |
||||
|
return {"hello": "world"} |
||||
|
|
||||
|
api_router.mount("/sub", subapi) |
||||
|
app.include_router(api_router) |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
response = client.get("/sub/hello") |
||||
|
assert response.status_code == 200 |
||||
|
assert response.json() == {"hello": "world"} |
||||
|
|
||||
|
|
||||
|
def test_router_mount_applies_router_prefix(): |
||||
|
"""Mount path should include the router's prefix.""" |
||||
|
router = APIRouter(prefix="/api") |
||||
|
|
||||
|
subapi = FastAPI() |
||||
|
|
||||
|
@subapi.get("/test") |
||||
|
def test_route(): |
||||
|
return {"test": True} |
||||
|
|
||||
|
router.mount("/mounted", subapi) |
||||
|
|
||||
|
# Check that the mount path includes the router prefix |
||||
|
mount_route = None |
||||
|
for route in router.routes: |
||||
|
if hasattr(route, "path") and "mounted" in route.path: |
||||
|
mount_route = route |
||||
|
break |
||||
|
|
||||
|
assert mount_route is not None |
||||
|
assert mount_route.path == "/api/mounted" |
||||
|
|
||||
|
|
||||
|
def test_router_mount_multiple_subapps(): |
||||
|
"""Multiple sub-applications can be mounted on the same router.""" |
||||
|
app = FastAPI() |
||||
|
api_router = APIRouter(prefix="/api") |
||||
|
|
||||
|
subapi1 = FastAPI() |
||||
|
subapi2 = FastAPI() |
||||
|
|
||||
|
@subapi1.get("/one") |
||||
|
def route_one(): |
||||
|
return {"app": 1} |
||||
|
|
||||
|
@subapi2.get("/two") |
||||
|
def route_two(): |
||||
|
return {"app": 2} |
||||
|
|
||||
|
api_router.mount("/first", subapi1) |
||||
|
api_router.mount("/second", subapi2) |
||||
|
app.include_router(api_router) |
||||
|
|
||||
|
client = TestClient(app) |
||||
|
|
||||
|
response1 = client.get("/api/first/one") |
||||
|
assert response1.status_code == 200 |
||||
|
assert response1.json() == {"app": 1} |
||||
|
|
||||
|
response2 = client.get("/api/second/two") |
||||
|
assert response2.status_code == 200 |
||||
|
assert response2.json() == {"app": 2} |
||||
Loading…
Reference in new issue