fastapi: OpenAPI routes do not acknowledge root_path

Also raised on https://github.com/tiangolo/fastapi/pull/26#issuecomment-562755647. See also #544.

Describe the bug

Write here a clear and concise description of what the bug is.

To Reproduce

Replace each part with your own scenario:

  1. Create a file with:
from fastapi import FastAPI

app = FastAPI()

@app.get("/app")
def read_root():
    return {"Hello": "World"}
  1. Launch it using uvicorn --root-path="bar" test_app:app
  2. Open the browser and go to http://127.0.0.1:8000/docs.
  3. From the documentation, call the GET /app route.
  4. The doc page calls /app and succeeds.

Expected behavior

The above test should fail after having called /bar/app, since root_path is supposed to prefix all generated URLs in case the application is served behind a reverse-proxy, among ther things. FastAPI only acknowledges openapi_prefix for the API doc.

Environment

  • OS: Windows
  • FastAPI Version: 0.45.0
  • Python version: 3.8.0

Additional context

A similar issue applies to sub-applications:

from fastapi import FastAPI

app = FastAPI()


@app.get("/app")
def read_main():
    return {"message": "Hello World from main app"}


#subapi = FastAPI(openapi_prefix="/subapi")
subapi = FastAPI()

@subapi.get("/sub")
def read_sub(request: Request):
    return {
        "root_path": request.scope['root_path'],
        "raw_path": request.scope['raw_path'],
        "path": request.scope['path'],
        "app_url_for": app.url_path_for("read_sub"),
        "subapp_url_for": subapi.url_path_for("read_sub"),
    }

app.mount("/subapi", subapi)
{
  "root_path":"bar/subapi",
  "raw_path":"/subapi/sub",
  "path":"/sub",
  "app_url_for":"/subapi/sub",
  "subapp_url_for":"/sub"
}

