FastAPI 部署在代理之后:Forwarded 头与 root_path 的完整实战指南
本文以 FastAPI 官方文档《Hinter einem Proxy》(代理之后部署)为主体,系统讲解 FastAPI 应用位于 Traefik、Nginx 等代理(Proxy)之后的两大核心问题:如何让 ASGI 服务器(Uvicorn)信任并解读代理注入的 X-Forwarded-* 头,以及当代理剥离(strip)路径前缀时如何通过 ASGI 标准的 root_path 机制让应用、重定向与内置文档界面(Swagger UI)继续正确工作。读完后,你将能够独立完成 --forwarded-allow-ips 与 --root-path 的配置、编写 Traefik 的本地实验环境,并理解 FastAPI 源码中 root_path 如何参与 OpenAPI 生成。
为什么要在 FastAPI 应用前放置代理
在多数生产场景中,你不会让 FastAPI 应用直接暴露在公网,而是在其前面放置一个代理,例如 Traefik 或 Nginx。这类代理通常负责处理 HTTPS 证书终结、负载均衡、访问控制等事项,你的 FastAPI 应用(经由 Uvicorn 等 ASGI 服务器运行)则只在内网监听。
这种架构带来两个必须由应用侧配合处理的问题:
- 代理到应用之间的连接往往是明文 HTTP,应用无法仅凭
request本身知道“外部客户端其实用的是 HTTPS、原始域名是什么”,这些信息要靠代理附加的 Forwarded 头传递; - 代理有时会把 URL 中的一段路径前缀(如
/api/v1)剥掉再转发,应用必须借助 ASGI 的root_path机制“知道”自己被挂载在这个前缀之下,才能生成正确的链接与文档界面。
下文按这两条主线展开。
代理 Forwarded 头(Proxy Forwarded Headers)
当你部署一个代理在应用前面时,代理通常会在把请求转发给你的服务器之前,在请求上即时(on-the-fly)添加若干 HTTP 头,以告知服务器:该请求是从代理转发来的,并携带原始(对外)URL 信息,包括原始域名、外部是否使用了 HTTPS 等。
服务器程序(例如经由 FastAPI CLI 启动的 Uvicorn)具备解读这些头并把信息传给应用的能力。但从安全角度出发——服务器并不知道它自己是否真的运行在一个可信代理之后——默认情况下它不会解读这些头。这是为了防止外部攻击者自行伪造 Forwarded 头来欺骗应用。
/// 技术细节
涉及的三个代理头分别是:
X-Forwarded-For:原始客户端的 IP 地址X-Forwarded-Proto:原始使用的协议(如https)X-Forwarded-Host:原始的 Host(如mysuperapp.com)
///
启用 Forwarded 头:--forwarded-allow-ips
你可以用 CLI 选项 --forwarded-allow-ips 启动 FastAPI CLI,并指定哪些来源 IP 是被信任的,只有这些 IP 发来的请求中携带的 Forwarded 头才会被服务器读取和信任:
$ uv run fastapi run --forwarded-allow-ips="*"
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
如果设置为 --forwarded-allow-ips="*",则会信任所有入站 IP。如果你的服务器位于一个可信代理之后,且只有该代理会与它通信,那么信任“代理的 IP 无论是什么”是可以接受的做法——这正是 "*" 的适用前提:内网隔离、外部流量必须经过代理。
从仓库源码结构看,fastapi/ 包内并未解析这个选项(对 fastapi/ 目录的正则检索无命中),也就是说它最终由底层 ASGI 服务器(Uvicorn)消费,FastAPI CLI 只是把它透传给 Uvicorn。这也解释了为什么官方同时提到 Hypercorn 也有对应的选项——这是 ASGI 服务器层面的通用能力,而非 FastAPI 框架本身的行为。
HTTPS 下的重定向问题
假设你定义了一个路径操作(Path Operation)/items/:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
def read_items():
return ["plumbus", "portal gun"]
(完整代码见 tutorial001_01)
当客户端访问 /items(少了一个斜杠)时,FastAPI 会默认将请求重定向到 /items/。但在设置 --forwarded-allow-ips 之前,服务器不知道外部用的是 HTTPS,重定向目标可能是 http://localhost:8000/items/ 这类“内部视角”的 URL。
而实际上你的应用可能托管在 https://mysuperapp.com 上,重定向应该指向 https://mysuperapp.com/items/。
设置 --proxy-headers / --forwarded-allow-ips 之后,FastAPI 就能依据 Forwarded 头生成正确的重定向地址:
https://mysuperapp.com/items/
提示:关于 HTTPS 的更多背景,可参考仓库中的 关于 HTTPS 指南。
Forwarded 头的工作流程
下面这张时序图展示了代理在客户端与应用服务器之间如何注入 Forwarded 头:
sequenceDiagram
participant Client
participant Proxy as Proxy/Loadbalancer
participant Server as FastAPI Server
Client->>Proxy: HTTPS 请求<br/>Host: mysuperapp.com<br/>路径: /items
Note over Proxy: 代理添加 Forwarded 头
Proxy->>Server: HTTP 请求<br/>X-Forwarded-For: [客户端 IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>路径: /items
Note over Server: 服务器解读这些头<br/>(需设置 --forwarded-allow-ips)
Server->>Proxy: HTTP 响应<br/>携带正确的 HTTPS URL
Proxy->>Client: HTTPS 响应
流程要点:
- 代理拦截客户端的原始请求,在转发给应用服务器之前附加
X-Forwarded-*头; - 这些头保存了原本会丢失的信息:客户端真实 IP(
X-Forwarded-For)、原始协议(X-Forwarded-Proto)、原始域名(X-Forwarded-Host); - 当 FastAPI CLI 配置了
--forwarded-allow-ips后,服务器才会信任并使用这些头,例如用于生成正确的重定向 URL。
剥离路径前缀的代理(Proxy with a Stripped Path Prefix)
另一种常见的代理形态:代理会给你的应用添加一个路径前缀。此时可以使用 root_path 来配置你的应用。
root_path 是 ASGI 规范提供的机制(FastAPI 经由 Starlette 构建于 ASGI 之上),专门用于处理这类场景;FastAPI 内部在挂载子应用时同样复用了它。
“剥离路径前缀的代理”具体含义是:你在代码里声明的路径是 /app,但在更高一层,代理把整个 FastAPI 应用放置在一个如 /api/v1 的前缀下。此时:
- 原始的
/app路径实际上对外暴露在/api/v1/app; - 代理会把路径前缀即时“剥掉”后再把请求转发给应用服务器(通常是经由 FastAPI CLI 的 Uvicorn),从而让应用以为它运行在
/app之下——你无需把全部代码改成带/api/v1前缀。
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")}
(完整代码见 tutorial001)
到这里一切照常工作。但是,当你打开内置文档界面(Swagger UI 前端)时,它会假设 OpenAPI 模式在 /openapi.json 而不是 /api/v1/openapi.json。于是运行在浏览器里的前端会去请求 /openapi.json,从而无法获取 OpenAPI 模式。
由于代理添加了 /api/v1 前缀,前端必须从 /api/v1/openapi.json 获取模式。整体链路如下:
graph LR
browser("Browser")
proxy["Proxy 位于 http://0.0.0.0:9999/api/v1/app"]
server["Server 位于 http://127.0.0.1:8000/app"]
browser --> proxy
proxy --> server
提示:IP
0.0.0.0通常表示程序监听该机器/服务器上所有可用的 IP。
要让文档界面正常工作,OpenAPI 模式需要声明该 API 的 server 位于 /api/v1(即代理之后)。例如:
{
"openapi": "3.1.0",
// 这里还有其他设置
"servers": [
{
"url": "/api/v1"
}
],
"paths": {
// 这里还有其他设置
}
}
在这个示例中,“代理”可以是 Traefik 之类的软件,而服务器则是运行你 FastAPI 应用的 Uvicorn(FastAPI CLI)。
通过 --root-path 提供 root_path
为此,可以使用命令行选项 --root-path:
$ uv run fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
如果你使用的是 Hypercorn,它同样提供 --root-path 选项。
/// 技术细节
ASGI 规范为这类场景定义了 root_path。命令行选项 --root-path 正是由 ASGI 服务器把该值写入每个请求的 scope 字典中传给应用的。
///
读取当前请求的 root_path
你可以获取每个请求实际使用的 root_path。它是 ASGI 规范中 scope 字典的一部分。上面的示例代码把它(仅用于演示)放进了响应消息中:
return {"message": "Hello World", "root_path": request.scope.get("root_path")}
以 uv run fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1 启动后,响应大致为:
{
"message": "Hello World",
"root_path": "/api/v1"
}
在 FastAPI 应用中直接设置 root_path
如果你没有机会传递 --root-path 之类的命令行选项(例如平台托管环境不允许你控制 ASGI 服务器参数),可以在创建 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")}
(完整代码见 tutorial002)
给 FastAPI 传 root_path 与给 Uvicorn/Hypercorn 传 --root-path 选项是等效的。从源码实现看,这种等效性体现在 applications.py:当你在应用上设置了 self.root_path 时,FastAPI 会在每次请求进入时把它写入 ASGI scope(scope["root_path"] = self.root_path),因此无论值来自命令行还是构造函数,最终都走同一条 ASGI scope 通道。
关于 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 前缀。也就是说 root_path 只影响应用生成的 URL(OpenAPI、重定向、文档界面),不影响 Uvicorn 自身的路由匹配。
关于“非剥离前缀”的代理形态
剥离路径前缀只是众多代理配置中的一种。多数情况下,默认配置其实是代理不剥离任何前缀:
- 代理监听类似
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。
这种模式下不存在“前缀被剥掉”的问题,root_path 通常保持默认即可,你主要需要关心的是上一节提到的 Forwarded 头(HTTPS 协议与原始域名信息)。
用 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"
这份文件做了三件关键事:
stripPrefix中间件api-stripprefix定义了要剥离的前缀/api/v1;- 路由
app-http匹配PathPrefix(/api/v1)的请求并应用该中间件; - 服务
app把请求转发到http://127.0.0.1:8000(你的 Uvicorn)。
第三步:启动 Traefik 与 FastAPI 应用
$ ./traefik --configFile=traefik.toml
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
然后使用 --root-path 选项启动应用:
$ uv run fastapi run main.py --forwarded-allow-ips="*" --root-path /api/v1
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
验证两种访问路径的响应
直接访问 Uvicorn 端口(http://127.0.0.1:8000/app),看到正常响应:
{
"message": "Hello World",
"root_path": "/api/v1"
}
注意:虽然你访问的是不带前缀的 /app,但 root_path 显示为 /api/v1——它来自 --root-path 选项,而不是来自 URL。
再通过 Traefik 端口访问,带上路径前缀(http://127.0.0.1:9999/api/v1/app),得到相同的响应:
{
"message": "Hello World",
"root_path": "/api/v1"
}
但这次是在代理提供的 /api/v1 前缀路径之下。
设计意图是:所有用户都应经由代理访问应用,因此带 /api/v1 前缀的版本才是“正确”的入口;Uvicorn 直接提供的无前缀版本(http://127.0.0.1:8000/app)是专供代理(Traefik)访问的内部地址。这个实验同时演示了:代理(Traefik)如何使用路径前缀,服务器(Uvicorn)如何使用 --root-path 选项提供的 root_path。
验证文档界面
接下来是关键验证。“官方”的访问方式是经由代理、带上定义的路径前缀:
- 直接访问 Uvicorn 端口的文档界面(
http://127.0.0.1:8000/docs)会预期地失败,因为前端会请求/openapi.json而服务器并没有这个带前缀路径的语义; - 但经由代理访问
http://127.0.0.1:9999/api/v1/docs则一切正常。
这正是我们期望的效果。原因是 FastAPI 使用 root_path 来创建 OpenAPI 中的默认 server,其 URL 就是 root_path 提供的值。源码中这一行为清晰可见:applications.py 中,Swagger UI 的 HTML 响应在渲染前会把 root_path 拼接到 openapi_url(以及 OAuth2 重定向 URL)前面,因此文档界面里嵌入的 OpenAPI 地址自动变成了 /api/v1/openapi.json。
附加服务器列表(Additional Servers)
警告:这是一个进阶用法,初次阅读可以跳过。
默认情况下,FastAPI 会在 OpenAPI 模式中创建一个 server,其 URL 就是 root_path。但你也可以提供其他备选 servers,例如让同一个文档界面同时对接 Staging 与生产环境。
当你传入了自定义的 servers 列表、且存在 root_path(因为你的 API 运行在代理之后)时,FastAPI 会把一个使用该 root_path 的 server 插入到列表最前面。例如:
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")}
(完整代码见 tutorial003)
生成的 OpenAPI 模式为:
{
"openapi": "3.1.0",
// 这里还有其他设置
"servers": [
{
"url": "/api/v1"
},
{
"url": "https://stag.example.com",
"description": "Staging environment"
},
{
"url": "https://prod.example.com",
"description": "Production environment"
}
],
"paths": {
// 这里还有其他设置
}
}
注意那个由 root_path 自动生成的 url 为 /api/v1 的 server。
提示:文档界面会与你选中的 Server 交互,切换下拉框即可让同一份文档对接不同环境。
/// 技术细节
OpenAPI 规范中 servers 属性是可选的。如果你不传 servers 参数、且 root_path 的值为 /,生成的 OpenAPI 模式中会完全省略 servers 属性,这等价于只有一个 url 为 / 的默认服务器。
///
对应实现可以参见 applications.py:在 /openapi.json 的响应处理函数中,FastAPI 从 req.scope 读取当前请求的 root_path(去掉尾部斜杠),若其非空且 root_path_in_servers 为真,并且该 URL 尚未出现在 servers 列表中,就会把 {"url": root_path} 插到列表首位——这正是上面 JSON 中第一项的来源。
禁用来自 root_path 的自动 server
如果你不希望 FastAPI 自动加入这个基于 root_path 的 server,可以使用参数 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")}
(完整代码见 tutorial004)
此时 root_path 对应的 server 就不会再出现在 OpenAPI 模式的 servers 列表里,你完全控制文档界面可选择的服务器清单。
与子应用(Mounts)组合使用
如果你需要挂载子应用(参见 子应用 – Mounts 一节),同时又在使用带 root_path 的代理,完全可以按常规方式组合使用:FastAPI 会在内部“聪明地”使用 root_path,使二者协同工作。
从路由源码结构看,这一点的支撑在于 routing.py 中重定向路径的拼接同样读取 request.scope.get("root_path"),因此子应用内部触发的路径修正与重定向也会自动包含代理前缀,而不需要手动处理。
小结:关键选项与行为对照
| 配置项 | 作用对象 | 解决的问题 |
|---|---|---|
--forwarded-allow-ips="*"(或具体 IP) |
ASGI 服务器(Uvicorn/Hypercorn,经 FastAPI CLI 透传) | 信任代理来源,解读 X-Forwarded-For/Proto/Host,使重定向等生成正确的 HTTPS/域名 URL |
--root-path /api/v1 |
ASGI 服务器写入请求 scope |
告知应用“我运行在 /api/v1 前缀之后”,修正 OpenAPI servers、文档界面 URL |
FastAPI(root_path="/api/v1") |
应用自身,等效于上面的 CLI 选项 | 无法控制服务器参数时的替代方案(源码见 applications.py 的 scope 注入) |
FastAPI(servers=[...]) |
OpenAPI 生成 | 为同一文档界面提供 Staging/Production 等多服务器选择 |
root_path_in_servers=False |
OpenAPI 生成 | 禁用自动插入 root_path 对应的 server |
适用前提与限制:root_path 只影响应用层面生成的 URL,Uvicorn 自身仍按无前缀路径(如 /app)路由;--forwarded-allow-ips="*" 仅在“服务器只与可信代理通信”的隔离前提下安全,暴露面更大时应显式列出代理 IP。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00


