Skip to content

代理背后

在某些情况下,你可能需要使用 代理 服务器,如 Traefik 或 Nginx,并配置一个额外的路径前缀,该前缀不会被你的应用程序看到。

在这些情况下,你可以使用 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

Tip

IP 0.0.0.0 通常用于表示程序监听机器/服务器上所有可用的 IP。

文档 UI 还需要 OpenAPI 模式来声明此 API server 位于 /api/v1(在代理背后)。例如:

{
    "openapi": "3.1.0",
    // 更多内容在这里
    "servers": [
        {
            "url": "/api/v1"
        }
    ],
    "paths": {
            // 更多内容在这里
    }
}

在这个例子中,“Proxy” 可以是类似 Traefik 的东西。而服务器可以是带有 Uvicorn 的 FastAPI CLI,运行你的 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

Tip

我们使用端口 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 --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"
}

Tip

注意,尽管你访问的是 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 检查:

但如果我们使用端口 9999 的代理访问“官方”URL 上的文档 UI,在 /api/v1/docs,它将正确工作!🎉

你可以在 http://127.0.0.1:9999/api/v1/docs 检查:

正如我们所期望的那样。✔️

这是因为 FastAPI 使用这个 root_path 在 OpenAPI 中创建默认的 server,其 URL 由 root_path 提供。

额外的服务器

Warning

这是一个更高级的使用场景。你可以随意跳过它。

默认情况下,FastAPI 会在 OpenAPI 模式中创建一个 server,其 URL 为 root_path

但你也可以提供其他备选的 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",
    // 更多内容
    "servers": [
        {
            "url": "/api/v1"
        },
        {
            "url": "https://stag.example.com",
            "description": "暂存环境"
        },
        {
            "url": "https://prod.example.com",
            "description": "生产环境"
        }
    ],
    "paths": {
            // 更多内容
    }
}

Tip

注意自动生成的服务器,其 url 值为 /api/v1,取自 root_path

在文档 UI 中,地址为 http://127.0.0.1:9999/api/v1/docs,它看起来像这样:

Tip

文档 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,因此它会正常工作。✨