(url_for not being prefixed with root_path is fixed upstream by encode/starlette#699)

Unless openapi_prefix="/subapi" is passed when creating the subapplication, both http://127.0.0.1:8000/docs and http://127.0.0.1:8000/subapi/docs will point towards http://127.0.0.1:8000/openapi.json, which goes against the point of having isolated subapplications.

openapi_prefix should probably just be deprecated and assumed to match root_path if absent.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 16
  • Comments: 18 (8 by maintainers)

Most upvoted comments

I feel that this is closely related to a question I was about to write, so I will post it here instead to avoid duplication.

– Hi, I’m deploying a minimal application written with FastAPI and Mangum as an adapter using AWS SAM. It works like a charm, and I’m in the process off writing an article about it to append in the fastapi documentation. Yet one thing that boggles my mind is the path of the /openapi.json and I can’t wrap my head around how to work it in different situations.

So let’s assume I have a little application like this:

from fastapi import FastAPI
from example_app.api.api_v1.api import router as api_router
from example_app.core.config import API_V1_STR, PROJECT_NAME
from mangum import Mangum

app = FastAPI(
    title=PROJECT_NAME,
    # if not custom domain
    # openapi_prefix="/prod"
)
app.include_router(api_router, prefix=API_V1_STR)

@app.get("/ping")
def pong():
    return {"ping": "pong!"}

handler = Mangum(app, enable_lifespan=False, )

I deploy it to API Gateway/Lambda to a stage called prod so the resulting url is https://xxxxxxxxxx.execute-api.eu-west-1.amazonaws.com/prod. The API Gateway is set up with {proxy+} integration. Now how is it, that the /ping endpoint “knows” that the base-url is […].amazonaws.com/prod, yet openapi.json assumes to be at […].amazonaws.com/openapi.json?
I know I can change the prefix with openapi_prefix="/prod" but that makes it inconvenient if I wanted to use another stage than prod. After all I don’t have to do it for my other endpoints either. So is there a reason it doesn’t work the same way as with my other endpoints? Is it a bug, or am I just missing something very obvious?

Recently ran into this issue and the best solution I could think of was monkey patching openapi.json:

app = FastAPI(
    title=settings.PROJECT_NAME,
    description=settings.PROJECT_DESCRIPTION,
    version=settings.PROJECT_VERSION,
    openapi_url=None,
    openapi_tags=openapi_tags,
    docs_url=None,
    redoc_url=None,
    root_path=settings.ROOT_PATH,
)

@app.get("/", include_in_schema=False)
async def access_documentation():
    openapi_url = app.root_path + "/openapi.json"
    return get_swagger_ui_html(openapi_url=openapi_url, title="docs")

@app.get("/openapi.json", include_in_schema=False)
async def access_openapi():
    openapi = get_openapi(
        title=app.title,
        version=app.version,
        description=app.description,
        routes=app.routes,
        tags=app.openapi_tags,
    )

    monkey_patched_openapi = {
        key: value for key, value in openapi.items() if key != "paths"
    }
    monkey_patched_openapi["paths"] = {}
    for key, value in openapi["paths"].items():
        monkey_patched_openapi["paths"][app.root_path + key] = value

    return monkey_patched_openapi

Not elegant, but this did exactly what I was looking for: serving my API at /api/v1/ with swaggerui working as expected.

Edit: I had to declare separate openapi functions because I wanted to protected them using basic auth, which is not used for anything else in the app (dependencies not shown above for clarity).

FYI, I’m running behind NGINX in docker-compose which looks like this:

events {}

http {
    upstream backend_server {
        server api:80;
    }

    server {
        listen 80;

        location /api/v1/ {
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_pass http://backend_server/;
        }
    }
}

Edit 2: the above can be achieved in a simpler way with a downside (or upside) of the “servers” drop-down appearing and the /api/v1 prefix disappearing:

@app.get("/openapi.json", include_in_schema=False)
async def access_openapi():
    openapi = get_openapi(
        title=app.title,
        version=app.version,
        description=app.description,
        routes=app.routes,
        tags=app.openapi_tags,
    )
    openapi["servers"] = [{"url": app.root_path}]
    return openapi

@sm-Fifteen @ycd Thanks guys! That’s pretty much what I ended up doing. I just wanna say the response time is amazing! I was expecting to wait 5-10 days 😃

As raised in #1294, another possibility could be to have the servers part of the OpenAPI document contain a single server with the root_path as its url (no matter how deeply the application happens to be nested), so that the rest of the routing and OpenAPI route generation logic can remain the same, while still letting OpenAPI clients (including the doc) know that all API calls should be prefixed.

openapi_prefix could then be deprecated as an alternate way of specifying the same thing, except passed directly at the application’s creation rather than as defined by the ASGI protocol, and unaccounted for when using url_for or url_path_for.

Therefore, using the example app from the initial post:

from fastapi import FastAPI, Request

app = FastAPI()


@app.get("/app")
def read_main():
    return {"message": "Hello World from main app"}


#subapi = FastAPI(openapi_prefix="/subapi")
subapi = FastAPI()

@subapi.get("/sub")
def read_sub(request: Request):
    return {
        "root_path": request.scope['root_path'],
        "raw_path": request.scope['raw_path'],
        "path": request.scope['path'],
        "app_url_for": app.url_path_for("read_sub"),
        "subapp_url_for": subapi.url_path_for("read_sub"),
    }

app.mount("/subapi", subapi)

We would get:

uvicorn test_submounts:app --root-path="/foo/"

/openapi.json

{
  "openapi": "3.0.2",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "servers": [
    {
      "url": "/foo/"
    }
  ],
  "paths": {
    "/app": {
      "get": {
        "summary": "Read Main",
        "operationId": "read_main_app_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    }
  }
}

/subapi/openapi.json

{
  "openapi": "3.0.2",
  "info": {
    "title": "FastAPI",
    "version": "0.1.0"
  },
  "paths": {
    "/sub": {
      "get": {
        "summary": "Read Sub",
        "operationId": "read_sub_sub_get",
        "responses": {
          "200": {
            "description": "Successful Response",
            "content": {
              "application/json": {
                "schema": {}
              }
            }
          }
        }
      }
    }
  },
  "servers": [
    {
      "url": "/foo/subapi"
    }
  ]
}

Which does exactly what we need in terms of ensuring the doc acknowledges the reverse proxy, and everything else just falls into place.

Running into the same issue as @iwpnd - I am unable to host OpenAPI docs via Lambda because the openapi.json path isn’t respecting the base URL prefix.

you can use it like that

app = FastAPI(
    title=PROJECT_NAME,
    # if not custom domain
    openapi_prefix="/your_stage"
)

but yeah, it’s not ideal.

Faced it when I was using uvicorn-gunicorn-fastapi-docker which was configured as nginx sub app

Example config:

location /fastapi/ {
    proxy_pass http://0.0.0.0:8088/;
    proxy_set_header Accept-Encoding "";
    sub_filter "http://0.0.0.0:8088/" "http://0.0.0.0:8088/fastapi/";
    sub_filter_once off;
}

@andreixk when you mount an application it basically looks into the route.path for route in app.routes, by default your root_path is / and app.routes looks like this

[
     {'path': '/openapi.json'},
     {'path': '/docs'},
     {'path': '/docs/oauth2-redirect'},
     {'path': '/redoc'},
]

When you mount another application you are just adding that route to the app.routes, for example, let us mount /subapi to our /app

Now it will look like this

[
     {'path': '/openapi.json'},
     {'path': '/docs'},
     {'path': '/docs/oauth2-redirect'},
     {'path': '/redoc'},
     {'path': '/subapi'},
]

You are just making /subapi accessible from your root_path so when you go to / you can access /subapi since that path is accessible from the root_path

When you go to /subapi it basically looks into the subapi.routes so imagine if you have an endpoint called /dummy and it is declared inside of subapi.routes, you can access it because when you go to /subapi it becomes your route path and after when you go to an endpoint from /subapi/dummy you can access it because it is declared inside subapi.routes

So if you are not behind the proxy you can use prefix which simply does the same thing underneath for example, when you are including a router

app.include_router(api, prefix="/api")

It just adds a prefix when adding the paths that declared inside APIRouter instance, is it still confusing? Let’s keep going, imagine you have an instance of APIRouter and you want to include that router to your app

from fastapi import APIRouter

api = APIRouter()

@app.get("/dummy")
...

@app.post("/dummier")
...

@app.delete("/dummiest")
...

From the main application when you are including it

from fastapi import FastAPI
from somewhere import api


app = FastAPI()


app.include_router(api, prefix="/api")

This only adds a prefix when adding paths to the app.routes

So in your case adding a prefix should be enough when including your router.

If you are still getting Not found .... just look into your app.routes like I did above. You will probably find your openapi.json is not prefixed and your other paths are prefixed or the opposite not sure.

@andreixk: No, root_path is only useful for the application to know what to prefix URL when generating URLs for its own routes, so it only makes sense to use it if you have a proxy that’s making it so what your application sees as localhost:8000/hello is actually exposed to the outside world as 192,168,1,123:80/api/v1/hello by a proxy. Without root_path, the application would generate route URLs that look like /hello in the documentation, and clients would understand it as meaning 192,168,1,123:80/hello, which doesn’t exist.

If you just want a route mounted at /api/v1/, you probably want to have all of your routes defined on an APIRouter instead of your app directly, and then mount that router on your app at /api/v1, like documented here.

@iwpnd Hah nice response time… I just dove through the source and started seeing that but haven’t tested it yet. Thanks for verifying that