使用代理¶
有些情况下,您可能要使用 Traefik 或 Nginx 等代理服务器,并添加应用不能识别的附加路径前缀配置。
此时,要使用 root_path
配置应用。
root_path
是 ASGI 规范提供的机制,FastAPI 就是基于此规范开发的(通过 Starlette)。
root_path
用于处理这些特定情况。
在挂载子应用时,也可以在内部使用。
移除路径前缀的代理¶
本例中,移除路径前缀的代理是指在代码中声明路径 /app
,然后在应用顶层添加代理,把 FastAPI 应用放在 /api/v1
路径下。
本例的原始路径 /app
实际上是在 /api/v1/app
提供服务。
哪怕所有代码都假设只有 /app
。
代理只在把请求传送给 Uvicorn 之前才会移除路径前缀,让应用以为它是在 /app
提供服务,因此不必在代码中加入前缀 /api/v1
。
但之后,在(前端)打开 API 文档时,代理会要求在 /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。
API 文档还需要 OpenAPI 概图声明 API server
位于 /api/v1
(使用代理时的 URL)。例如:
{
"openapi": "3.0.2",
// More stuff here
"servers": [
{
"url": "/api/v1"
}
],
"paths": {
// More stuff here
}
}
本例中的 Proxy
是 Traefik,server
是运行 FastAPI 应用的 Uvicorn。
提供 root_path
¶
为此,要以如下方式使用命令行选项 --root-path
:
$ uvicorn main:app --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 规范的内容)。
我们在这里的信息里包含 roo_path
只是为了演示。
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:
$ uvicorn main:app --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:800/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
代理(未移除路径前缀) 会在同样的路径:http://127.0.0.1:8000/api/v1/app
访问 Uvicorn。
本地测试 Traefik¶
您可以轻易地在本地使用 Traefik 运行移除路径前缀的试验。
下载 Traefik,这是一个二进制文件,需要解压文件,并在 Terminal 中直接运行。
然后创建包含如下内容的 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
。
然后,它把请求重定位到运行在 http://127.0.0.1:8000
上的 Uvicorn。
现在,启动 Traefik:
$ ./traefik --configFile=traefik.toml
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
接下来,使用 Uvicorn 启动应用,并使用 --root-path
选项:
$ uvicorn main:app --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
中提取的 /api/v1
,这是 root_path
的值。
打开含 Traefik 端口的 URL,包含路径前缀:http://127.0.0.1:9999/api/v1/app。
得到同样的响应:
{
"message": "Hello World",
"root_path": "/api/v1"
}
但这一次 URL 包含了代理提供的路径前缀:/api/v1
。
当然,这是通过代理访问应用的方式,因此,路径前缀 /app/v1
版本才是正确的。
而不带路径前缀的版本(http://127.0.0.1:8000/app
),则由 Uvicorn 直接提供,专供代理(Traefik)访问。
这演示了代理(Traefik)如何使用路径前缀,以及服务器(Uvicorn)如何使用选项 --root-path
中的 root_path
。
查看文档¶
但这才是有趣的地方 ✨
访问应用的官方方式是通过含路径前缀的代理。因此,不出所料,如果没有在 URL 中添加路径前缀,直接访问通过 Uvicorn 运行的 API 文档,不能正常访问,因为需要通过代理才能访问。
输入 http://127.0.0.1:8000/docs 查看 API 文档:
但输入官方链接 /api/v1/docs
,并使用端口 9999
访问 API 文档,就能正常运行了!🎉
输入 http://127.0.0.1:9999/api/v1/docs 查看文档:
一切正常。 ✔️
这是因为 FastAPI 在 OpenAPI 里使用 root_path
提供的 URL 创建默认 server
。
附加的服务器¶
警告
此用例较难,可以跳过。
默认情况下,FastAPI 使用 root_path
的链接在 OpenAPI 概图中创建 server
。
但也可以使用其它备选 servers
,例如,需要同一个 API 文档与 staging 和生产环境交互。
如果传递自定义 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.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
}
}
提示
注意,自动生成服务器时,url
的值 /api/v1
提取自 roog_path
。
http://127.0.0.1:9999/api/v1/docs 的 API 文档所示如下:
提示
API 文档与所选的服务器进行交互。
从 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
,因此子应用也可以正常运行。✨