]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
Add Open API prefix route - correct docs behind reverse proxy (#26)
authorKabir Khan <kakh@microsoft.com>
Thu, 14 Feb 2019 18:57:49 +0000 (10:57 -0800)
committerSebastián Ramírez <tiangolo@gmail.com>
Thu, 14 Feb 2019 18:57:49 +0000 (22:57 +0400)
Add Open API prefix route - correct docs behind reverse proxy.

docs/img/tutorial/sub-applications/image01.png [new file with mode: 0644]
docs/img/tutorial/sub-applications/image02.png [new file with mode: 0644]
docs/src/sub_applications/tutorial001.py [new file with mode: 0644]
docs/tutorial/sub-applications-proxy.md [new file with mode: 0644]
fastapi/applications.py
fastapi/openapi/utils.py
mkdocs.yml
tests/test_tutorial/test_sub_applications/__init__.py [new file with mode: 0644]
tests/test_tutorial/test_sub_applications/test_tutorial001.py [new file with mode: 0644]

diff --git a/docs/img/tutorial/sub-applications/image01.png b/docs/img/tutorial/sub-applications/image01.png
new file mode 100644 (file)
index 0000000..7627144
Binary files /dev/null and b/docs/img/tutorial/sub-applications/image01.png differ
diff --git a/docs/img/tutorial/sub-applications/image02.png b/docs/img/tutorial/sub-applications/image02.png
new file mode 100644 (file)
index 0000000..47abeda
Binary files /dev/null and b/docs/img/tutorial/sub-applications/image02.png differ
diff --git a/docs/src/sub_applications/tutorial001.py b/docs/src/sub_applications/tutorial001.py
new file mode 100644 (file)
index 0000000..3b1f77a
--- /dev/null
@@ -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)
diff --git a/docs/tutorial/sub-applications-proxy.md b/docs/tutorial/sub-applications-proxy.md
new file mode 100644 (file)
index 0000000..daf6742
--- /dev/null
@@ -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).
\ No newline at end of file
index 2d5a0b8627e46feb03e9cbcd9d2bf6ecc26f38c5..ac47b130f910543c6dd39b84f062670b94ad5673 100644 (file)
@@ -25,6 +25,7 @@ class FastAPI(Starlette):
         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],
@@ -43,6 +44,7 @@ class FastAPI(Starlette):
         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
@@ -66,6 +68,7 @@ class FastAPI(Starlette):
                 openapi_version=self.openapi_version,
                 description=self.description,
                 routes=self.routes,
+                openapi_prefix=self.openapi_prefix,
             )
         return self.openapi_schema
 
@@ -80,7 +83,8 @@ class FastAPI(Starlette):
             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,
             )
@@ -88,7 +92,8 @@ class FastAPI(Starlette):
             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,
             )
index d681088e5297ae7e71dfca2b70f1a5d9cd044739..4a603aa857731bc7b85f9d57dae1e8252c331f48 100644 (file)
@@ -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:
@@ -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
index f8a3b31167fcb870f954a2a033ef05f8ec70a40a..d3631b7f79686fa5ef646c0d3c3872c68416819d 100644 (file)
@@ -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'
diff --git a/tests/test_tutorial/test_sub_applications/__init__.py b/tests/test_tutorial/test_sub_applications/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/test_tutorial/test_sub_applications/test_tutorial001.py b/tests/test_tutorial/test_sub_applications/test_tutorial001.py
new file mode 100644 (file)
index 0000000..87b44ab
--- /dev/null
@@ -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"}