代理背后的 FastAPI¶
在某些情况下,您可能需要使用像 Traefik 或 Nginx 这样的 **代理** 服务器,其配置会在您的应用程序看不到的情况下添加额外的路径前缀。
在这些情况下,您可以使用 root_path
来配置您的应用程序。
root_path
是 ASGI 规范 (FastAPI 是基于 Starlette 通过 ASGI 规范构建的) 提供的一种机制。
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 CLI 这样的东西,它使用 **Uvicorn** 运行您的 FastAPI 应用程序。
提供 root_path
¶
要实现这一点,您可以使用类似于以下内容的命令行选项 --root-path
$ fastapi run main.py --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 --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 会将它的请求重定向到您的 Uvicorn,该 Uvicorn 在 http://127.0.0.1:8000
上运行。
现在启动 Traefik
$ ./traefik --configFile=traefik.toml
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
现在启动您的应用程序,使用 --root-path
选项
$ fastapi run main.py --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
。
当然,这里的想法是每个人都通过代理访问应用程序,因此带有路径前缀 /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 处进行检查。
但是,如果我们在“官方”URL 处使用代理访问文档 UI,该代理使用端口 9999
,位于 /api/v1/docs
处,它可以正常工作!🎉
您可以在 http://127.0.0.1:9999/api/v1/docs 处进行检查。
正如我们想要的那样。✔️
这是因为 FastAPI 使用此 root_path
在 OpenAPI 中使用 root_path
提供的 URL 创建默认的 server
。
其他服务器¶
警告
这是一个更高级的用例。您可以跳过它。
默认情况下,FastAPI 会在 OpenAPI 架构中创建一个带有 root_path
URL 的 server
。
但您也可以提供其他可选的 servers
,例如,如果您希望同一个 文档 UI 与暂存环境和生产环境交互。
如果您传递了一个自定义的 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")}
将生成一个类似于以下内容的 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
,因此它将正常工作。✨