置于代理之后¶
在许多情况下,你会在 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 而不是 /api/v1/openapi.json 处获取 OpenAPI 模式。
因此,前端(在浏览器中运行)将尝试访问 /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 之类的东西。而服务器可以是运行你的 FastAPI 应用的、带有 Uvicorn 的 FastAPI CLI。
提供 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 访问它,然后由代理负责在上面添加额外的 /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"
}
但这次是在由代理提供的路径前缀 /api/v1 下的 URL。
当然,这里的构思是每个人都通过代理访问应用,因此带有路径前缀 /api/v1 的版本是“正确”的版本。
而不带路径前缀的版本(http://127.0.0.1:8000/app,由 Uvicorn 直接提供)将专门供代理(Traefik)访问。
这演示了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用 --root-path 选项中的 root_path。
检查文档 UI¶
但有趣的部分来了。✨
访问应用的“官方”方式是通过我们定义的带路径前缀的代理。所以正如我们预期的那样,如果你直接访问 Uvicorn 提供的文档 UI(URL 中不带路径前缀),它将无法工作,因为它期望通过代理被访问。
你可以在 http://127.0.0.1:8000/docs 进行检查。

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

正如我们所愿。✔️
这是因为 FastAPI 使用此 root_path 在 OpenAPI 中创建了默认 server,并带有 root_path 提供的 URL。
附加服务器¶
警告
这是一个更高级的用例。随意跳过它。
默认情况下,FastAPI 将在 OpenAPI 模式中使用 root_path 的 URL 创建一个 server。
但你也可以提供其他替代的 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。
在文档 UI (http://127.0.0.1:9999/api/v1/docs) 中,它看起来像这样:

提示
文档 UI 将与你选择的服务器进行交互。
技术细节
OpenAPI 规范中的 servers 属性是可选的。
如果你没有指定 servers 参数且 root_path 等于 /,则生成的 OpenAPI 模式中的 servers 属性默认会被完全忽略,这等同于一个 url 值为 / 的单一服务器。
禁用基于 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 的代理的情况下挂载子应用(如 子应用 - 挂载 (Sub Applications - Mounts) 中所述),你可以像往常一样正常操作。
FastAPI 将智能地在内部使用 root_path,所以它会直接工作。✨