在代理之后¶
在许多情况下,您会在 FastAPI 应用前使用像 Traefik 或 Nginx 这样的代理。
这些代理可以处理 HTTPS 证书和其他事务。
代理转发的请求头¶
在您的应用前面的代理通常会在将请求发送到您的服务器之前动态设置一些请求头,以让服务器知道该请求是由代理转发的,让它知道原始的(公共)URL,包括域名、它正在使用 HTTPS 等信息。
服务器程序(例如通过 FastAPI CLI 运行的 Uvicorn)能够解释这些请求头,然后将这些信息传递给您的应用。
但出于安全考虑,由于服务器不知道它位于受信任的代理之后,它不会解释这些请求头。
启用代理转发的请求头¶
您可以使用命令行选项 --forwarded-allow-ips 启动 FastAPI CLI,并传递应信任读取这些转发请求头的 IP 地址。
如果您将其设置为 --forwarded-allow-ips="*",它将信任所有传入的 IP。
如果您的服务器位于受信任的代理之后,并且只有代理与其通信,这将使其接受该代理的任何 IP。
$ fastapi run --forwarded-allow-ips="*"
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
使用 HTTPS 进行重定向¶
例如,假设您定义了一个路径操作 /items/
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
def read_items():
return ["plumbus", "portal gun"]
如果客户端尝试访问 /items,默认情况下,它将被重定向到 /items/。
但在设置命令行选项 --forwarded-allow-ips 之前,它可能会重定向到 https://:8000/items/。
但也许您的应用托管在 https://mysuperapp.com,重定向应该指向 https://mysuperapp.com/items/。
通过设置 --proxy-headers,FastAPI 现在将能够重定向到正确的位置。😎
https://mysuperapp.com/items/
提示
如果您想了解有关 HTTPS 的更多信息,请查看指南关于 HTTPS。
代理转发的请求头如何工作¶
以下是代理在客户端和应用服务器之间添加转发请求头的直观表示:
sequenceDiagram
participant Client
participant Proxy as Proxy/Load Balancer
participant Server as FastAPI Server
Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items
Note over Proxy: Proxy adds forwarded headers
Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items
Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set)
Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs
Proxy->>Client: HTTPS Response
代理拦截原始客户端请求,并在将请求传递给应用服务器之前添加特殊的转发请求头(X-Forwarded-*)。
这些请求头保留了否则会丢失的原始请求信息:
- X-Forwarded-For:原始客户端的 IP 地址
- X-Forwarded-Proto:原始协议(
https) - X-Forwarded-Host:原始主机名(
mysuperapp.com)
当 FastAPI CLI 配置了 --forwarded-allow-ips 时,它会信任这些请求头并使用它们,例如在重定向中生成正确的 URL。
剥离路径前缀的代理¶
您可能有一个为您的应用添加路径前缀的代理。
在这些情况下,您可以使用 root_path 来配置您的应用。
root_path 是由 ASGI 规范(FastAPI 通过 Starlette 构建于其上)提供的一种机制。
root_path 用于处理这些特定情况。
它也在挂载子应用时在内部使用。
在这种情况下,有一个剥离路径前缀的代理意味着您可以在代码中声明一个路径 /app,但之后,您在上面添加了一个层(代理),它会将您的 FastAPI 应用置于像 /api/v1 这样的路径下。
在这种情况下,原始路径 /app 实际上将在 /api/v1/app 提供服务。
即使您所有的代码都假设只有 /app。
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")}
代理会在将请求传输到应用服务器(可能是通过 FastAPI CLI 运行的 Uvicorn)之前动态“剥离”路径前缀,让您的应用确信它是在 /app 路径下提供服务,这样您就不必更新所有代码来包含前缀 /api/v1。
到目前为止,一切都会正常工作。
但是,当您打开集成的文档 UI(前端)时,它会期望在 /openapi.json 获取 OpenAPI 模式,而不是 /api/v1/openapi.json。
因此,前端(在浏览器中运行)会尝试访问 /openapi.json,但无法获取 OpenAPI 模式。
因为我们的应用有一个路径前缀为 /api/v1 的代理,前端需要从 /api/v1/openapi.json 获取 OpenAPI 模式。
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
提示
IP 0.0.0.0 通常用于表示程序在该机器/服务器上所有可用的 IP 地址上监听。
文档 UI 还需要 OpenAPI 模式声明此 API 的 server 位于 /api/v1(在代理之后)。例如:
{
"openapi": "3.1.0",
// More stuff here
"servers": [
{
"url": "/api/v1"
}
],
"paths": {
// More stuff here
}
}
在此示例中,“代理”可以是像 Traefik 这样的东西。而服务器则是像带有 Uvicorn 的 FastAPI CLI,运行您的 FastAPI 应用。
提供 root_path¶
为了实现这一点,您可以使用命令行选项 --root-path,例如:
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
如果您使用 Hypercorn,它也有 --root-path 选项。
技术细节
ASGI 规范为此用例定义了一个 root_path。
而 --root-path 命令行选项提供了那个 root_path。
检查当前的 root_path¶
您可以获取您的应用为每个请求使用的当前 root_path,它是 scope 字典的一部分(这是 ASGI 规范的一部分)。
这里我们将其包含在消息中只是为了演示。
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")}
然后,如果您使用以下命令启动 Uvicorn:
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
响应会是这样的:
{
"message": "Hello World",
"root_path": "/api/v1"
}
在 FastAPI 应用中设置 root_path¶
或者,如果您没有办法提供像 --root-path 或等效的命令行选项,您可以在创建 FastAPI 应用时设置 root_path 参数:
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")}
将 root_path 传递给 FastAPI 相当于将 --root-path 命令行选项传递给 Uvicorn 或 Hypercorn。
关于 root_path¶
请记住,服务器(Uvicorn)不会将该 root_path 用于除传递给应用之外的任何其他事情。
但如果您用浏览器访问 http://127.0.0.1:8000/app,您会看到正常的响应:
{
"message": "Hello World",
"root_path": "/api/v1"
}
所以,它不会期望在 http://127.0.0.1:8000/api/v1/app 被访问。
Uvicorn 会期望代理在 http://127.0.0.1:8000/app 访问 Uvicorn,然后由代理负责在上面添加额外的 /api/v1 前缀。
关于剥离路径前缀的代理¶
请记住,具有剥离路径前缀的代理只是配置它的方式之一。
在许多情况下,默认情况可能是代理没有剥离路径前缀。
在这种情况下(没有剥离路径前缀),代理会监听像 https://myawesomeapp.com 这样的地址,然后如果浏览器访问 https://myawesomeapp.com/api/v1/app,而您的服务器(例如 Uvicorn)在 http://127.0.0.1:8000 监听,那么代理(没有剥离路径前缀)会以相同的路径访问 Uvicorn:http://127.0.0.1:8000/api/v1/app。
使用 Traefik 在本地测试¶
您可以使用 Traefik 轻松地在本地运行带剥离路径前缀的实验。
下载 Traefik,它是一个单一的二进制文件,您可以解压压缩文件并直接从终端运行它。
然后创建一个名为 traefik.toml 的文件,内容如下:
[entryPoints]
[entryPoints.http]
address = ":9999"
[providers]
[providers.file]
filename = "routes.toml"
这告诉 Traefik 监听端口 9999 并使用另一个文件 routes.toml。
提示
我们使用端口 9999 而不是标准的 HTTP 端口 80,这样您就不必使用管理员(sudo)权限运行它。
现在创建另一个文件 routes.toml
[http]
[http.middlewares]
[http.middlewares.api-stripprefix.stripPrefix]
prefixes = ["/api/v1"]
[http.routers]
[http.routers.app-http]
entryPoints = ["http"]
service = "app"
rule = "PathPrefix(`/api/v1`)"
middlewares = ["api-stripprefix"]
[http.services]
[http.services.app]
[http.services.app.loadBalancer]
[[http.services.app.loadBalancer.servers]]
url = "http://127.0.0.1:8000"
这个文件配置 Traefik 使用路径前缀 /api/v1。
然后 Traefik 会将其请求重定向到您在 http://127.0.0.1:8000 上运行的 Uvicorn。
现在启动 Traefik
$ ./traefik --configFile=traefik.toml
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
现在启动您的应用,使用 --root-path 选项:
$ fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
<span style="color: green;">INFO</span>: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
检查响应¶
现在,如果您访问 Uvicorn 的 URL 和端口:http://127.0.0.1:8000/app,您将看到正常的响应:
{
"message": "Hello World",
"root_path": "/api/v1"
}
提示
请注意,即使您在 http://127.0.0.1:8000/app 访问它,它仍然显示了来自 --root-path 选项的 root_path 为 /api/v1。
现在打开 Traefik 的 URL 和端口,包括路径前缀:http://127.0.0.1:9999/api/v1/app。
我们得到了相同的响应:
{
"message": "Hello World",
"root_path": "/api/v1"
}
但这次是在代理提供的带前缀路径的 URL 上:/api/v1。
当然,这里的想法是每个人都应该通过代理访问应用,所以带有路径前缀 /api/v1 的版本是“正确的”。
而由 Uvicorn 直接提供的没有路径前缀的版本(http://127.0.0.1:8000/app)将专供代理(Traefik)访问。
这演示了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用来自 --root-path 选项的 root_path。
检查文档 UI¶
但有趣的部分来了。✨
访问应用的“官方”方式是通过我们定义的带路径前缀的代理。所以,正如我们所料,如果您尝试直接访问由 Uvicorn 提供的文档 UI,而 URL 中没有路径前缀,它将无法工作,因为它期望通过代理被访问。
您可以在 http://127.0.0.1:8000/docs 进行检查。

但是如果我们通过使用端口 9999 的代理,在“官方”URL /api/v1/docs 访问文档 UI,它就能正常工作!🎉
您可以在 http://127.0.0.1:9999/api/v1/docs 进行检查。

正如我们所愿。✔️
这是因为 FastAPI 使用这个 root_path 在 OpenAPI 中创建默认的 server,其 URL 由 root_path 提供。
附加服务器¶
警告
这是一个更高级的用例。您可以随意跳过。
默认情况下,FastAPI 会在 OpenAPI 模式中创建一个 server,其 URL 为 root_path。
但您也可以提供其他备选的 servers,例如,如果您希望同一个文档 UI 与暂存环境和生产环境都进行交互。
如果您传递了一个自定义的 servers 列表,并且存在一个 root_path(因为您的 API 位于代理之后),FastAPI 会将一个带有此 root_path 的“服务器”插入到列表的开头。
例如
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")}
将生成一个像下面这样的 OpenAPI 模式:
{
"openapi": "3.1.0",
// 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
}
}
提示
请注意自动生成的服务器,其 url 值为 /api/v1,取自 root_path。
在 http://127.0.0.1:9999/api/v1/docs 的文档 UI 中,它会看起来像这样:

提示
文档 UI 将与您选择的服务器进行交互。
禁用来自 root_path 的自动服务器¶
如果您不希望 FastAPI 使用 root_path 包含一个自动服务器,您可以使用参数 root_path_in_servers=False:
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")}
然后它将不会将其包含在 OpenAPI 模式中。
挂载子应用¶
如果您需要挂载一个子应用(如 子应用 - 挂载 中所述),同时使用带 root_path 的代理,您可以像预期那样正常进行。
FastAPI 会在内部巧妙地使用 root_path,所以它会正常工作。✨