跳到内容

代理服务器之后

在某些情况下,您可能需要使用像 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 获取 OpenAPI 架构,而不是 /api/v1/openapi.json

因此,前端(在浏览器中运行)会尝试访问 /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 将其请求重定向到运行在 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"
}

提示

请注意,即使您通过 http://127.0.0.1:8000/app 访问它,它也会显示 /api/v1root_path,该值取自 --root-path 选项。

现在打开带有 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-pathroot_path

检查文档 UI

但有趣的部分来了。✨

访问应用程序的“官方”方式是通过我们定义的带路径前缀的代理。因此,正如我们所期望的,如果您直接尝试通过 Uvicorn 提供的文档 UI,而不带 URL 中的路径前缀,它将无法工作,因为它期望通过代理访问。

您可以在 http://127.0.0.1:8000/docs 查看

但是如果我们通过代理(端口 9999)在“官方”URL /api/v1/docs 访问文档 UI,它就能正常工作! 🎉

您可以在 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 的“服务器”。

例如:

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 将与您选择的服务器进行交互。

禁用 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,所以它会正常工作。✨