Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mounting StaticFiles with an APIRouter doesn't work #1469

Closed
lazka opened this issue May 24, 2020 · 16 comments
Closed

Mounting StaticFiles with an APIRouter doesn't work #1469

lazka opened this issue May 24, 2020 · 16 comments

Comments

@lazka
Copy link

lazka commented May 24, 2020

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.

@lazka lazka added the bug Something isn't working label May 24, 2020
@dbanty
Copy link
Contributor

dbanty commented May 24, 2020

The include_router function in FastAPI is expecting an APIRouter, and will only register Routes 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("/")

@lazka
Copy link
Author

lazka commented May 24, 2020

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)

@dbanty
Copy link
Contributor

dbanty commented May 24, 2020

@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.

@lazka
Copy link
Author

lazka commented May 26, 2020

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

@MatthewScholefield
Copy link

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
Copy link
Author

lazka commented Feb 8, 2021

@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)

@facundopadilla
Copy link

I have the same problem, I cannot render static files in an APIRouter.

@Netzvamp
Copy link

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.

@JalinWang
Copy link

JalinWang commented Oct 20, 2021

I tried the same way as lazka and failed.

It confuses me a lot that APIRouter has mount() which doesn't work actually.
image

@nikeshnaik
Copy link

nikeshnaik commented Nov 13, 2021

the same issue, still can't figure it out, how to serve static files with the router.

@septatrix
Copy link

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

@waseigo
Copy link

waseigo commented Jul 22, 2022

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 #1469 (comment) gets addressed?

@JarroVGIT
Copy link
Contributor

JarroVGIT commented Jul 23, 2022

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='/https/github.com/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

@SF-300
Copy link

SF-300 commented Nov 14, 2022

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.

@isConic
Copy link

isConic commented Nov 30, 2022

any update on this? Running into the same issue.

@TaiJuWu
Copy link

TaiJuWu commented Feb 15, 2023

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)}")

@tiangolo tiangolo added question Question or problem reviewed and removed bug Something isn't working labels Feb 24, 2023
@tiangolo tiangolo changed the title [BUG] Mounting StaticFiles with an APIRouter doesn't work Mounting StaticFiles with an APIRouter doesn't work Feb 24, 2023
@fastapi fastapi locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #9070 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests