fastapi
fastapi copied to clipboard
[BUG] Mounting StaticFiles with an APIRouter doesn't work
Describe the bug
Mounting StaticFiles with an APIRouter doesn't work.
To Reproduce
from typing import Any
from fastapi import FastAPI, Request, APIRouter
from fastapi.staticfiles import StaticFiles
from fastapi.testclient import TestClient
router = APIRouter()
@router.get("/")
async def foo(request: Request) -> Any:
# this raises starlette.routing.NoMatchFound
return request.url_for("static", path="/bar")
app = FastAPI()
router.mount("/static", StaticFiles(directory="."), name="static")
# uncomment to fix
# app.mount("/static", StaticFiles(directory="."), name="static")
app.include_router(router)
client = TestClient(app)
client.get("/")
- Execute the script, raises NoMatchFound
- Uncomment line to mount with app instead
- Executes as expected
Expected behavior
I can use APIRouter() as if it was a FastAPI() as noted in the docs.
The include_router
function in FastAPI
is expecting an APIRouter
, and will only register Route
s that are included on that APIRouter
. A StaticFiles
is a sub-application, not a Route
. I believe that FastAPI only supports mounting sub-applications on the app.
I'm not sure it makes sense to mount it on an APIRouter
as the features of that class (default dependencies, response models) don't do much for static files. Maybe the bug here is that the mount
function should be overloaded (from Starlette's) to raise an exception directly telling developers it's not supported?
If there is a specific need to include static files under a router, could you outline why that feature would be more useful then the basic mounting on the app?
Could you also point out the specific docs that were confusing so they can be clarified?
Finally, if you do need some Router
functionality for your StaticFiles
, you could use the Starlette methods directly in the meantime. Something like this:
from typing import Any
from fastapi import FastAPI, Request, APIRouter
from fastapi.staticfiles import StaticFiles
from fastapi.testclient import TestClient
from starlette.routing import Router
app = FastAPI()
router = APIRouter()
@router.get("/")
async def foo(request: Request) -> Any:
# this raises starlette.routing.NoMatchFound
return request.url_for("static", path="/bar")
app.include_router(router)
static_router = Router()
static_router.mount("/", StaticFiles(directory="."), name="static")
app.mount("/static", other_router)
client = TestClient(app)
client.get("/")
If there is a specific need to include static files under a router, could you outline why that feature would be more useful then the basic mounting on the app?
I have different modules with different static things in my app and wanted to separate everything. So that changing the prefix also changes the static prefix. And so they can live in another package not in the same repo.
Could you also point out the specific docs that were confusing so they can be clarified?
It's here https://fastapi.tiangolo.com/tutorial/bigger-applications/#path-operations-with-apirouter "All the same options are supported.", but I now see that this only refers to path operations.
(btw the search in the docs is broken, it just says "Initializing search" forever)
@tiangolo we probably need your opinion here on design. Would it be appropriate for include_router
to also mount any sub-applications that have been mounted to thatAPIRouter
(with appropriate prefix)? Or should we explicitly disable the mount
function on APIRouter
and make a note in the docs about it?
@lazka there is an open issue about the search (#1448). It works on some platforms and not on others (not sure that's been narrowed down yet). I have good luck on Chrome on macOS.
Turns out what I actually want is to mount another FastAPI() like so
subapp = FastAPI()
app = FastAPI()
subapp.mount("/prefix", subapp)
This is more in line with flask blueprints. Only downside is the "startup" event isn't triggered for "subapp" :( -> I've filed #1480
I just ran into the same issue. I agree that at the very least there should be an error (although I would definitely support mounting within routers).
@lazka Good to know that's a solution. Would this introduce any additional latency (because of the duplicated "bookkeeping", FastAPI/starlette does for each ASGI request? Or is that not something to even worry about?
Basically, I'm in a similar situation where I have organized my app into around 7 different APIRouter
instances that each have around 3 routes defined and are all imported and included in the root router. While I assumed this was one of the intended purposes of APIRouter
, if not perhaps we should update the bigger-applications docs page or at least mention when an APIRouter
behaves differently than the FastAPI
class.
@lazka Good to know that's a solution. Would this introduce any additional latency (because of the duplicated "bookkeeping", FastAPI/starlette does for each ASGI request? Or is that not something to even worry about?
I don't know. I'm still using it that way and hadn't had any issues (besides the events not working, which prevents me from modularization the app)
I have the same problem, I cannot render static files in an APIRouter.
Same here. I use @lazka's solution for now, but it would be nice to have this more intuitive. Or at least throw an error if someone tries to mount StaticFiles
to a APIRouter
. It took some time to get to this page.
I tried the same way as lazka and failed.
It confuses me a lot that APIRouter has mount()
which doesn't work actually.
the same issue, still can't figure it out, how to serve static files with the router.
The underlying problems seems to be that the route does not get applied to the application. The cause is a very restrictive check:
https://github.com/tiangolo/fastapi/blob/f0388915a8b1cd9f3ae2259bace234ac6249c51a/fastapi/routing.py#L713-L721
This should be expanded to work with routing.BaseRoute
Hello, same issue here.
I use an APIRouter
instance like so: prefix_router = APIRouter(prefix=ROOT_PATH)
.
After that, app.mount("/static", StaticFiles(directory="static"), name="static")
delivers the static files iff ROOT_PATH == ""
.
If I set a non-empty ROOT_PATH
, I get 404s on everything under e.g. http://example.com/ROOT_PATH/static.
Switching app.mount()
to prefix_router.mount()
fails.
Any workarounds until https://github.com/tiangolo/fastapi/issues/1469#issuecomment-1013952356 gets addressed?
You need to refer to your static
folder as a root folder. So, in a response, if you refer to it as static/some_file.ext
, it will parse it to http://example.com/ROOT_PATH/static/some_file.ext
. But, if you refer to it as /static/some_file.ext
, it will parse it to http://example.com/static/some_file.ext
.
Below is a full working example of this behaviour:
from fastapi import APIRouter, FastAPI
from fastapi.staticfiles import StaticFiles
from fastapi.responses import HTMLResponse
app = FastAPI()
app.mount('/static', StaticFiles(directory='static'), name='static')
my_router = APIRouter(prefix='/router')
@my_router.get("/not-working")
async def router_root():
content = "<img src='static/black.png'>"
# -------------------^ note, not absolute path!
return HTMLResponse(content=content)
@my_router.get("/working")
async def router_root():
content = "<img src='/static/black.png'>"
# -------------------^ note, prefixed slash!
return HTMLResponse(content=content)
app.include_router(my_router)
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
Output (first called not-working, then working):
INFO: 127.0.0.1:53410 - "GET /router/not-working HTTP/1.1" 200 OK
INFO: 127.0.0.1:53410 - "GET /router/static/black.png HTTP/1.1" 404 Not Found
INFO: 127.0.0.1:53412 - "GET /router/working HTTP/1.1" 200 OK
INFO: 127.0.0.1:53412 - "GET /black.png HTTP/1.1" 200 OK
As @septatrix has pointed out, the problem lies inside a loop which copies routes from downstream router to an upstream. Mount
routes are simply ingored. But it seems that it's quite easy to make it work by appending the following code:
elif isinstance(route, routing.Mount):
self.mount(prefix + route.path, route.app, route.name)
to if-else chain at https://github.com/tiangolo/fastapi/blob/f0388915a8b1cd9f3ae2259bace234ac6249c51a/fastapi/routing.py#L726-L729 I haven't tested it thoroughly but for my use case everything looks fine. Here is the full snippet I use for current fastapi version as less hacky solution is not available to my knowledge.
any update on this? Running into the same issue.
I find there is not else statement. Maybe we can add else statement to avoid unexpected type and help us to address error?
elif isinstance(route, routing.WebSocketRoute):
self.add_websocket_route(
prefix + route.path, route.endpoint, name=route.name
)
elif isinstance(route, routing.Mount):
self.mount(prefix + route.path, route.app, route.name)
else:
raise Exception(f"Unexcepted route type {type(route)}")