За прокси‑сервером¶
Во многих случаях перед приложением 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
внутри, так что всё просто работает. ✨