Atrás de um Proxy¶
🌐 Translation by AI and humans
This translation was made by AI guided by humans. 🤝
It could have mistakes of misunderstanding the original meaning, or looking unnatural, etc. 🤖
You can improve this translation by helping us guide the AI LLM better.
Em muitas situações, você usaria um proxy como Traefik ou Nginx na frente da sua aplicação FastAPI.
Esses proxies podem lidar com certificados HTTPS e outras coisas.
Headers Encaminhados pelo Proxy¶
Um proxy na frente da sua aplicação normalmente definiria alguns headers dinamicamente antes de enviar as requisições para o seu servidor, para informar ao servidor que a requisição foi encaminhada pelo proxy, informando a URL original (pública), incluindo o domínio, que está usando HTTPS, etc.
O programa do servidor (por exemplo, Uvicorn via CLI do FastAPI) é capaz de interpretar esses headers e então repassar essas informações para a sua aplicação.
Mas, por segurança, como o servidor não sabe que está atrás de um proxy confiável, ele não interpretará esses headers.
Ativar headers encaminhados pelo proxy¶
Você pode iniciar a CLI do FastAPI com a opção de linha de comando --forwarded-allow-ips e informar os endereços IP que devem ser confiáveis para ler esses headers encaminhados.
Se você definir como --forwarded-allow-ips="*", ele confiará em todos os IPs de entrada.
Se o seu servidor estiver atrás de um proxy confiável e somente o proxy falar com ele, isso fará com que ele aceite seja qual for o IP desse proxy.
$ 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)
Redirecionamentos com HTTPS¶
Por exemplo, suponha que você defina uma operação de rota /items/:
from fastapi import FastAPI
app = FastAPI()
@app.get("/items/")
def read_items():
return ["plumbus", "portal gun"]
Se o cliente tentar ir para /items, por padrão, ele seria redirecionado para /items/.
Mas antes de definir a opção de linha de comando --forwarded-allow-ips, poderia redirecionar para http://localhost:8000/items/.
Mas talvez sua aplicação esteja hospedada em https://mysuperapp.com, e o redirecionamento deveria ser para https://mysuperapp.com/items/.
Ao definir --proxy-headers, agora o FastAPI conseguirá redirecionar para o local correto. 😎
https://mysuperapp.com/items/
Dica
Se você quiser saber mais sobre HTTPS, confira o tutorial Sobre HTTPS.
Como funcionam os headers encaminhados pelo proxy¶
Aqui está uma representação visual de como o proxy adiciona headers encaminhados entre o cliente e o servidor da aplicação:
sequenceDiagram
participant Client
participant Proxy as Proxy/Load Balancer
participant Server as FastAPI Server
Client->>Proxy: HTTPS Request<br/>Host: mysuperapp.com<br/>Path: /items
Note over Proxy: Proxy adds forwarded headers
Proxy->>Server: HTTP Request<br/>X-Forwarded-For: [client IP]<br/>X-Forwarded-Proto: https<br/>X-Forwarded-Host: mysuperapp.com<br/>Path: /items
Note over Server: Server interprets headers<br/>(if --forwarded-allow-ips is set)
Server->>Proxy: HTTP Response<br/>with correct HTTPS URLs
Proxy->>Client: HTTPS Response
O proxy intercepta a requisição original do cliente e adiciona os headers especiais de encaminhamento (X-Forwarded-*) antes de repassar a requisição para o servidor da aplicação.
Esses headers preservam informações sobre a requisição original que, de outra forma, seriam perdidas:
- X-Forwarded-For: o endereço IP original do cliente
- X-Forwarded-Proto: o protocolo original (
https) - X-Forwarded-Host: o host original (
mysuperapp.com)
Quando a CLI do FastAPI é configurada com --forwarded-allow-ips, ela confia nesses headers e os utiliza, por exemplo, para gerar as URLs corretas em redirecionamentos.
Proxy com um prefixo de path removido¶
Você pode ter um proxy que adiciona um prefixo de path à sua aplicação.
Nesses casos, você pode usar root_path para configurar sua aplicação.
O root_path é um mecanismo fornecido pela especificação ASGI (na qual o FastAPI é construído, através do Starlette).
O root_path é usado para lidar com esses casos específicos.
E também é usado internamente ao montar sub-aplicações.
Ter um proxy com um prefixo de path removido, nesse caso, significa que você poderia declarar um path em /app no seu código, mas então você adiciona uma camada no topo (o proxy) que colocaria sua aplicação FastAPI sob um path como /api/v1.
Nesse caso, o path original /app seria servido em /api/v1/app.
Embora todo o seu código esteja escrito assumindo que existe apenas /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")}
E o proxy estaria "removendo" o prefixo de path dinamicamente antes de transmitir a solicitação para o servidor da aplicação (provavelmente Uvicorn via CLI do FastAPI), mantendo sua aplicação convencida de que está sendo servida em /app, para que você não precise atualizar todo o seu código para incluir o prefixo /api/v1.
Até aqui, tudo funcionaria normalmente.
Mas então, quando você abre a interface de documentação integrada (o frontend), ela esperaria obter o OpenAPI schema em /openapi.json, em vez de /api/v1/openapi.json.
Então, o frontend (que roda no navegador) tentaria acessar /openapi.json e não conseguiria obter o OpenAPI schema.
Como temos um proxy com um prefixo de path de /api/v1 para nossa aplicação, o frontend precisa buscar o OpenAPI schema em /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
Dica
O IP 0.0.0.0 é comumente usado para significar que o programa escuta em todos os IPs disponíveis naquela máquina/servidor.
A interface de documentação também precisaria do OpenAPI schema para declarar que este server da API está localizado em /api/v1 (atrás do proxy). Por exemplo:
{
"openapi": "3.1.0",
// Mais coisas aqui
"servers": [
{
"url": "/api/v1"
}
],
"paths": {
// Mais coisas aqui
}
}
Neste exemplo, o "Proxy" poderia ser algo como Traefik. E o servidor seria algo como a CLI do FastAPI com Uvicorn, executando sua aplicação FastAPI.
Fornecendo o root_path¶
Para conseguir isso, você pode usar a opção de linha de comando --root-path assim:
$ 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)
Se você usar Hypercorn, ele também tem a opção --root-path.
Detalhes Técnicos
A especificação ASGI define um root_path para esse caso de uso.
E a opção de linha de comando --root-path fornece esse root_path.
Verificando o root_path atual¶
Você pode obter o root_path atual usado pela sua aplicação para cada solicitação, ele faz parte do dicionário scope (que faz parte da especificação ASGI).
Aqui estamos incluindo-o na mensagem apenas para fins de demonstração.
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")}
Então, se você iniciar o Uvicorn com:
$ 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)
A resposta seria algo como:
{
"message": "Hello World",
"root_path": "/api/v1"
}
Configurando o root_path na aplicação FastAPI¶
Alternativamente, se você não tiver uma maneira de fornecer uma opção de linha de comando como --root-path ou equivalente, você pode definir o parâmetro root_path ao criar sua aplicação 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")}
Passar o root_path para FastAPI seria o equivalente a passar a opção de linha de comando --root-path para Uvicorn ou Hypercorn.
Sobre root_path¶
Tenha em mente que o servidor (Uvicorn) não usará esse root_path para nada além de passá-lo para a aplicação.
Mas se você acessar com seu navegador http://127.0.0.1:8000/app você verá a resposta normal:
{
"message": "Hello World",
"root_path": "/api/v1"
}
Portanto, ele não esperará ser acessado em http://127.0.0.1:8000/api/v1/app.
O Uvicorn esperará que o proxy acesse o Uvicorn em http://127.0.0.1:8000/app, e então seria responsabilidade do proxy adicionar o prefixo extra /api/v1 no topo.
Sobre proxies com um prefixo de path removido¶
Tenha em mente que um proxy com prefixo de path removido é apenas uma das maneiras de configurá-lo.
Provavelmente, em muitos casos, o padrão será que o proxy não tenha um prefixo de path removido.
Em um caso como esse (sem um prefixo de path removido), o proxy escutaria em algo como https://myawesomeapp.com, e então, se o navegador acessar https://myawesomeapp.com/api/v1/app e seu servidor (por exemplo, Uvicorn) escutar em http://127.0.0.1:8000, o proxy (sem um prefixo de path removido) acessaria o Uvicorn no mesmo path: http://127.0.0.1:8000/api/v1/app.
Testando localmente com Traefik¶
Você pode facilmente executar o experimento localmente com um prefixo de path removido usando Traefik.
Faça o download do Traefik, ele é um único binário, você pode extrair o arquivo compactado e executá-lo diretamente do terminal.
Então, crie um arquivo traefik.toml com:
[entryPoints]
[entryPoints.http]
address = ":9999"
[providers]
[providers.file]
filename = "routes.toml"
Isso diz ao Traefik para escutar na porta 9999 e usar outro arquivo routes.toml.
Dica
Estamos usando a porta 9999 em vez da porta padrão HTTP 80 para que você não precise executá-lo com privilégios de administrador (sudo).
Agora crie esse outro arquivo 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"
Esse arquivo configura o Traefik para usar o prefixo de path /api/v1.
E então o Traefik redirecionará suas solicitações para seu Uvicorn rodando em http://127.0.0.1:8000.
Agora inicie o Traefik:
$ ./traefik --configFile=traefik.toml
INFO[0000] Configuration loaded from file: /home/user/awesomeapi/traefik.toml
E agora inicie sua aplicação, usando a opção --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)
Verifique as respostas¶
Agora, se você for ao URL com a porta para o Uvicorn: http://127.0.0.1:8000/app, você verá a resposta normal:
{
"message": "Hello World",
"root_path": "/api/v1"
}
Dica
Perceba que, mesmo acessando em http://127.0.0.1:8000/app, ele mostra o root_path de /api/v1, retirado da opção --root-path.
E agora abra o URL com a porta para o Traefik, incluindo o prefixo de path: http://127.0.0.1:9999/api/v1/app.
Obtemos a mesma resposta:
{
"message": "Hello World",
"root_path": "/api/v1"
}
mas desta vez no URL com o prefixo de path fornecido pelo proxy: /api/v1.
Claro, a ideia aqui é que todos acessariam a aplicação através do proxy, então a versão com o prefixo de path /api/v1 é a "correta".
E a versão sem o prefixo de path (http://127.0.0.1:8000/app), fornecida diretamente pelo Uvicorn, seria exclusivamente para o proxy (Traefik) acessá-la.
Isso demonstra como o Proxy (Traefik) usa o prefixo de path e como o servidor (Uvicorn) usa o root_path da opção --root-path.
Verifique a interface de documentação¶
Mas aqui está a parte divertida. ✨
A maneira "oficial" de acessar a aplicação seria através do proxy com o prefixo de path que definimos. Então, como esperaríamos, se você tentar a interface de documentação servida diretamente pelo Uvicorn, sem o prefixo de path no URL, ela não funcionará, porque espera ser acessada através do proxy.
Você pode verificar em http://127.0.0.1:8000/docs:

Mas se acessarmos a interface de documentação no URL "oficial" usando o proxy com a porta 9999, em /api/v1/docs, ela funciona corretamente! 🎉
Você pode verificar em http://127.0.0.1:9999/api/v1/docs:

Exatamente como queríamos. ✔️
Isso porque o FastAPI usa esse root_path para criar o server padrão no OpenAPI com o URL fornecido pelo root_path.
Servidores adicionais¶
Atenção
Este é um caso de uso mais avançado. Sinta-se à vontade para pular.
Por padrão, o FastAPI criará um server no OpenAPI schema com o URL para o root_path.
Mas você também pode fornecer outros servers alternativos, por exemplo, se quiser que a mesma interface de documentação interaja com ambientes de staging e produção.
Se você passar uma lista personalizada de servers e houver um root_path (porque sua API está atrás de um proxy), o FastAPI inserirá um "server" com esse root_path no início da lista.
Por exemplo:
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")}
Gerará um OpenAPI schema como:
{
"openapi": "3.1.0",
// Mais coisas aqui
"servers": [
{
"url": "/api/v1"
},
{
"url": "https://stag.example.com",
"description": "Staging environment"
},
{
"url": "https://prod.example.com",
"description": "Production environment"
}
],
"paths": {
// Mais coisas aqui
}
}
Dica
Perceba o servidor gerado automaticamente com um valor url de /api/v1, retirado do root_path.
Na interface de documentação em http://127.0.0.1:9999/api/v1/docs parecerá:

Dica
A interface de documentação interagirá com o servidor que você selecionar.
Detalhes Técnicos
A propriedade servers na especificação OpenAPI é opcional.
Se você não especificar o parâmetro servers e root_path for igual a /, a propriedade servers no OpenAPI gerado será totalmente omitida por padrão, o que equivale a um único servidor com valor de url igual a /.
Desabilitar servidor automático de root_path¶
Se você não quiser que o FastAPI inclua um servidor automático usando o root_path, você pode usar o parâmetro 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")}
e então ele não será incluído no OpenAPI schema.
Montando uma sub-aplicação¶
Se você precisar montar uma sub-aplicação (como descrito em Sub-aplicações - Montagens) enquanto também usa um proxy com root_path, você pode fazer isso normalmente, como esperaria.
O FastAPI usará internamente o root_path de forma inteligente, então tudo funcionará. ✨