]> git.ipfire.org Git - thirdparty/fastapi/fastapi.git/commitdiff
✨ Auto-generate OpenAPI servers from root_path (#1596)
authorRupsi Kaushik <rkaus053@uottawa.ca>
Fri, 10 Jul 2020 17:28:18 +0000 (13:28 -0400)
committerGitHub <noreply@github.com>
Fri, 10 Jul 2020 17:28:18 +0000 (19:28 +0200)
* root_path included in servers object instead of path prefix

* ♻️ Refactor implementation of auto-including root_path in OpenAPI servers

* 📝 Update docs and examples for Behind a Proxy, including servers

* 📝 Update Extending OpenAPI as openapi_prefix is no longer needed

* ✅ Add extra tests for root_path in servers and root_path_in_servers=False

* 🍱 Update security docs images with relative token URL

* 📝 Update security docs with relative token URL

* 📝 Update example sources with relative token URLs

* ✅ Update tests with relative tokens

Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
35 files changed:
docs/en/docs/advanced/behind-a-proxy.md
docs/en/docs/advanced/extending-openapi.md
docs/en/docs/img/tutorial/behind-a-proxy/image01.png
docs/en/docs/img/tutorial/behind-a-proxy/image02.png
docs/en/docs/img/tutorial/behind-a-proxy/image03.png [new file with mode: 0644]
docs/en/docs/img/tutorial/security/image02.png
docs/en/docs/img/tutorial/security/image04.png
docs/en/docs/img/tutorial/security/image05.png
docs/en/docs/img/tutorial/security/image08.png
docs/en/docs/img/tutorial/security/image11.png
docs/en/docs/tutorial/security/first-steps.md
docs/en/docs/tutorial/security/simple-oauth2.md
docs_src/behind_a_proxy/tutorial003.py [new file with mode: 0644]
docs_src/behind_a_proxy/tutorial004.py [new file with mode: 0644]
docs_src/extending_openapi/tutorial001.py
docs_src/security/tutorial001.py
docs_src/security/tutorial002.py
docs_src/security/tutorial003.py
docs_src/security/tutorial004.py
docs_src/security/tutorial005.py
fastapi/applications.py
fastapi/openapi/utils.py
pending_tests/main.py
tests/test_deprecated_openapi_prefix.py
tests/test_security_oauth2.py
tests/test_security_oauth2_authorization_code_bearer.py
tests/test_security_oauth2_optional.py
tests/test_tutorial/test_behind_a_proxy/test_tutorial001.py
tests/test_tutorial/test_behind_a_proxy/test_tutorial002.py
tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py [new file with mode: 0644]
tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py [new file with mode: 0644]
tests/test_tutorial/test_security/test_tutorial001.py
tests/test_tutorial/test_security/test_tutorial003.py
tests/test_tutorial/test_security/test_tutorial005.py
tests/test_tutorial/test_sub_applications/test_tutorial001.py

index 660e374a430198925d9f1b17b09ec2253a7a035e..8f0cecf4af94dca951ca13d194ef07ab30ad0f5e 100644 (file)
@@ -42,16 +42,19 @@ 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:
+The docs UI would also need the OpenAPI schema to declare that this API `server` is located at `/api/v1` (behind the proxy). For example:
 
-```JSON hl_lines="5"
+```JSON hl_lines="4 5 6 7 8"
 {
     "openapi": "3.0.2",
     // More stuff here
+    "servers": [
+        {
+            "url": "/api/v1"
+        }
+    ],
     "paths": {
-        "/api/v1/app": {
             // More stuff here
-        }
     }
 }
 ```
@@ -264,15 +267,77 @@ You can check it at <a href="http://127.0.0.1:8000/docs" class="external-link" t
 
 <img src="/img/tutorial/behind-a-proxy/image01.png">
 
-But if we access the docs UI at the "official" URL using the proxy, at `/api/v1/docs`, it works correctly! 🎉
+But if we access the docs UI at the "official" URL using the proxy with port `9999`, at `/api/v1/docs`, it works correctly! 🎉
+
+You can check it at <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>:
+
+<img src="/img/tutorial/behind-a-proxy/image02.png">
 
 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`.
+This is because FastAPI uses this `root_path` to create the default `server` in OpenAPI with the URL provided by `root_path`.
 
-You can check it at <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a>:
+## Additional servers
 
-<img src="/img/tutorial/behind-a-proxy/image02.png">
+!!! warning
+    This is a more advanced use case. Feel free to skip it.
+
+By default, **FastAPI** will create a `server` in the OpenAPI schema with the URL for the `root_path`.
+
+But you can also provide other alternative `servers`, for example if you want *the same* docs UI to interact with a staging and production environments.
+
+If you pass a custom list of `servers` and there's a `root_path` (because your API lives behind a proxy), **FastAPI** will insert a "server" with this `root_path` at the beginning of the list.
+
+For example:
+
+```Python hl_lines="4 5 6 7"
+{!../../../docs_src/behind_a_proxy/tutorial003.py!}
+```
+
+Will generate an OpenAPI schema like:
+
+```JSON hl_lines="5 6 7"
+{
+    "openapi": "3.0.2",
+    // More stuff here
+    "servers": [
+        {
+            "url": "/api/v1"
+        },
+        {
+            "url": "https://stag.example.com",
+            "description": "Staging environment"
+        },
+        {
+            "url": "https://prod.example.com",
+            "description": "Production environment"
+        }
+    ],
+    "paths": {
+            // More stuff here
+    }
+}
+```
+
+!!! tip
+    Notice the auto-generated server with a `url` value of `/api/v1`, taken from the `root_path`.
+
+In the docs UI at <a href="http://127.0.0.1:9999/api/v1/docs" class="external-link" target="_blank">http://127.0.0.1:9999/api/v1/docs</a> it would look like:
+
+<img src="/img/tutorial/behind-a-proxy/image03.png">
+
+!!! tip
+    The docs UI will interact with the server that you select.
+
+### Disable automatic server from `root_path`
+
+If you don't want **FastAPI** to include an automatic server using the `root_path`, you can use the parameter `root_path_in_servers=False`:
+
+```Python hl_lines="9"
+{!../../../docs_src/behind_a_proxy/tutorial004.py!}
+```
+
+and then it won't include it in the OpenAPI schema.
 
 ## Mounting a sub-application
 
index 30cd857d5f80d6bc3e40794868f1a1499fef94d0..4985aebde9391e2b0c8fe1dfdc6b1c7b02116b10 100644 (file)
@@ -32,7 +32,6 @@ And that function `get_openapi()` receives as parameters:
 * `openapi_version`: The version of the OpenAPI specification used. By default, the latest: `3.0.2`.
 * `description`: The description of your API.
 * `routes`: A list of routes, these are each of the registered *path operations*. They are taken from `app.routes`.
-* `openapi_prefix`: The URL prefix to be used in your OpenAPI.
 
 ## Overriding the defaults
 
@@ -52,22 +51,15 @@ 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 21"
+```Python hl_lines="2  15 16 17 18 19 20"
 {!../../../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="22 23 24"
+```Python hl_lines="21 22 23"
 {!../../../docs_src/extending_openapi/tutorial001.py!}
 ```
 
@@ -79,7 +71,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  25 26"
+```Python hl_lines="13 14  24 25"
 {!../../../docs_src/extending_openapi/tutorial001.py!}
 ```
 
@@ -87,7 +79,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="29"
+```Python hl_lines="28"
 {!../../../docs_src/extending_openapi/tutorial001.py!}
 ```
 
index 4ceae4421988a968a2903e55aacd66fc82741742..8012031401c8365f9ff01c92479e05854bc8bf00 100644 (file)
Binary files a/docs/en/docs/img/tutorial/behind-a-proxy/image01.png and b/docs/en/docs/img/tutorial/behind-a-proxy/image01.png differ
index 8012031401c8365f9ff01c92479e05854bc8bf00..95c207fcf86b8b9f6a7c09027e8bb948c28af02d 100644 (file)
Binary files a/docs/en/docs/img/tutorial/behind-a-proxy/image02.png and b/docs/en/docs/img/tutorial/behind-a-proxy/image02.png differ
diff --git a/docs/en/docs/img/tutorial/behind-a-proxy/image03.png b/docs/en/docs/img/tutorial/behind-a-proxy/image03.png
new file mode 100644 (file)
index 0000000..278bd07
Binary files /dev/null and b/docs/en/docs/img/tutorial/behind-a-proxy/image03.png differ
index f434335a6be47cd596f89d996a63509ac7fdb520..a437ac0e710f52f78018d6f01097dd52d7dfe7e1 100644 (file)
Binary files a/docs/en/docs/img/tutorial/security/image02.png and b/docs/en/docs/img/tutorial/security/image02.png differ
index d56ed53e4c1215c831a025cd5bded4408dd09b1b..231c53d2ef1e3b1468c511afbf0b8ed7c89f54b4 100644 (file)
Binary files a/docs/en/docs/img/tutorial/security/image04.png and b/docs/en/docs/img/tutorial/security/image04.png differ
index af2023a1835e71a1f62fba7c569c42ce95eb48d1..cd4525f5717f6032b722a80d774a629db141c020 100644 (file)
Binary files a/docs/en/docs/img/tutorial/security/image05.png and b/docs/en/docs/img/tutorial/security/image05.png differ
index 64cc76293cd43bc65cd043e14b4fba6443e9d392..5289afce28862c94e758455e3008ce8ca13ff941 100644 (file)
Binary files a/docs/en/docs/img/tutorial/security/image08.png and b/docs/en/docs/img/tutorial/security/image08.png differ
index 3e1a0ce9195bce6a9471e536dc71d5ce6f2581de..278f049400fa4e62e6d7bd1bfc6ac7f6727d13e5 100644 (file)
Binary files a/docs/en/docs/img/tutorial/security/image11.png and b/docs/en/docs/img/tutorial/security/image11.png differ
index 287e92c23ef39081e69a3a53b7d75d84adc01125..81226dffae6ce3239b90b53d2292afce24561f67 100644 (file)
@@ -86,8 +86,8 @@ But in this case, the same **FastAPI** application will handle the API and the a
 So, let's review it from that simplified point of view:
 
 * The user types his `username` and `password` in the frontend, and hits `Enter`.
-* The frontend (running in the user's browser) sends that `username` and `password` to a specific URL in our API.
-* The API checks that `username` and `password`, and responds with a "token".
+* The frontend (running in the user's browser) sends that `username` and `password` to a specific URL in our API (declared with `tokenUrl="token"`).
+* The API checks that `username` and `password`, and responds with a "token" (we haven't implemented any of this yet).
     * A "token" is just a string with some content that we can use later to verify this user.
     * Normally, a token is set to expire after some time.
         * So, the user will have to login again at some point later.
@@ -114,13 +114,20 @@ In this example we are going to use **OAuth2**, with the **Password** flow, usin
 
     In that case, **FastAPI** also provides you with the tools to build it.
 
-`OAuth2PasswordBearer` is a class that we create passing a parameter of the URL in where the client (the frontend running in the user's browser) can use to send the `username` and `password` and get a token.
+`OAuth2PasswordBearer` is a class that we create passing a parameter with the URL the client (the frontend running in the user's browser) can use to send the `username` and `password` and get a token.
 
 ```Python hl_lines="6"
 {!../../../docs_src/security/tutorial001.py!}
 ```
 
-It doesn't create that endpoint / *path operation*, but declares that that URL is the one that the client should use to get the token. That information is used in OpenAPI, and then in the interactive API documentation systems.
+!!! tip
+    here `tokenUrl="token"` refers to a relative URL `token` that we haven't created yet. As it's a relative URL, it's equivalent to `./token`.
+
+    Because we are using a relative URL, if your API was located at `https://example.com/`, then it would refer to `https://example.com/token`. But if your API was located at `https://example.com/api/v1/`, then it would refer to `https://example.com/api/v1/token`.
+
+    Using a relative URL is important to make sure your application keeps working even in an advanced use case like [Behind a Proxy](../../advanced/behind-a-proxy.md){.internal-link target=_blank}.
+
+It doesn't create that endpoint / *path operation* for `./token`, but declares that that URL `./token` is the one that the client should use to get the token. That information is used in OpenAPI, and then in the interactive API documentation systems.
 
 !!! info
     If you are a very strict "Pythonista" you might dislike the style of the parameter name `tokenUrl` instead of `token_url`.
index 2f58a6d109395cf5182ea97f2cf0e66d8d445288..d3343c3e88ba09100918b2f6a1dba06fb615bd71 100644 (file)
@@ -47,7 +47,7 @@ Now let's use the utilities provided by **FastAPI** to handle this.
 
 ### `OAuth2PasswordRequestForm`
 
-First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` for the path `/token`:
+First, import `OAuth2PasswordRequestForm`, and use it as a dependency with `Depends` in the *path operation* for `/token`:
 
 ```Python hl_lines="4  76"
 {!../../../docs_src/security/tutorial003.py!}
diff --git a/docs_src/behind_a_proxy/tutorial003.py b/docs_src/behind_a_proxy/tutorial003.py
new file mode 100644 (file)
index 0000000..3b7d8be
--- /dev/null
@@ -0,0 +1,14 @@
+from fastapi import FastAPI, Request
+
+app = FastAPI(
+    servers=[
+        {"url": "https://stag.example.com", "description": "Staging environment"},
+        {"url": "https://prod.example.com", "description": "Production environment"},
+    ],
+    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/behind_a_proxy/tutorial004.py b/docs_src/behind_a_proxy/tutorial004.py
new file mode 100644 (file)
index 0000000..51bd5ba
--- /dev/null
@@ -0,0 +1,15 @@
+from fastapi import FastAPI, Request
+
+app = FastAPI(
+    servers=[
+        {"url": "https://stag.example.com", "description": "Staging environment"},
+        {"url": "https://prod.example.com", "description": "Production environment"},
+    ],
+    root_path="/api/v1",
+    root_path_in_servers=False,
+)
+
+
+@app.get("/app")
+def read_main(request: Request):
+    return {"message": "Hello World", "root_path": request.scope.get("root_path")}
index d9d7e9844bec95c10027680241d4c25f145ea4f0..561e95898f5f46521516a77a1f60fcf0f87a7515 100644 (file)
@@ -9,7 +9,7 @@ async def read_items():
     return [{"name": "Foo"}]
 
 
-def custom_openapi(openapi_prefix: str):
+def custom_openapi():
     if app.openapi_schema:
         return app.openapi_schema
     openapi_schema = get_openapi(
@@ -17,7 +17,6 @@ def custom_openapi(openapi_prefix: str):
         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"
index bd8ce4d1018c4563abbf3143848afdb61ea3371c..224e59602e227e5fadfaac1de252db40137f17ec 100644 (file)
@@ -3,7 +3,7 @@ from fastapi.security import OAuth2PasswordBearer
 
 app = FastAPI()
 
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 
 @app.get("/items/")
index 2fccec7ed205e4230686aa7dc701f1dad9bb5cd9..03e0cd5fcee35c644d32a07989735a9161fbd00d 100644 (file)
@@ -6,7 +6,7 @@ from pydantic import BaseModel
 
 app = FastAPI()
 
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 
 class User(BaseModel):
index 0fd15ae3c76b7751df93ecd8c9e8f6ea6159be79..a6bb176e4f3dc07b34c190808c3a178ccfc0e52e 100644 (file)
@@ -28,7 +28,7 @@ def fake_hash_password(password: str):
     return "fakehashed" + password
 
 
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 
 class User(BaseModel):
index f3cb6a905b554923fb3c061863f0186a3b899ce7..54dbe46d7e8b382d89eab9da5544470ff4602eb7 100644 (file)
@@ -48,7 +48,7 @@ class UserInDB(User):
 
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
 
 app = FastAPI()
 
index 05c29deb19699daddbc8ab7a313eb922f26e1cb8..d66acb03ccd26f310389add030fdec212a5c52c1 100644 (file)
@@ -61,7 +61,7 @@ class UserInDB(User):
 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
 
 oauth2_scheme = OAuth2PasswordBearer(
-    tokenUrl="/token",
+    tokenUrl="token",
     scopes={"me": "Read information about the current user.", "items": "Read items."},
 )
 
index 08dba9b59958d044520dbc97137760a65da63be9..d5b70e116f6e4aa86c0851750a081a0e44cb6cc6 100644 (file)
@@ -50,6 +50,7 @@ class FastAPI(Starlette):
         on_shutdown: Sequence[Callable] = None,
         openapi_prefix: str = "",
         root_path: str = "",
+        root_path_in_servers: bool = True,
         **extra: Dict[str, Any],
     ) -> None:
         self.default_response_class = default_response_class
@@ -71,7 +72,7 @@ class FastAPI(Starlette):
         self.title = title
         self.description = description
         self.version = version
-        self.servers = servers
+        self.servers = servers or []
         self.openapi_url = openapi_url
         self.openapi_tags = openapi_tags
         # TODO: remove when discarding the openapi_prefix parameter
@@ -83,6 +84,7 @@ class FastAPI(Starlette):
                 "https://fastapi.tiangolo.com/advanced/sub-applications/"
             )
         self.root_path = root_path or openapi_prefix
+        self.root_path_in_servers = root_path_in_servers
         self.docs_url = docs_url
         self.redoc_url = redoc_url
         self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
@@ -98,7 +100,7 @@ class FastAPI(Starlette):
         self.openapi_schema: Optional[Dict[str, Any]] = None
         self.setup()
 
-    def openapi(self, openapi_prefix: str = "") -> Dict:
+    def openapi(self) -> Dict:
         if not self.openapi_schema:
             self.openapi_schema = get_openapi(
                 title=self.title,
@@ -106,7 +108,6 @@ class FastAPI(Starlette):
                 openapi_version=self.openapi_version,
                 description=self.description,
                 routes=self.routes,
-                openapi_prefix=openapi_prefix,
                 tags=self.openapi_tags,
                 servers=self.servers,
             )
@@ -114,10 +115,19 @@ class FastAPI(Starlette):
 
     def setup(self) -> None:
         if self.openapi_url:
+            server_urls = set()
+            for server_data in self.servers:
+                url = server_data.get("url")
+                if url:
+                    server_urls.add(url)
 
             async def openapi(req: Request) -> JSONResponse:
                 root_path = req.scope.get("root_path", "").rstrip("/")
-                return JSONResponse(self.openapi(root_path))
+                if root_path not in server_urls:
+                    if root_path and self.root_path_in_servers:
+                        self.servers.insert(0, {"url": root_path})
+                        server_urls.add(root_path)
+                return JSONResponse(self.openapi())
 
             self.add_route(self.openapi_url, openapi, include_in_schema=False)
         if self.openapi_url and self.docs_url:
index ad1a9d83bb87f91154e1eceb6e9b715b3de0d672..1cf79d71e33b043bfcf8d3ada6aea91139c7caf8 100644 (file)
@@ -331,7 +331,6 @@ def get_openapi(
     openapi_version: str = "3.0.2",
     description: str = None,
     routes: Sequence[BaseRoute],
-    openapi_prefix: str = "",
     tags: Optional[List[Dict[str, Any]]] = None,
     servers: Optional[List[Dict[str, Union[str, Any]]]] = None,
 ) -> Dict:
@@ -356,9 +355,7 @@ def get_openapi(
             if result:
                 path, security_schemes, path_definitions = result
                 if path:
-                    paths.setdefault(openapi_prefix + route.path_format, {}).update(
-                        path
-                    )
+                    paths.setdefault(route.path_format, {}).update(path)
                 if security_schemes:
                     components.setdefault("securitySchemes", {}).update(
                         security_schemes
index cb464cb312d2862e63022e5a62adf7698943dfad..5e919f1bcf3dc8421c12c458addecb92f8679f89 100644 (file)
@@ -31,7 +31,7 @@ def get_security(sec=Security(HTTPBasic())):
 reusable_oauth2 = OAuth2(
     flows={
         "password": {
-            "tokenUrl": "/token",
+            "tokenUrl": "token",
             "scopes": {"read:user": "Read a User", "write:user": "Create a user"},
         }
     }
index df7e69bd562375ab12c35c252139af17ace16ff8..a3355256f9cd1a81bff2fbf471331552c3bd597d 100644 (file)
@@ -15,7 +15,7 @@ openapi_schema = {
     "openapi": "3.0.2",
     "info": {"title": "FastAPI", "version": "0.1.0"},
     "paths": {
-        "/api/v1/app": {
+        "/app": {
             "get": {
                 "summary": "Read Main",
                 "operationId": "read_main_app_get",
@@ -28,6 +28,7 @@ openapi_schema = {
             }
         }
     },
+    "servers": [{"url": "/api/v1"}],
 }
 
 
index 6c513039f259aac405df0c2c0c72b73097582a38..a0ae93afa6d5efcf0d7fe783cc9ee794ed5cd282 100644 (file)
@@ -9,7 +9,7 @@ app = FastAPI()
 reusable_oauth2 = OAuth2(
     flows={
         "password": {
-            "tokenUrl": "/token",
+            "tokenUrl": "token",
             "scopes": {"read:users": "Read the users", "write:users": "Create users"},
         }
     }
@@ -144,7 +144,7 @@ openapi_schema = {
                             "read:users": "Read the users",
                             "write:users": "Create users",
                         },
-                        "tokenUrl": "/token",
+                        "tokenUrl": "token",
                     }
                 },
             }
index 3e0155e1fa9f0068f046b055545551e6d4dfb503..ad9a39ded746c8e7f86cc46f338b692e9490125b 100644 (file)
@@ -7,7 +7,7 @@ from fastapi.testclient import TestClient
 app = FastAPI()
 
 oauth2_scheme = OAuth2AuthorizationCodeBearer(
-    authorizationUrl="/authorize", tokenUrl="/token", auto_error=True
+    authorizationUrl="authorize", tokenUrl="token", auto_error=True
 )
 
 
@@ -42,8 +42,8 @@ openapi_schema = {
                 "type": "oauth2",
                 "flows": {
                     "authorizationCode": {
-                        "authorizationUrl": "/authorize",
-                        "tokenUrl": "/token",
+                        "authorizationUrl": "authorize",
+                        "tokenUrl": "token",
                         "scopes": {},
                     }
                 },
index c2c9764b050362960865bd216e72de4582824e0e..e9e6d5d27a9cc68702619b59731783a03c31d52c 100644 (file)
@@ -11,7 +11,7 @@ app = FastAPI()
 reusable_oauth2 = OAuth2(
     flows={
         "password": {
-            "tokenUrl": "/token",
+            "tokenUrl": "token",
             "scopes": {"read:users": "Read the users", "write:users": "Create users"},
         }
     },
@@ -148,7 +148,7 @@ openapi_schema = {
                             "read:users": "Read the users",
                             "write:users": "Create users",
                         },
-                        "tokenUrl": "/token",
+                        "tokenUrl": "token",
                     }
                 },
             }
index a6b7401cf8cc0d9553956f607e412222930a00cd..be9e499bf8f1d13f8f61b773bb8e3f3b7ca22636 100644 (file)
@@ -8,7 +8,7 @@ openapi_schema = {
     "openapi": "3.0.2",
     "info": {"title": "FastAPI", "version": "0.1.0"},
     "paths": {
-        "/api/v1/app": {
+        "/app": {
             "get": {
                 "summary": "Read Main",
                 "operationId": "read_main_app_get",
@@ -21,6 +21,7 @@ openapi_schema = {
             }
         }
     },
+    "servers": [{"url": "/api/v1"}],
 }
 
 
index 26da7dea46b0308c0cd1c73a2263e74698a27a15..ac192e3db769b58f955dc04c8fc3f27b7f92b67a 100644 (file)
@@ -8,7 +8,7 @@ openapi_schema = {
     "openapi": "3.0.2",
     "info": {"title": "FastAPI", "version": "0.1.0"},
     "paths": {
-        "/api/v1/app": {
+        "/app": {
             "get": {
                 "summary": "Read Main",
                 "operationId": "read_main_app_get",
@@ -21,6 +21,7 @@ openapi_schema = {
             }
         }
     },
+    "servers": [{"url": "/api/v1"}],
 }
 
 
diff --git a/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial003.py
new file mode 100644 (file)
index 0000000..2727525
--- /dev/null
@@ -0,0 +1,41 @@
+from fastapi.testclient import TestClient
+
+from docs_src.behind_a_proxy.tutorial003 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "FastAPI", "version": "0.1.0"},
+    "servers": [
+        {"url": "/api/v1"},
+        {"url": "https://stag.example.com", "description": "Staging environment"},
+        {"url": "https://prod.example.com", "description": "Production environment"},
+    ],
+    "paths": {
+        "/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_tutorial004.py b/tests/test_tutorial/test_behind_a_proxy/test_tutorial004.py
new file mode 100644 (file)
index 0000000..4c4e4b7
--- /dev/null
@@ -0,0 +1,40 @@
+from fastapi.testclient import TestClient
+
+from docs_src.behind_a_proxy.tutorial004 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+    "openapi": "3.0.2",
+    "info": {"title": "FastAPI", "version": "0.1.0"},
+    "servers": [
+        {"url": "https://stag.example.com", "description": "Staging environment"},
+        {"url": "https://prod.example.com", "description": "Production environment"},
+    ],
+    "paths": {
+        "/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"}
index 82818b430bc69a6f90b0ec93c6d65fc99afbcbae..8a033c4f26be06e7ed152fe1cf8acb0650f5e7a2 100644 (file)
@@ -26,7 +26,7 @@ openapi_schema = {
         "securitySchemes": {
             "OAuth2PasswordBearer": {
                 "type": "oauth2",
-                "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}},
+                "flows": {"password": {"scopes": {}, "tokenUrl": "token"}},
             }
         }
     },
index bf2a81723b70019530154b41cd44a245e4545427..3fc7f5f40f49f8f18c07b2ff891382ade4b3084e 100644 (file)
@@ -102,7 +102,7 @@ openapi_schema = {
         "securitySchemes": {
             "OAuth2PasswordBearer": {
                 "type": "oauth2",
-                "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}},
+                "flows": {"password": {"scopes": {}, "tokenUrl": "token"}},
             }
         },
     },
index 509b200da2e2979947c045f756d5709ee51841c2..a37f2d60acd5c5c54da55363cb415873cf0e3a4b 100644 (file)
@@ -168,7 +168,7 @@ openapi_schema = {
                             "me": "Read information about the current user.",
                             "items": "Read items.",
                         },
-                        "tokenUrl": "/token",
+                        "tokenUrl": "token",
                     }
                 },
             }
index 4e1dc9e09920388f0bf2f39283db337cf941facb..00e9aec577900538c74846d4e28ec5c12d62f3c7 100644 (file)
@@ -26,7 +26,7 @@ openapi_schema_sub = {
     "openapi": "3.0.2",
     "info": {"title": "FastAPI", "version": "0.1.0"},
     "paths": {
-        "/subapi/sub": {
+        "/sub": {
             "get": {
                 "responses": {
                     "200": {
@@ -39,6 +39,7 @@ openapi_schema_sub = {
             }
         }
     },
+    "servers": [{"url": "/subapi"}],
 }