Перейти к содержанию

За прокси‑сервером

Во многих случаях перед приложением FastAPI используется прокси‑сервер, например Traefik или Nginx.

Такие прокси могут обрабатывать HTTPS‑сертификаты и многое другое.

Пересылаемые заголовки прокси

Прокси перед вашим приложением обычно на лету добавляет некоторые HTTP‑заголовки перед отправкой запроса на ваш сервер, чтобы сообщить ему, что запрос был переслан прокси, а также передать исходный (публичный) URL (включая домен), информацию об использовании HTTPS и т.д.

Программа сервера (например, Uvicorn, запущенный через FastAPI CLI) умеет интерпретировать эти заголовки и передавать соответствующую информацию вашему приложению.

Но из соображений безопасности, пока сервер не уверен, что находится за доверенным прокси, он не будет интерпретировать эти заголовки.

Технические детали

Заголовки прокси:

Включить пересылаемые заголовки прокси

Вы можете запустить FastAPI CLI с опцией командной строки --forwarded-allow-ips и передать IP‑адреса, которым следует доверять при чтении этих пересылаемых заголовков.

Если указать --forwarded-allow-ips="*", приложение будет доверять всем входящим IP.

Если ваш сервер находится за доверенным прокси и только прокси обращается к нему, этого достаточно, чтобы он принимал IP этого прокси.

$ fastapi run --forwarded-allow-ips="*"

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Редиректы с HTTPS

Например, вы объявили операцию пути /items/:

from fastapi import FastAPI

app = FastAPI()


@app.get("/items/")
def read_items():
    return ["plumbus", "portal gun"]

Если клиент обратится к /items, по умолчанию произойдёт редирект на /items/.

Но до установки опции --forwarded-allow-ips редирект может вести на http://localhost:8000/items/.

Однако приложение может быть доступно по https://mysuperapp.com, и редирект должен вести на https://mysuperapp.com/items/.

Указав --proxy-headers, FastAPI сможет редиректить на корректный адрес. 😎

https://mysuperapp.com/items/

Совет

Если хотите узнать больше об HTTPS, смотрите руководство О HTTPS.

Как работают пересылаемые заголовки прокси

Ниже показано, как прокси добавляет пересылаемые заголовки между клиентом и сервером приложения:

sequenceDiagram
    participant Client as Клиент
    participant Proxy as Прокси/Балансировщик нагрузки
    participant Server as FastAPI-сервер

    Client->>Proxy: HTTPS-запрос<br/>Host: mysuperapp.com<br/>Path: /items

    Note over Proxy: Прокси-сервер добавляет пересылаемые заголовки

    Proxy->>Server: HTTP-запрос<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items

    Note over Server: Server интерпретирует HTTP-заголовки<br/>(если --forwarded-allow-ips установлен)

    Server->>Proxy: HTTP-ответ<br/>с верными HTTPS URLs

    Proxy->>Client: HTTPS-ответ

Прокси перехватывает исходный клиентский запрос и добавляет специальные пересылаемые заголовки (X-Forwarded-*) перед передачей запроса на сервер приложения.

Эти заголовки сохраняют информацию об исходном запросе, которая иначе была бы потеряна:

  • X-Forwarded-For: исходный IP‑адрес клиента
  • X-Forwarded-Proto: исходный протокол (https)
  • X-Forwarded-Host: исходный хост (mysuperapp.com)

Когда FastAPI CLI сконфигурирован с --forwarded-allow-ips, он доверяет этим заголовкам и использует их, например, чтобы формировать корректные URL в редиректах.

Прокси с функцией удаления префикса пути

Прокси может добавлять к вашему приложению префикс пути (размещать приложение по пути с дополнительным префиксом).

В таких случаях вы можете использовать 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")}

Прокси будет «обрезать» префикс пути на лету перед передачей запроса на сервер приложения (скорее всего Uvicorn, запущенный через FastAPI CLI), поддерживая у вашего приложения иллюзию, что его обслуживают по /app, чтобы вам не пришлось менять весь код и добавлять префикс /api/v1.

