Browse Source

fix: propagate StaticFiles Mount objects when using include_router

Per maintainer feedback on #14924, limit Mount propagation in
include_router() to StaticFiles only, matching the approach in #13944.

Non-StaticFiles sub-app mounts are silently ignored (not propagated)
to preserve existing behavior, since sub-applications should be mounted
directly on the root FastAPI instance.

Fixes #10180
pull/14924/head
kimchikingdom 5 months ago
parent
commit
7a6d25f894
  1. 4
      fastapi/routing.py
  2. 102
      tests/test_router_mount.py

4
fastapi/routing.py

@ -74,6 +74,7 @@ from starlette.routing import (
get_name,
)
from starlette.routing import Mount as Mount # noqa
from starlette.staticfiles import StaticFiles
from starlette.types import AppType, ASGIApp, Lifespan, Receive, Scope, Send
from starlette.websockets import WebSocket
from typing_extensions import deprecated
@ -1490,7 +1491,8 @@ class APIRouter(routing.Router):
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)
if isinstance(route.app, StaticFiles):
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:

102
tests/test_router_mount.py

@ -1,11 +1,17 @@
"""Tests for mounting sub-applications under APIRouter (issue #10180)."""
"""Tests for mounting StaticFiles under APIRouter (issue #10180)."""
from pathlib import Path
import pytest
from fastapi import APIRouter, FastAPI
from starlette.staticfiles import StaticFiles
from starlette.testclient import TestClient
from fastapi.staticfiles import StaticFiles
from fastapi.testclient import TestClient
def test_mount_staticfiles_under_router_with_prefix(tmp_path: Path) -> None:
"""StaticFiles mounted on APIRouter with prefix should be accessible."""
static_file = tmp_path / "hello.txt"
static_file.write_text("hello world")
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")
@ -13,13 +19,7 @@ def test_mount_subapp_under_router_with_prefix() -> None:
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)
api_router.mount("/static", StaticFiles(directory=tmp_path), name="static")
app.include_router(api_router)
client = TestClient(app)
@ -27,80 +27,68 @@ def test_mount_subapp_under_router_with_prefix() -> None:
# 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")
# StaticFiles should be accessible under router prefix
r = client.get("/api/static/hello.txt")
assert r.status_code == 200
assert r.json() == {"message": "Hello World from sub API"}
assert r.text == "hello world"
def test_mount_staticfiles_under_router_without_prefix(tmp_path: Path) -> None:
"""StaticFiles mounted on APIRouter without prefix should be accessible."""
static_file = tmp_path / "test.txt"
static_file.write_text("test content")
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)
router.mount("/static", StaticFiles(directory=tmp_path), name="static")
app.include_router(router)
client = TestClient(app)
r = client.get("/sub/hello")
r = client.get("/static/test.txt")
assert r.status_code == 200
assert r.json() == {"msg": "hello"}
assert r.text == "test content"
def test_mount_staticfiles_with_include_router_prefix(tmp_path: Path) -> None:
"""include_router prefix + router prefix + mount path should all combine."""
static_file = tmp_path / "file.txt"
static_file.write_text("combined prefix")
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)
router.mount("/static", StaticFiles(directory=tmp_path), name="static")
app.include_router(router, prefix="/v1")
client = TestClient(app)
r = client.get("/v1/api/sub/test")
r = client.get("/v1/api/static/file.txt")
assert r.status_code == 200
assert r.json() == {"msg": "test"}
assert r.text == "combined prefix"
def test_mount_preserves_regular_routes() -> None:
"""Adding Mount support should not break existing APIRoute handling."""
def test_mount_non_staticfiles_app_is_ignored() -> None:
"""Mounting a non-StaticFiles ASGI app on APIRouter should be silently ignored
(not propagated to the main app) to preserve existing behavior."""
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"}
@subapi.get("/sub")
def read_sub() -> dict:
return {"message": "sub"}
api_router.mount("/health", subapi)
api_router.mount("/subapi", 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
# 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
# (no routes defined here, but include_router itself should not crash)
assert app is not None

Loading…
Cancel
Save