!!! 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
- }
}
}
```
<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
* `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
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!}
```
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!}
```
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!}
```
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.
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`.
### `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!}
--- /dev/null
+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")}
--- /dev/null
+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")}
return [{"name": "Foo"}]
-def custom_openapi(openapi_prefix: str):
+def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_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"
app = FastAPI()
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@app.get("/items/")
app = FastAPI()
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
return "fakehashed" + password
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
class User(BaseModel):
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
-oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/token")
+oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
app = FastAPI()
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."},
)
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
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
"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
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,
openapi_version=self.openapi_version,
description=self.description,
routes=self.routes,
- openapi_prefix=openapi_prefix,
tags=self.openapi_tags,
servers=self.servers,
)
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:
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:
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
reusable_oauth2 = OAuth2(
flows={
"password": {
- "tokenUrl": "/token",
+ "tokenUrl": "token",
"scopes": {"read:user": "Read a User", "write:user": "Create a user"},
}
}
"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",
}
}
},
+ "servers": [{"url": "/api/v1"}],
}
reusable_oauth2 = OAuth2(
flows={
"password": {
- "tokenUrl": "/token",
+ "tokenUrl": "token",
"scopes": {"read:users": "Read the users", "write:users": "Create users"},
}
}
"read:users": "Read the users",
"write:users": "Create users",
},
- "tokenUrl": "/token",
+ "tokenUrl": "token",
}
},
}
app = FastAPI()
oauth2_scheme = OAuth2AuthorizationCodeBearer(
- authorizationUrl="/authorize", tokenUrl="/token", auto_error=True
+ authorizationUrl="authorize", tokenUrl="token", auto_error=True
)
"type": "oauth2",
"flows": {
"authorizationCode": {
- "authorizationUrl": "/authorize",
- "tokenUrl": "/token",
+ "authorizationUrl": "authorize",
+ "tokenUrl": "token",
"scopes": {},
}
},
reusable_oauth2 = OAuth2(
flows={
"password": {
- "tokenUrl": "/token",
+ "tokenUrl": "token",
"scopes": {"read:users": "Read the users", "write:users": "Create users"},
}
},
"read:users": "Read the users",
"write:users": "Create users",
},
- "tokenUrl": "/token",
+ "tokenUrl": "token",
}
},
}
"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",
}
}
},
+ "servers": [{"url": "/api/v1"}],
}
"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",
}
}
},
+ "servers": [{"url": "/api/v1"}],
}
--- /dev/null
+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"}
--- /dev/null
+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"}
"securitySchemes": {
"OAuth2PasswordBearer": {
"type": "oauth2",
- "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}},
+ "flows": {"password": {"scopes": {}, "tokenUrl": "token"}},
}
}
},
"securitySchemes": {
"OAuth2PasswordBearer": {
"type": "oauth2",
- "flows": {"password": {"scopes": {}, "tokenUrl": "/token"}},
+ "flows": {"password": {"scopes": {}, "tokenUrl": "token"}},
}
},
},
"me": "Read information about the current user.",
"items": "Read items.",
},
- "tokenUrl": "/token",
+ "tokenUrl": "token",
}
},
}
"openapi": "3.0.2",
"info": {"title": "FastAPI", "version": "0.1.0"},
"paths": {
- "/subapi/sub": {
+ "/sub": {
"get": {
"responses": {
"200": {
}
}
},
+ "servers": [{"url": "/subapi"}],
}