Skip to content

Add Open API prefix route - correct docs behind reverse proxy #26

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

Merged
merged 11 commits into from
Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/img/tutorial/sub-applications/image01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/img/tutorial/sub-applications/image02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
19 changes: 19 additions & 0 deletions docs/src/sub_applications/tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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.get("/sub")
def read_sub():
return {"message": "Hello World from sub API"}


app.mount("/subapi", subapi)
95 changes: 95 additions & 0 deletions docs/tutorial/sub-applications-proxy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
There are at least two situations where you could need to create your **FastAPI** application using some specific paths.

But then you need to set them up to be served with a path prefix.

It could happen if you have a:

* **Proxy** server.
* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette).

## Proxy

Having a proxy in this case means that you could declare a path at `/app`, but then, you could need to 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` will actually be served at `/api/v1/app`.

Even though your application "thinks" it is serving at `/app`.

And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`.

Up to here, everything would work as normally.

But then, when you open the integrated docs, they 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.

So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`.

And it's also needed that the returned JSON OpenAPI schema has the defined path at `/api/v1/app` (behind the proxy) instead of `/app`.

---

For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application.

See the section below, about "mounting", for an example.


## Mounting a **FastAPI** application

"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths.

You could want to do this if you have several "independent" applications that you want to separate, having their own independent OpenAPI schema and user interfaces.

### Top-level application

First, create the main, top-level, **FastAPI** application, and its path operations:

```Python hl_lines="3 6 7 8"
{!./src/sub_applications/tutorial001.py!}
```

### Sub-application

Then, create your sub-application, and its path operations.

This sub-application is just another standard FastAPI application, but this is the one that will be "mounted".

When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`:

```Python hl_lines="11 14 15 16"
{!./src/sub_applications/tutorial001.py!}
```

### Mount the sub-application

In your top-level application, `app`, mount the sub-application, `subapi`.

Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`:

```Python hl_lines="11 19"
{!./src/sub_applications/tutorial001.py!}
```

## Check the automatic API docs

Now, run `uvicorn`, if your file is at `main.py`, it would be:

```bash
uvicorn main:app --debug
```

And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.

You will see the automatic API docs for the main app, including only its own paths:

<img src="/img/tutorial/sub-applications/image01.png">


And then, open the docs for the sub-application, at <a href="http://127.0.0.1:8000/subapi/docs" target="_blank">http://127.0.0.1:8000/subapi/docs</a>.

You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix:

<img src="/img/tutorial/sub-applications/image02.png">


If you try interacting with any of the two user interfaces, they will work, because the browser will be able to talk to the correct path (or sub-path).
9 changes: 7 additions & 2 deletions fastapi/applications.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(
description: str = "",
version: str = "0.1.0",
openapi_url: Optional[str] = "/openapi.json",
openapi_prefix: str = "",
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
**extra: Dict[str, Any],
Expand All @@ -43,6 +44,7 @@ def __init__(
self.description = description
self.version = version
self.openapi_url = openapi_url
self.openapi_prefix = openapi_prefix.rstrip("/")
self.docs_url = docs_url
self.redoc_url = redoc_url
self.extra = extra
Expand All @@ -66,6 +68,7 @@ def openapi(self) -> Dict:
openapi_version=self.openapi_version,
description=self.description,
routes=self.routes,
openapi_prefix=self.openapi_prefix,
)
return self.openapi_schema

Expand All @@ -80,15 +83,17 @@ def setup(self) -> None:
self.add_route(
self.docs_url,
lambda r: get_swagger_ui_html(
openapi_url=self.openapi_url, title=self.title + " - Swagger UI"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - Swagger UI",
),
include_in_schema=False,
)
if self.openapi_url and self.redoc_url:
self.add_route(
self.redoc_url,
lambda r: get_redoc_html(
openapi_url=self.openapi_url, title=self.title + " - ReDoc"
openapi_url=self.openapi_prefix + self.openapi_url,
title=self.title + " - ReDoc",
),
include_in_schema=False,
)
Expand Down
5 changes: 3 additions & 2 deletions fastapi/openapi/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,8 @@ def get_openapi(
version: str,
openapi_version: str = "3.0.2",
description: str = None,
routes: Sequence[BaseRoute]
routes: Sequence[BaseRoute],
openapi_prefix: str = ""
) -> Dict:
info = {"title": title, "version": version}
if description:
Expand All @@ -234,7 +235,7 @@ def get_openapi(
if result:
path, security_schemes, path_definitions = result
if path:
paths.setdefault(route.path, {}).update(path)
paths.setdefault(openapi_prefix + route.path, {}).update(path)
if security_schemes:
components.setdefault("securitySchemes", {}).update(
security_schemes
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ nav:
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
- Sub Applications - Under a Proxy: 'tutorial/sub-applications-proxy.md'
- Application Configuration: 'tutorial/application-configuration.md'
- Extra Starlette options: 'tutorial/extra-starlette.md'
- Concurrency and async / await: 'async.md'
Expand Down
Empty file.
66 changes: 66 additions & 0 deletions tests/test_tutorial/test_sub_applications/test_tutorial001.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from starlette.testclient import TestClient

from sub_applications.tutorial001 import app

client = TestClient(app)

openapi_schema_main = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/app": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Main Get",
"operationId": "read_main_app_get",
}
}
},
}
openapi_schema_sub = {
"openapi": "3.0.2",
"info": {"title": "Fast API", "version": "0.1.0"},
"paths": {
"/subapi/sub": {
"get": {
"responses": {
"200": {
"description": "Successful Response",
"content": {"application/json": {"schema": {}}},
}
},
"summary": "Read Sub Get",
"operationId": "read_sub_sub_get",
}
}
},
}


def test_openapi_schema_main():
response = client.get("/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema_main


def test_main():
response = client.get("/app")
assert response.status_code == 200
assert response.json() == {"message": "Hello World from main app"}


def test_openapi_schema_sub():
response = client.get("/subapi/openapi.json")
assert response.status_code == 200
assert response.json() == openapi_schema_sub


def test_sub():
response = client.get("/subapi/sub")
assert response.status_code == 200
assert response.json() == {"message": "Hello World from sub API"}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy