diff --git a/docs/en/docs/advanced/behind-a-proxy.md b/docs/en/docs/advanced/behind-a-proxy.md new file mode 100644 index 000000000..660e374a4 --- /dev/null +++ b/docs/en/docs/advanced/behind-a-proxy.md @@ -0,0 +1,281 @@ +# Behind a Proxy + +In some situations, 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. + +In these cases you can use `root_path` to configure your application. + +The `root_path` is a mechanism provided by the ASGI specification (that FastAPI is built on, through Starlette). + +The `root_path` is used to handle these specific cases. + +And it's also used internally when mounting sub-applications. + +## 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. + +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`. + +```mermaid +graph LR + +browser("Browser") +proxy["Proxy on http://0.0.0.0:9999/api/v1/app"] +server["Server on http://127.0.0.1:8000/app"] + +browser --> proxy +proxy --> server +``` + +!!! tip + The IP `0.0.0.0` is commonly used to mean that the program listens on all the IPs available in that machine/server. + +The docs UI would also need that the JSON payload with the OpenAPI schema has the path defined as `/api/v1/app` (behind the proxy) instead of `/app`. For example, something like: + +```JSON hl_lines="5" +{ + "openapi": "3.0.2", + // More stuff here + "paths": { + "/api/v1/app": { + // More stuff here + } + } +} +``` + +In this example, the "Proxy" could be something like **Traefik**. And the server would be something like **Uvicorn**, running your FastAPI application. + +### Providing the `root_path` + +To achieve this, you can use the command line option `--root-path` like: + +
+
+But if we access the docs UI at the "official" URL using the proxy, at `/api/v1/docs`, it works correctly! 🎉
+
+Right as we wanted it. ✔️
+
+This is because FastAPI uses this `root_path` internally to tell the docs UI to use the URL for OpenAPI with the path prefix provided by `root_path`.
+
+You can check it at http://127.0.0.1:9999/api/v1/docs:
+
+
+
+## Mounting a sub-application
+
+If you need to mount a sub-application (as described in [Sub Applications - Mounts](./sub-applications.md){.internal-link target=_blank}) while also using a proxy with `root_path`, you can do it normally, as you would expect.
+
+FastAPI will internally use the `root_path` smartly, so it will just work. ✨
diff --git a/docs/en/docs/advanced/extending-openapi.md b/docs/en/docs/advanced/extending-openapi.md
index f98be49a6..30cd857d5 100644
--- a/docs/en/docs/advanced/extending-openapi.md
+++ b/docs/en/docs/advanced/extending-openapi.md
@@ -52,15 +52,22 @@ First, write all your **FastAPI** application as normally:
Then, use the same utility function to generate the OpenAPI schema, inside a `custom_openapi()` function:
-```Python hl_lines="2 15 16 17 18 19 20"
+```Python hl_lines="2 15 16 17 18 19 20 21"
{!../../../docs_src/extending_openapi/tutorial001.py!}
```
+!!! tip
+ The `openapi_prefix` will contain any prefix needed for the generated OpenAPI *path operations*.
+
+ FastAPI will automatically use the `root_path` to pass it in the `openapi_prefix`.
+
+ But the important thing is that your function should receive that parameter `openapi_prefix` and pass it along.
+
### Modify the OpenAPI schema
Now you can add the ReDoc extension, adding a custom `x-logo` to the `info` "object" in the OpenAPI schema:
-```Python hl_lines="21 22 23"
+```Python hl_lines="22 23 24"
{!../../../docs_src/extending_openapi/tutorial001.py!}
```
@@ -72,7 +79,7 @@ That way, your application won't have to generate the schema every time a user o
It will be generated only once, and then the same cached schema will be used for the next requests.
-```Python hl_lines="13 14 24 25"
+```Python hl_lines="13 14 25 26"
{!../../../docs_src/extending_openapi/tutorial001.py!}
```
@@ -80,7 +87,7 @@ It will be generated only once, and then the same cached schema will be used for
Now you can replace the `.openapi()` method with your new function.
-```Python hl_lines="28"
+```Python hl_lines="29"
{!../../../docs_src/extending_openapi/tutorial001.py!}
```
diff --git a/docs/en/docs/advanced/sub-applications-proxy.md b/docs/en/docs/advanced/sub-applications-proxy.md
deleted file mode 100644
index 03a7f9446..000000000
--- a/docs/en/docs/advanced/sub-applications-proxy.md
+++ /dev/null
@@ -1,100 +0,0 @@
-# Sub Applications - Behind a Proxy, Mounts
-
-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"
-{!../../../docs_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"
-{!../../../docs_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"
-{!../../../docs_src/sub_applications/tutorial001.py!}
-```
-
-## Check the automatic API docs
-
-Now, run `uvicorn`, if your file is at `main.py`, it would be:
-
-
-
-And then, open the docs for the sub-application, at http://127.0.0.1:8000/subapi/docs.
-
-You will see the automatic API docs for the sub-application, including only its own sub-paths, with their correct prefix:
-
-
-
-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).
diff --git a/docs/en/docs/advanced/sub-applications.md b/docs/en/docs/advanced/sub-applications.md
new file mode 100644
index 000000000..68d5790db
--- /dev/null
+++ b/docs/en/docs/advanced/sub-applications.md
@@ -0,0 +1,73 @@
+# Sub Applications - Mounts
+
+If you need to have two independent FastAPI applications, with their own independent OpenAPI and their own docs UIs, you can have a main app and "mount" one (or more) sub-application(s).
+
+## Mounting a **FastAPI** application
+
+"Mounting" means adding a completely "independent" application in a specific path, that then takes care of handling all everything under that path, with the _path operations_ declared in that sub-application.
+
+### Top-level application
+
+First, create the main, top-level, **FastAPI** application, and its *path operations*:
+
+```Python hl_lines="3 6 7 8"
+{!../../../docs_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":
+
+```Python hl_lines="11 14 15 16"
+{!../../../docs_src/sub_applications/tutorial001.py!}
+```
+
+### Mount the sub-application
+
+In your top-level application, `app`, mount the sub-application, `subapi`.
+
+In this case, it will be mounted at the path `/subapi`:
+
+```Python hl_lines="11 19"
+{!../../../docs_src/sub_applications/tutorial001.py!}
+```
+
+### Check the automatic API docs
+
+Now, run `uvicorn` with the main app, if your file is `main.py`, it would be:
+
+
+
+And then, open the docs for the sub-application, at http://127.0.0.1:8000/subapi/docs.
+
+You will see the automatic API docs for the sub-application, including only its own _path operations_, all under the correct sub-path prefix `/subapi`:
+
+
+
+If you try interacting with any of the two user interfaces, they will work correctly, because the browser will be able to talk to each specific app or sub-app.
+
+### Technical Details: `root_path`
+
+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_path`s automatically.
+
+You will learn more about the `root_path` and how to use it explicitly in the section about [Behind a Proxy](./behind-a-proxy.md){.internal-link target=_blank}.
diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image01.png b/docs/en/docs/img/tutorial/behind-a-proxy/image01.png
new file mode 100644
index 000000000..4ceae4421
Binary files /dev/null and b/docs/en/docs/img/tutorial/behind-a-proxy/image01.png differ
diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image02.png b/docs/en/docs/img/tutorial/behind-a-proxy/image02.png
new file mode 100644
index 000000000..801203140
Binary files /dev/null and b/docs/en/docs/img/tutorial/behind-a-proxy/image02.png differ
diff --git a/docs/en/mkdocs.yml b/docs/en/mkdocs.yml
index 1dac0fde4..ee500318e 100644
--- a/docs/en/mkdocs.yml
+++ b/docs/en/mkdocs.yml
@@ -92,7 +92,8 @@ nav:
- advanced/sql-databases-peewee.md
- advanced/async-sql-databases.md
- advanced/nosql-databases.md
- - advanced/sub-applications-proxy.md
+ - advanced/sub-applications.md
+ - advanced/behind-a-proxy.md
- advanced/templates.md
- advanced/graphql.md
- advanced/websockets.md
diff --git a/docs_src/behind_a_proxy/tutorial001.py b/docs_src/behind_a_proxy/tutorial001.py
new file mode 100644
index 000000000..ede59ada1
--- /dev/null
+++ b/docs_src/behind_a_proxy/tutorial001.py
@@ -0,0 +1,8 @@
+from fastapi import FastAPI, Request
+
+app = FastAPI()
+
+
+@app.get("/app")
+def read_main(request: Request):
+ return {"message": "Hello World", "root_path": request.scope.get("root_path")}
diff --git a/docs_src/behind_a_proxy/tutorial002.py b/docs_src/behind_a_proxy/tutorial002.py
new file mode 100644
index 000000000..c1600cde9
--- /dev/null
+++ b/docs_src/behind_a_proxy/tutorial002.py
@@ -0,0 +1,8 @@
+from fastapi import FastAPI, Request
+
+app = FastAPI(root_path="/api/v1")
+
+
+@app.get("/app")
+def read_main(request: Request):
+ return {"message": "Hello World", "root_path": request.scope.get("root_path")}
diff --git a/docs_src/extending_openapi/tutorial001.py b/docs_src/extending_openapi/tutorial001.py
index 561e95898..d9d7e9844 100644
--- a/docs_src/extending_openapi/tutorial001.py
+++ b/docs_src/extending_openapi/tutorial001.py
@@ -9,7 +9,7 @@ async def read_items():
return [{"name": "Foo"}]
-def custom_openapi():
+def custom_openapi(openapi_prefix: str):
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
@@ -17,6 +17,7 @@ def custom_openapi():
version="2.5.0",
description="This is a very custom OpenAPI schema",
routes=app.routes,
+ openapi_prefix=openapi_prefix,
)
openapi_schema["info"]["x-logo"] = {
"url": "https://fastapi.tiangolo.com/img/logo-margin/logo-teal.png"
diff --git a/docs_src/sub_applications/tutorial001.py b/docs_src/sub_applications/tutorial001.py
index 3b1f77a82..57e627e80 100644
--- a/docs_src/sub_applications/tutorial001.py
+++ b/docs_src/sub_applications/tutorial001.py
@@ -8,7 +8,7 @@ def read_main():
return {"message": "Hello World from main app"}
-subapi = FastAPI(openapi_prefix="/subapi")
+subapi = FastAPI()
@subapi.get("/sub")
diff --git a/fastapi/applications.py b/fastapi/applications.py
index a5dfa4fdf..39e694fae 100644
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -8,6 +8,7 @@ from fastapi.exception_handlers import (
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
+from fastapi.logger import logger
from fastapi.openapi.docs import (
get_redoc_html,
get_swagger_ui_html,
@@ -36,7 +37,6 @@ class FastAPI(Starlette):
description: str = "",
version: str = "0.1.0",
openapi_url: Optional[str] = "/openapi.json",
- openapi_prefix: str = "",
default_response_class: Type[Response] = JSONResponse,
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
@@ -46,6 +46,8 @@ class FastAPI(Starlette):
exception_handlers: Dict[Union[int, Type[Exception]], Callable] = None,
on_startup: Sequence[Callable] = None,
on_shutdown: Sequence[Callable] = None,
+ openapi_prefix: str = "",
+ root_path: str = "",
**extra: Dict[str, Any],
) -> None:
self.default_response_class = default_response_class
@@ -68,7 +70,15 @@ class FastAPI(Starlette):
self.description = description
self.version = version
self.openapi_url = openapi_url
- self.openapi_prefix = openapi_prefix.rstrip("/")
+ # TODO: remove when discarding the openapi_prefix parameter
+ if openapi_prefix:
+ logger.warning(
+ '"openapi_prefix" has been deprecated in favor of "root_path", which '
+ "follows more closely the ASGI standard, is simpler, and more "
+ "automatic. Check the docs at "
+ "https://fastapi.tiangolo.com/advanced/sub-applications-proxy/"
+ )
+ self.root_path = root_path or openapi_prefix
self.docs_url = docs_url
self.redoc_url = redoc_url
self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
@@ -84,7 +94,7 @@ class FastAPI(Starlette):
self.openapi_schema: Optional[Dict[str, Any]] = None
self.setup()
- def openapi(self) -> Dict:
+ def openapi(self, openapi_prefix: str = "") -> Dict:
if not self.openapi_schema:
self.openapi_schema = get_openapi(
title=self.title,
@@ -92,7 +102,7 @@ class FastAPI(Starlette):
openapi_version=self.openapi_version,
description=self.description,
routes=self.routes,
- openapi_prefix=self.openapi_prefix,
+ openapi_prefix=openapi_prefix,
)
return self.openapi_schema
@@ -100,17 +110,22 @@ class FastAPI(Starlette):
if self.openapi_url:
async def openapi(req: Request) -> JSONResponse:
- return JSONResponse(self.openapi())
+ root_path = req.scope.get("root_path", "").rstrip("/")
+ return JSONResponse(self.openapi(root_path))
self.add_route(self.openapi_url, openapi, include_in_schema=False)
- openapi_url = self.openapi_prefix + self.openapi_url
if self.openapi_url and self.docs_url:
async def swagger_ui_html(req: Request) -> HTMLResponse:
+ root_path = req.scope.get("root_path", "").rstrip("/")
+ openapi_url = root_path + self.openapi_url
+ oauth2_redirect_url = self.swagger_ui_oauth2_redirect_url
+ if oauth2_redirect_url:
+ oauth2_redirect_url = root_path + oauth2_redirect_url
return get_swagger_ui_html(
openapi_url=openapi_url,
title=self.title + " - Swagger UI",
- oauth2_redirect_url=self.swagger_ui_oauth2_redirect_url,
+ oauth2_redirect_url=oauth2_redirect_url,
init_oauth=self.swagger_ui_init_oauth,
)
@@ -129,6 +144,8 @@ class FastAPI(Starlette):
if self.openapi_url and self.redoc_url:
async def redoc_html(req: Request) -> HTMLResponse:
+ root_path = req.scope.get("root_path", "").rstrip("/")
+ openapi_url = root_path + self.openapi_url
return get_redoc_html(
openapi_url=openapi_url, title=self.title + " - ReDoc"
)
@@ -140,6 +157,8 @@ class FastAPI(Starlette):
)
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
+ if self.root_path:
+ scope["root_path"] = self.root_path
if AsyncExitStack:
async with AsyncExitStack() as stack:
scope["fastapi_astack"] = stack
diff --git a/tests/test_deprecated_openapi_prefix.py b/tests/test_deprecated_openapi_prefix.py
new file mode 100644
index 000000000..df7e69bd5
--- /dev/null
+++ b/tests/test_deprecated_openapi_prefix.py
@@ -0,0 +1,43 @@
+from fastapi import FastAPI, Request
+from fastapi.testclient import TestClient
+
+app = FastAPI(openapi_prefix="/api/v1")
+
+
+@app.get("/app")
+def read_main(request: Request):
+ return {"message": "Hello World", "root_path": request.scope.get("root_path")}
+
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/api/v1/app": {
+ "get": {
+ "summary": "Read Main",
+ "operationId": "read_main_app_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ }
+ },
+}
+
+
+def test_openapi():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200
+ assert response.json() == openapi_schema
+
+
+def test_main():
+ response = client.get("/app")
+ assert response.status_code == 200
+ assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
diff --git a/tests/test_tutorial/test_behind_a_proxy/__init__.py b/tests/test_tutorial/test_behind_a_proxy/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py
new file mode 100644
index 000000000..8b3b526ed
--- /dev/null
+++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py
@@ -0,0 +1,36 @@
+from fastapi.testclient import TestClient
+
+from behind_a_proxy.tutorial001 import app
+
+client = TestClient(app, root_path="/api/v1")
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/api/v1/app": {
+ "get": {
+ "summary": "Read Main",
+ "operationId": "read_main_app_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ }
+ },
+}
+
+
+def test_openapi():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200
+ assert response.json() == openapi_schema
+
+
+def test_main():
+ response = client.get("/app")
+ assert response.status_code == 200
+ assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}
diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py
new file mode 100644
index 000000000..0a889c469
--- /dev/null
+++ b/tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py
@@ -0,0 +1,36 @@
+from fastapi.testclient import TestClient
+
+from behind_a_proxy.tutorial002 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/api/v1/app": {
+ "get": {
+ "summary": "Read Main",
+ "operationId": "read_main_app_get",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {"application/json": {"schema": {}}},
+ }
+ },
+ }
+ }
+ },
+}
+
+
+def test_openapi():
+ response = client.get("/openapi.json")
+ assert response.status_code == 200
+ assert response.json() == openapi_schema
+
+
+def test_main():
+ response = client.get("/app")
+ assert response.status_code == 200
+ assert response.json() == {"message": "Hello World", "root_path": "/api/v1"}