Skip to content
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

OpenAPI routes do not acknowledge root_path #829

Closed
sm-Fifteen opened this issue Jan 6, 2020 · 18 comments
Closed

OpenAPI routes do not acknowledge root_path #829

sm-Fifteen opened this issue Jan 6, 2020 · 18 comments
Labels
answered bug Something isn't working reviewed

Comments

@sm-Fifteen
Copy link
Contributor

sm-Fifteen commented Jan 6, 2020

Also raised on #26 (comment). 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.

@sm-Fifteen sm-Fifteen added the bug Something isn't working label Jan 6, 2020
@and-sm
Copy link

and-sm commented Jan 12, 2020

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;
}

@iwpnd
Copy link
Contributor

iwpnd commented Jan 21, 2020

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?

@kevlarr
Copy link

kevlarr commented Feb 21, 2020

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.

@iwpnd
Copy link
Contributor

iwpnd commented Feb 21, 2020

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.

@kevlarr
Copy link

kevlarr commented Feb 21, 2020

@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

@kevlarr
Copy link

kevlarr commented Feb 21, 2020

Ah, so I think my issue is that I'm combining FastAPI with Mangum, with the latter's api_gateway_base_path param set to strip away the "v2" prefix fronting all of my FastAPI routes.

As far as my app is concerned, there is no "v2" prefix (because it's a sub-app and I felt that it shouldn't have knowledge of where it lives) but this breaks the "openapi.json" URL link in the HTML docs pages because it doesn't know to append "v2/" to the URL.

Neither the openapi_prefix or openapi_url params for the FastAPI class can fix this, because I need the app to respond to /openapi.json while including the prefix ONLY in the generated HTML. The openapi_prefix is for some reason adding the prefix twice, so I wind up getting "/v2/v2/openapi.json"

@yeyeric
Copy link

yeyeric commented May 25, 2020

same here with AWS Lambda and API Gateway, my standard base path is api.mycompany.com and I have /my_stage (staging / production)
when going to api.mycompany.com/staging/docs it doesn't work

how should we do ?
Adding /staging in openurl_prefix doesn't fix this unfortunately

@sm-Fifteen
Copy link
Contributor Author

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.

@tiangolo
Copy link
Member

tiangolo commented Jun 11, 2020

Hey all, thanks for the discussion here! 🍰

This was fixed in #1199 implementing support for root_path 🚀 🎉

Available in FastAPI 0.56.0.

You can read the new docs at Sub Applications - Mounts, which now ✨ just works ✨ without any extra parameters.

And the new extensive docs at Behind a Proxy that explain it all in detail, including how to test it all using a local Traefik proxy on top of Uvicorn, to test all the root_path workflow (with the parameter --root-path).


This took me a bit of time to handle as I wanted to first re-read the WSGI and ASGI specs to make sure I wasn't missing anything.

And then to test it all with an actual reverse proxy with path prefix stripping (what the root_path would be used for).

@sm-Fifteen
Copy link
Contributor Author

@tiangolo: What's your opinion about prepending the root_path/mount path to every route in the openapi doc (what we're doing now) compared to declaring the root as a server object so the endpoint paths don't change if the server runs with a different root_path (what I suggested above)?

@github-actions github-actions bot removed the answered label Jun 12, 2020
@sm-Fifteen
Copy link
Contributor Author

Closing this, now that #1596 has been merged. I much prefer this new approach compared to the other one, since it seems more in line with how the OpenAPI specs intends its definitons to look (the routes known to Starlette actually match what's in openapi.yml).

@andreixk
Copy link

I'm a little confused. if i'm not behind any proxy, i just want all routes to be prefixed with /api/v1 (e.g. /api/v1/user), root_path option doesn't work, i'm getting the "Not Found /api/v1/openapi.json" error when i go to /docs. So do I need to manually add "/api/v1" to every @app.get entry?

@sm-Fifteen
Copy link
Contributor Author

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

@ycd
Copy link
Contributor

ycd commented Nov 11, 2020

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

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

@georgebv
Copy link

georgebv commented Apr 2, 2021

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

@carderne
Copy link

Adding some commentary here for any future travelers.
Monkey patching the root_path into the URLs is not recommended (see discussion #1294 and OpenAPI docs).
Instead you should add the ["servers"] directive, but this can be done a bit more ergonomically thanks to #1547:

app = FastAPI(
    openapi_url="/api",
    root_path=settings.ROOT_PATH,
    servers=[{"url": settings.ROOT_PATH}],
)

That's it. No need to create your own access_openapi() route or anything.

@tiangolo tiangolo changed the title [BUG] OpenAPI routes do not acknowledge root_path OpenAPI routes do not acknowledge root_path Feb 24, 2023
@C-Duv
Copy link

C-Duv commented Dec 6, 2023

app = FastAPI(
    openapi_url="/api",
    root_path=settings.ROOT_PATH,
    servers=[{"url": settings.ROOT_PATH}],
)

Isn't openapi_url supposed to points to a openapi.json file? Here it points to a directory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
answered bug Something isn't working reviewed
Projects
None yet
Development

No branches or pull requests