pythonfastapiuvicornhypercorn

FastAPI / uvicorn (or hypercorn): where is my root-path?


Based on a few FastAPI tutorials, including this, I made a simple FastAPI app:

from fastapi import FastAPI, Request
app = FastAPI() # also tried FastAPI(root_path="/api/v1")

@app.get("/app")
def read_main(request: Request):
    return {"message": "Hello World", "root_path": request.scope.get("root_path")}

Which i want to have at a path other than root (e.g. /api/vi)... Again based on most tutorials and common sense, I tried to start it with e.g.:

uvicorn main:app --root-path /api/v1

The service comes up ok (on http://127.0.0.1:8000), however, the root-path seems to be ignored: Any GET request to http://127.0.0.1:8000/ gives:

message "Hello World"
root_path   "/api/v1"

and any GET request to http://127.0.0.1:8000/api/v1 gives:

detail  "Not Found"

I would expect the requests to produce the reverse outcomes... What is going on here?!?

I also tried initializing FastAPI with FastAPI(root_path="/api/v1") as well as switching to hypercorn without avail...

Details of the versions of apps (I might have tried a few others as well, though these should be the latest tried):

python                    3.9.7           hf930737_3_cpython    conda-forge
fastapi                   0.85.1             pyhd8ed1ab_0    conda-forge
uvicorn                   0.20.0           py39h06a4308_0  
hypercorn                 0.14.3           py39hf3d152e_1    conda-forge

Solution

  • As noted by @MatsLindh in the comments section, root_path (or --root-path) does not change your application's prefix path, but is rather designated for behind the proxy cases, where "you might need to use a proxy server like Traefik or Nginx with a configuration that adds an extra path prefix that is not seen by your application" (see the relevant documentation).

    As described in the documentation:

    Proxy with a stripped path prefix

    Having a proxy with a stripped path prefix, in this case, means that you could declare a path at /app in your code, but then, you add a layer on top (the proxy) that would put your FastAPI application under a path like /api/v1.

    In this case, the original path /app would actually be served at /api/v1/app.

    Even though all your code is written assuming there's just /app.

    And the proxy would be "stripping" the path prefix on the fly before transmitting the request to Uvicorn, keep your application convinced that it is serving at /app, so that you don't have to update all your code to include the prefix /api/v1.

    Up to here, everything would work as normally.

    But then, when you open the integrated docs UI (the frontend), it would expect to get the OpenAPI schema at /openapi.json, instead of /api/v1/openapi.json.

    So, the frontend (that runs in the browser) would try to reach /openapi.json and wouldn't be able to get the OpenAPI schema (it would show "Failed to load API definition" error).

    Because we have a proxy with a path prefix of /api/v1 for our app, the frontend needs to fetch the OpenAPI schema at /api/v1/openapi.json. The docs UI would also need the OpenAPI schema to declare that this API server is located at /api/v1 (behind the proxy).

    To achieve this, you can use the command line option --root-path:

     uvicorn main:app --root-path /api/v1
    

    [...]

    Alternatively, if you don't have a way to provide a command line option like --root-path or equivalent, you can set the root_path parameter when creating your FastAPI app:

    app = FastAPI(root_path="/api/v1")
    

    Option 1

    Hence, in your case, since you are not using a proxy, but rather need to have a custom prefix for your API, you could instead use an APIRouter, which allows you to define a prefix for the API routes (note that the prefix must not include a final /). You can either give the prefix when instantiating the APIRouter (e.g., router = APIRouter(prefix='/api/v1')) or using .include_router(), which, as described in the documentation, would allow you to include the same router multiple times with different prefix:

    You can also use .include_router() multiple times with the same router using different prefixes.

    This could be useful, for example, to expose the same API under different prefixes, e.g. /api/v1 and /api/latest.

    This is an advanced usage that you might not really need, but it's there in case you do.

    The /app endpoint in the example below can be accessed at http://127.0.0.1:8000/api/v1/app.

    Working Example

    from fastapi import FastAPI
    from fastapi.routing import APIRouter
    
    
    router = APIRouter()
    
    
    @router.get('/app')
    def main():
        return 'Hello world!'
    
    
    app = FastAPI()
    app.include_router(router, prefix='/api/v1')
    

    Once you have multiple versions of the API endpoints, you could use:

    from fastapi import FastAPI
    from fastapi.routing import APIRouter
    
    
    router_v1 = APIRouter()
    router_v2 = APIRouter()
    
    
    @router_v1.get('/app')
    def main():
        return 'Hello world - v1'
    
    
    @router_v2.get('/app')
    def main():
        return 'Hello world - v2'
    
    
    app = FastAPI()
    
    app.include_router(router_v1, prefix='/api/v1')
    
    app.include_router(router_v2, prefix='/api/v2')
    app.include_router(router_v2, prefix='/latest')  # optional
    

    Option 2

    Alternatively, one could also mount sub-application(s) with the desired prefix, as demonstrated in this answer and this answer (see Option 3).

    As described in the documentation:

    When you mount a sub-application as described above, FastAPI will take care of communicating the mount path for the sub-application using a mechanism from the ASGI specification called a root_path.

    That way, the sub-application will know to use that path prefix for the docs UI.

    And the sub-application could also have its own mounted sub-applications and everything would work correctly, because FastAPI handles all these root_paths automatically.

    Hence, in the example given below, you can access the /app endpoint from the main app at http://127.0.0.1:8000/app and the /app endpoint from the sub app at http://127.0.0.1:8000/api/v1/app. Similarly, the Swagger UI autodocs can be accessed at http://127.0.0.1:8000/docs and http://127.0.0.1:8000/api/v1/docs, respectively.

    Working Example

    from fastapi import FastAPI
    
    app = FastAPI()
    subapi = FastAPI()
    
    
    @app.get('/app')
    def read_main():
        return {'message': 'Hello World from main app'}
        
     
    @subapi.get('/app')
    def read_sub():
        return {'message': 'Hello World from sub API'}
    
    
    app.mount('/api/v1', subapi)