До этого момента всё будет работать как обычно.

Но когда вы откроете встроенный интерфейс документации (фронтенд), он будет ожидать получить схему OpenAPI по адресу /openapi.json, а не /api/v1/openapi.json.

Поэтому фронтенд (который работает в браузере) попытается обратиться к /openapi.json и не сможет получить схему OpenAPI.

Так как для нашего приложения используется прокси с префиксом пути /api/v1, фронтенду нужно забирать схему OpenAPI по /api/v1/openapi.json.

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‑адресах, доступных на этой машине/сервере.

Интерфейсу документации также нужна схема OpenAPI, в которой будет указано, что этот API server находится по пути /api/v1 (за прокси). Например:

{
    "openapi": "3.1.0",
    // Здесь ещё что-то
    "servers": [
        {
            "url": "/api/v1"
        }
    ],
    "paths": {
            // Здесь ещё что-то
    }
}

В этом примере «Proxy» может быть, например, Traefik. А сервером будет что‑то вроде FastAPI CLI с Uvicorn, на котором запущено ваше приложение FastAPI.

Указание root_path

Для этого используйте опцию командной строки --root-path, например так:

$ fastapi run main.py --forwarded-allow-ips="*" --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 --forwarded-allow-ips="*" --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"
}

Установка root_path в приложении FastAPI

Если нет возможности передать опцию командной строки --root-path (или аналог), вы можете указать параметр root_path при создании приложения FastAPI:

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, а уже задача прокси — добавить сверху префикс /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, работающий на 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 --forwarded-allow-ips="*" --root-path /api/v1

<span style="color: green;">INFO</span>:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Проверьте ответы

Теперь, если вы перейдёте на URL с портом Uvicorn: 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.

А теперь откройте URL с портом Traefik и префиксом пути: 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.

Проверьте интерфейс документации

А вот самое интересное. ✨

«Официальный» способ доступа к приложению — через прокси с заданным префиксом пути. Поэтому, как и ожидается, если открыть интерфейс документации, отдаваемый напрямую Uvicorn, без префикса пути в URL, он не будет работать, так как предполагается доступ через прокси.

Проверьте по адресу http://127.0.0.1:8000/docs:

А вот если открыть интерфейс документации по «официальному» URL через прокси на порту 9999, по /api/v1/docs, всё работает корректно! 🎉

Проверьте по адресу http://127.0.0.1:9999/api/v1/docs:

Именно как и хотелось. ✔️

Это потому, что FastAPI использует root_path, чтобы создать в OpenAPI сервер по умолчанию с URL из root_path.

Дополнительные серверы

Предупреждение

Это более продвинутый сценарий. Можно пропустить.

По умолчанию FastAPI создаёт в схеме OpenAPI server с URL из root_path.

Но вы также можете указать дополнительные servers, например, если хотите, чтобы один и тот же интерфейс документации работал и со стейджингом, и с продакшн.

Если вы передадите свой список servers и при этом задан root_path (потому что ваш API работает за прокси), FastAPI вставит «server» с этим 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",
    // Здесь ещё что-то
    "servers": [
        {
            "url": "/api/v1"
        },
        {
            "url": "https://stag.example.com",
            "description": "Staging environment"
        },
        {
            "url": "https://prod.example.com",
            "description": "Production environment"
        }
    ],
    "paths": {
            // Здесь ещё что-то
    }
}

Совет

Обратите внимание на автоматически добавленный сервер с url равным /api/v1, взятым из root_path.

В интерфейсе документации по адресу http://127.0.0.1:9999/api/v1/docs это будет выглядеть так:

Совет

Интерфейс документации будет взаимодействовать с сервером, который вы выберете.

Отключить автоматическое добавление сервера из 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 внутри, так что всё просто работает. ✨