Skip to content

Commit 0ea0d0e

Browse files
Kabir Khantiangolo
Kabir Khan
authored andcommitted
Add Open API prefix route - correct docs behind reverse proxy (#26)
Add Open API prefix route - correct docs behind reverse proxy.
1 parent 890f1f7 commit 0ea0d0e

File tree

9 files changed

+191
-4
lines changed

9 files changed

+191
-4
lines changed
49.3 KB
Loading
52.1 KB
Loading
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from fastapi import FastAPI
2+
3+
app = FastAPI()
4+
5+
6+
@app.get("/app")
7+
def read_main():
8+
return {"message": "Hello World from main app"}
9+
10+
11+
subapi = FastAPI(openapi_prefix="/subapi")
12+
13+
14+
@subapi.get("/sub")
15+
def read_sub():
16+
return {"message": "Hello World from sub API"}
17+
18+
19+
app.mount("/subapi", subapi)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
There are at least two situations where you could need to create your **FastAPI** application using some specific paths.
2+
3+
But then you need to set them up to be served with a path prefix.
4+
5+
It could happen if you have a:
6+
7+
* **Proxy** server.
8+
* You are "**mounting**" a FastAPI application inside another FastAPI application (or inside another ASGI application, like Starlette).
9+
10+
## Proxy
11+
12+
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`.
13+
14+
In this case, the original path `/app` will actually be served at `/api/v1/app`.
15+
16+
Even though your application "thinks" it is serving at `/app`.
17+
18+
And the Proxy could be re-writing the path "on the fly" to keep your application convinced that it is serving at `/app`.
19+
20+
Up to here, everything would work as normally.
21+
22+
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`.
23+
24+
So, the frontend (that runs in the browser) would try to reach `/openapi.json` and wouldn't be able to get the OpenAPI schema.
25+
26+
So, it's needed that the frontend looks for the OpenAPI schema at `/api/v1/openapi.json`.
27+
28+
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`.
29+
30+
---
31+
32+
For these cases, you can declare an `openapi_prefix` parameter in your `FastAPI` application.
33+
34+
See the section below, about "mounting", for an example.
35+
36+
37+
## Mounting a **FastAPI** application
38+
39+
"Mounting" means adding a complete "independent" application in a specific path, that then takes care of handling all the sub-paths.
40+
41+
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.
42+
43+
### Top-level application
44+
45+
First, create the main, top-level, **FastAPI** application, and its path operations:
46+
47+
```Python hl_lines="3 6 7 8"
48+
{!./src/sub_applications/tutorial001.py!}
49+
```
50+
51+
### Sub-application
52+
53+
Then, create your sub-application, and its path operations.
54+
55+
This sub-application is just another standard FastAPI application, but this is the one that will be "mounted".
56+
57+
When creating the sub-application, use the parameter `openapi_prefix`. In this case, with a prefix of `/subapi`:
58+
59+
```Python hl_lines="11 14 15 16"
60+
{!./src/sub_applications/tutorial001.py!}
61+
```
62+
63+
### Mount the sub-application
64+
65+
In your top-level application, `app`, mount the sub-application, `subapi`.
66+
67+
Here you need to make sure you use the same path that you used for the `openapi_prefix`, in this case, `/subapi`:
68+
69+
```Python hl_lines="11 19"
70+
{!./src/sub_applications/tutorial001.py!}
71+
```
72+
73+
## Check the automatic API docs
74+
75+
Now, run `uvicorn`, if your file is at `main.py`, it would be:
76+
77+
```bash
78+
uvicorn main:app --debug
79+
```
80+
81+
And open the docs at <a href="http://127.0.0.1:8000/docs" target="_blank">http://127.0.0.1:8000/docs</a>.
82+
83+
You will see the automatic API docs for the main app, including only its own paths:
84+
85+
<img src="/img/tutorial/sub-applications/image01.png">
86+
87+
88+
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>.
89+
90+
You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix:
91+
92+
<img src="/img/tutorial/sub-applications/image02.png">
93+
94+
95+
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).

fastapi/applications.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def __init__(
2525
description: str = "",
2626
version: str = "0.1.0",
2727
openapi_url: Optional[str] = "/openapi.json",
28+
openapi_prefix: str = "",
2829
docs_url: Optional[str] = "/docs",
2930
redoc_url: Optional[str] = "/redoc",
3031
**extra: Dict[str, Any],
@@ -43,6 +44,7 @@ def __init__(
4344
self.description = description
4445
self.version = version
4546
self.openapi_url = openapi_url
47+
self.openapi_prefix = openapi_prefix.rstrip("/")
4648
self.docs_url = docs_url
4749
self.redoc_url = redoc_url
4850
self.extra = extra
@@ -66,6 +68,7 @@ def openapi(self) -> Dict:
6668
openapi_version=self.openapi_version,
6769
description=self.description,
6870
routes=self.routes,
71+
openapi_prefix=self.openapi_prefix,
6972
)
7073
return self.openapi_schema
7174

@@ -80,15 +83,17 @@ def setup(self) -> None:
8083
self.add_route(
8184
self.docs_url,
8285
lambda r: get_swagger_ui_html(
83-
openapi_url=self.openapi_url, title=self.title + " - Swagger UI"
86+
openapi_url=self.openapi_prefix + self.openapi_url,
87+
title=self.title + " - Swagger UI",
8488
),
8589
include_in_schema=False,
8690
)
8791
if self.openapi_url and self.redoc_url:
8892
self.add_route(
8993
self.redoc_url,
9094
lambda r: get_redoc_html(
91-
openapi_url=self.openapi_url, title=self.title + " - ReDoc"
95+
openapi_url=self.openapi_prefix + self.openapi_url,
96+
title=self.title + " - ReDoc",
9297
),
9398
include_in_schema=False,
9499
)

fastapi/openapi/utils.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,8 @@ def get_openapi(
215215
version: str,
216216
openapi_version: str = "3.0.2",
217217
description: str = None,
218-
routes: Sequence[BaseRoute]
218+
routes: Sequence[BaseRoute],
219+
openapi_prefix: str = ""
219220
) -> Dict:
220221
info = {"title": title, "version": version}
221222
if description:
@@ -234,7 +235,7 @@ def get_openapi(
234235
if result:
235236
path, security_schemes, path_definitions = result
236237
if path:
237-
paths.setdefault(route.path, {}).update(path)
238+
paths.setdefault(openapi_prefix + route.path, {}).update(path)
238239
if security_schemes:
239240
components.setdefault("securitySchemes", {}).update(
240241
security_schemes

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ nav:
5757
- SQL (Relational) Databases: 'tutorial/sql-databases.md'
5858
- NoSQL (Distributed / Big Data) Databases: 'tutorial/nosql-databases.md'
5959
- Bigger Applications - Multiple Files: 'tutorial/bigger-applications.md'
60+
- Sub Applications - Under a Proxy: 'tutorial/sub-applications-proxy.md'
6061
- Application Configuration: 'tutorial/application-configuration.md'
6162
- Extra Starlette options: 'tutorial/extra-starlette.md'
6263
- Concurrency and async / await: 'async.md'

tests/test_tutorial/test_sub_applications/__init__.py

Whitespace-only changes.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
from starlette.testclient import TestClient
2+
3+
from sub_applications.tutorial001 import app
4+
5+
client = TestClient(app)
6+
7+
openapi_schema_main = {
8+
"openapi": "3.0.2",
9+
"info": {"title": "Fast API", "version": "0.1.0"},
10+
"paths": {
11+
"/app": {
12+
"get": {
13+
"responses": {
14+
"200": {
15+
"description": "Successful Response",
16+
"content": {"application/json": {"schema": {}}},
17+
}
18+
},
19+
"summary": "Read Main Get",
20+
"operationId": "read_main_app_get",
21+
}
22+
}
23+
},
24+
}
25+
openapi_schema_sub = {
26+
"openapi": "3.0.2",
27+
"info": {"title": "Fast API", "version": "0.1.0"},
28+
"paths": {
29+
"/subapi/sub": {
30+
"get": {
31+
"responses": {
32+
"200": {
33+
"description": "Successful Response",
34+
"content": {"application/json": {"schema": {}}},
35+
}
36+
},
37+
"summary": "Read Sub Get",
38+
"operationId": "read_sub_sub_get",
39+
}
40+
}
41+
},
42+
}
43+
44+
45+
def test_openapi_schema_main():
46+
response = client.get("/openapi.json")
47+
assert response.status_code == 200
48+
assert response.json() == openapi_schema_main
49+
50+
51+
def test_main():
52+
response = client.get("/app")
53+
assert response.status_code == 200
54+
assert response.json() == {"message": "Hello World from main app"}
55+
56+
57+
def test_openapi_schema_sub():
58+
response = client.get("/subapi/openapi.json")
59+
assert response.status_code == 200
60+
assert response.json() == openapi_schema_sub
61+
62+
63+
def test_sub():
64+
response = client.get("/subapi/sub")
65+
assert response.status_code == 200
66+
assert response.json() == {"message": "Hello World from sub API"}

0 commit comments

Comments
 (0)
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