Introdução ao Desenvolvimento Web com Flask

Introdução ao Desenvolvimento Web com Flask

Flask começa com uma rota e uma função, e os erros aparecem no que ele deixa para você: o get_json() que recusa formulário com 415, o type=bool que lê "false" como verdadeiro, o handler genérico que transforma 405 em 500 e o debug=True que abre um console Python no navegador.
Python

• • 22 min de leitura

Flask é um microframework web para Python — leve, flexível e direto ao ponto. Diferente de frameworks como Django, Flask não impõe estrutura, ORM ou sistema de templates específico. Você começa com o mínimo e adiciona apenas o que precisa. Isso o torna ideal para APIs, microserviços e projetos onde o controle total da arquitetura é importante.

Instalação e Primeiro Servidor

pip install flask
# app.py
from flask import Flask

app = Flask(__name__)

@app.route("/")
def inicio():
    return "Olá, Flask!"

@app.route("/sobre")
def sobre():
    return "Sistema de gestão escolar v1.0"

if __name__ == "__main__":
    app.run(debug=True)
python app.py
# * Running on http://127.0.0.1:5000

debug=True ativa o reloader automático — o servidor reinicia ao salvar o arquivo — e troca a página de erro pelo depurador do Werkzeug. É por causa dele que o modo nunca pode ir para produção: a página de erro traz um console Python interativo, que executa código no servidor a partir do navegador, protegido apenas por um PIN. O mesmo vale para o app.run() em geral, que é um servidor de desenvolvimento; em produção a aplicação roda sob um servidor WSGI, como o Gunicorn ou o Waitress. No dia a dia, o comando de linha de comando dispensa o bloco if __name__: flask --app app run --debug faz o mesmo que python app.py com debug=True, sem que a opção fique gravada no código.

Rotas e Métodos HTTP

from flask import Flask, request, jsonify

app = Flask(__name__)

# Rota com parâmetro de URL
@app.route("/usuarios/<int:usuario_id>")
def buscar_usuario(usuario_id):
    return jsonify({"id": usuario_id, "nome": f"Usuário {usuario_id}"})

# Parâmetro string
@app.route("/produtos/<string:slug>")
def buscar_produto(slug):
    return jsonify({"slug": slug})

# Múltiplos métodos
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        dados = request.get_json(silent=True) or {}   # None se o corpo não for JSON
        email = dados.get("email")
        senha = dados.get("senha")
        if not email or not senha:
            return jsonify({"erro": "email e senha são obrigatórios."}), 400
        return jsonify({"mensagem": f"Login de {email} processado."})
    return jsonify({"mensagem": "Envie POST com email e senha."})

# Rota com valor padrão
@app.route("/pagina/")
@app.route("/pagina/<int:numero>")
def pagina(numero=1):
    return jsonify({"pagina": numero})

Três comportamentos das rotas que o exemplo não mostra. /pagina, sem a barra, responde 308 redirecionando para /pagina/, porque a rota foi declarada com a barra final; o contrário, declarar sem barra e acessar com ela, dá 404. O conversor int não aceita negativos — /pagina/-1 é 404, e <int(signed=True):numero> resolve —, e o string não aceita barra, de modo que /produtos/a/b também é 404 (para isso existe path). E o login usa get_json(silent=True) de propósito: desde o Flask 2.3, get_json() sem esse argumento responde 415 Unsupported Media Type quando o corpo não chega como application/json — um POST vindo de formulário HTML nunca alcançaria o código da função.

Request: Acessando Dados da Requisição

from flask import request

@app.route("/dados", methods=["POST"])
def receber_dados():
    # JSON — sem silent=True, um POST de formulário pararia aqui com 415
    dados_json = request.get_json(silent=True)

    # Query parameters — ?nome=Ana&idade=25
    nome  = request.args.get("nome", "Anônimo")
    idade = request.args.get("idade", type=int)   # None se ausente OU inválido

    # Form data
    campo = request.form.get("campo")

    # Headers
    token = request.headers.get("Authorization")

    # Arquivos
    arquivo = request.files.get("documento")

    # Informações da requisição
    ip      = request.remote_addr   # atrás de proxy, é o IP do proxy
    metodo  = request.method
    url     = request.url

    return jsonify({
        "ip":     ip,
        "metodo": metodo,
        "nome":   nome,
    })

O mesmo 415 explica o comentário no topo da função: com get_json() na primeira linha, qualquer envio de formulário ou de arquivo para /dados parava ali, e as linhas que leem request.form e request.files nunca eram executadas. Duas armadilhas silenciosas completam o quadro. O type=int de request.args.get devolve None tanto para parâmetro ausente quanto para inválido: ?idade=abc não gera erro nenhum. E remote_addr, atrás de um proxy reverso ou balanceador, é o endereço do proxy, não o do cliente; o IP real vem no cabeçalho X-Forwarded-For, e o jeito seguro de lê-lo é o werkzeug.middleware.proxy_fix.ProxyFix, configurado com o número de proxies que existem de fato — confiar no cabeçalho sem isso deixa qualquer cliente escolher o próprio IP.

Responses: Controlando a Resposta

from flask import jsonify, make_response, redirect, url_for, abort

# JSON com status code
@app.route("/criado", methods=["POST"])
def criar():
    return jsonify({"id": 1, "mensagem": "Criado"}), 201

# Response customizada
@app.route("/customizado")
def customizado():
    resposta = make_response(
        jsonify({"dados": "aqui"}),
        200
    )
    resposta.headers["X-Custom-Header"] = "valor"
    resposta.headers["Cache-Control"]   = "no-cache"
    return resposta

# Redirecionamento
@app.route("/antigo")
def antigo():
    return redirect(url_for("inicio"), 301)

# Abortando com erro
@app.route("/restrito")
def restrito():
    abort(403)   # lança Forbidden automaticamente

Tratamento de Erros

from flask import jsonify
from werkzeug.exceptions import HTTPException

@app.errorhandler(404)
def nao_encontrado(erro):
    return jsonify({
        "erro":    "Recurso não encontrado.",
        "codigo":  404
    }), 404

@app.errorhandler(403)
def proibido(erro):
    return jsonify({
        "erro":   "Acesso negado.",
        "codigo": 403
    }), 403

# Os demais erros HTTP (405, 415, 400...) mantêm o próprio código
@app.errorhandler(HTTPException)
def erro_http(erro):
    return jsonify({
        "erro":   erro.description,
        "codigo": erro.code
    }), erro.code

# Qualquer outra exceção: o detalhe vai para o log, não para o cliente
@app.errorhandler(Exception)
def erro_generico(erro):
    app.logger.exception("Erro não tratado")
    return jsonify({
        "erro":   "Erro interno do servidor.",
        "codigo": 500
    }), 500

O errorhandler(Exception) é o ponto que mais engana. Ele não pega só os erros do código: pega também as exceções HTTP que não têm handler próprio. Registrado sem o de HTTPException — só ele e os de 403, 404 e 500 —, um POST numa rota que só aceita GET responde 500 com o texto 405 Method Not Allowed — o cliente recebe o código errado —, e o handler de 500 nunca é chamado, porque o de Exception chega antes. Pior: devolver str(erro) manda ao cliente a mensagem interna da exceção, e um arquivo que não abre expõe o caminho completo dele no servidor. Por isso o handler de HTTPException vem antes, preservando o código de cada erro, e o genérico registra o detalhe no log e responde uma mensagem neutra.

Blueprints: Organizando a Aplicação

Blueprints permitem dividir a aplicação em módulos independentes:

projeto/
├── app.py
├── config.py
└── routes/
    ├── __init__.py
    ├── alunos.py
    └── disciplinas.py
# routes/alunos.py
from flask import Blueprint, jsonify, request

alunos_bp = Blueprint("alunos", __name__, url_prefix="/alunos")

# Banco em memória para exemplo
_alunos = {}
_proximo_id = 1

@alunos_bp.route("/", methods=["GET"])
def listar():
    nota_min = request.args.get("nota_min", 0, type=float)
    resultado = [
        a for a in _alunos.values()
        if a["nota"] >= nota_min
    ]
    return jsonify(resultado)

@alunos_bp.route("/<int:aluno_id>", methods=["GET"])
def buscar(aluno_id):
    aluno = _alunos.get(aluno_id)
    if not aluno:
        return jsonify({"erro": "Aluno não encontrado."}), 404
    return jsonify(aluno)

@alunos_bp.route("/", methods=["POST"])
def criar():
    global _proximo_id
    dados = request.get_json(silent=True)

    if not isinstance(dados, dict) or not dados.get("nome") or not dados.get("email"):
        return jsonify({"erro": "nome e email são obrigatórios."}), 400
    try:
        nota = float(dados.get("nota", 0.0))
    except (TypeError, ValueError):
        return jsonify({"erro": "nota deve ser um número."}), 400

    aluno = {
        "id":    _proximo_id,
        "nome":  dados["nome"],
        "email": dados["email"],
        "nota":  nota,
    }
    _alunos[_proximo_id] = aluno
    _proximo_id += 1
    return jsonify(aluno), 201

@alunos_bp.route("/<int:aluno_id>", methods=["PUT"])
def atualizar(aluno_id):
    aluno = _alunos.get(aluno_id)
    if not aluno:
        return jsonify({"erro": "Aluno não encontrado."}), 404

    dados = request.get_json(silent=True)
    if not isinstance(dados, dict):
        return jsonify({"erro": "Body JSON obrigatório."}), 400
    try:
        nota = float(dados.get("nota", aluno["nota"]))
    except (TypeError, ValueError):
        return jsonify({"erro": "nota deve ser um número."}), 400

    aluno.update({
        "nome":  dados.get("nome",  aluno["nome"]),
        "email": dados.get("email", aluno["email"]),
        "nota":  nota,
    })
    return jsonify(aluno)

@alunos_bp.route("/<int:aluno_id>", methods=["DELETE"])
def deletar(aluno_id):
    if aluno_id not in _alunos:
        return jsonify({"erro": "Aluno não encontrado."}), 404
    del _alunos[aluno_id]
    return "", 204
# app.py
from flask import Flask
from routes.alunos import alunos_bp

def criar_app():
    app = Flask(__name__)

    # Registrando blueprints
    app.register_blueprint(alunos_bp)

    return app

app = criar_app()

if __name__ == "__main__":
    app.run(debug=True)

A validação da nota não é detalhe. Sem a conversão com float, um único aluno criado com "nota": "9" — texto, não número — era aceito com 201, e a partir dali toda listagem respondia 500 com TypeError: '>=' not supported between instances of 'str' and 'int': um dado ruim gravado derruba a rota para todo mundo. Repare também que o blueprint registra a rota como / dentro de url_prefix="/alunos", então o endereço canônico é /alunos/; /alunos responde 308 e o cliente precisa seguir o redirecionamento (o 308, ao contrário do 301, preserva o método POST).

Middleware e Before/After Request

from flask import request, g
import time

@app.before_request
def antes_da_requisicao():
    g.inicio = time.perf_counter()   # g é o contexto global da requisição
    print(f"→ {request.method} {request.path}")

@app.after_request
def depois_da_requisicao(resposta):
    duracao = time.perf_counter() - g.get("inicio", time.perf_counter())
    resposta.headers["X-Tempo-Ms"] = f"{duracao * 1000:.2f}"
    print(f"← {resposta.status_code} em {duracao*1000:.1f}ms")
    return resposta

@app.teardown_request
def encerrar_requisicao(excecao=None):
    # Executado ao final — com ou sem erro
    # Ideal para fechar conexões de banco
    pass

Configuração

# config.py
import os
from dotenv import load_dotenv

load_dotenv()

class Config:
    SECRET_KEY       = os.getenv("SECRET_KEY", "dev-inseguro")
    DEBUG            = False
    TESTING          = False

class ConfigDesenvolvimento(Config):
    DEBUG            = True
    DATABASE_URL     = "sqlite:///dev.db"

class ConfigProducao(Config):
    DATABASE_URL     = os.getenv("DATABASE_URL")
    SECRET_KEY       = os.getenv("SECRET_KEY")   # obrigatório em produção

class ConfigTestes(Config):
    TESTING          = True
    DATABASE_URL     = "sqlite:///:memory:"


configs = {
    "desenvolvimento": ConfigDesenvolvimento,
    "producao":        ConfigProducao,
    "testes":          ConfigTestes,
}

# app.py
def criar_app(ambiente="desenvolvimento"):
    app = Flask(__name__)
    app.config.from_object(configs[ambiente])
    if not app.config["SECRET_KEY"]:
        # sem isto, a aplicação sobe e só quebra no primeiro uso de sessão
        raise RuntimeError("SECRET_KEY não definida.")
    return app

A checagem em criar_app existe porque a falta da chave não impede a aplicação de subir. Com SECRET_KEY ausente do ambiente, ConfigProducao fica com None, tudo funciona, e só a primeira rota que usar session quebra, com 500 e RuntimeError: The session is unavailable because no secret key was set. Falhar na inicialização troca um erro intermitente em produção por um deploy que não completa. Note ainda que o padrão "dev-inseguro" da classe base continua valendo em desenvolvimento e em testes, que é exatamente onde ele deve valer.

Exemplo Completo: API REST de Escola

import re
from datetime import datetime, UTC

from flask import Flask, jsonify, request

app = Flask(__name__)
app.json.ensure_ascii = False   # "José", e não "Jos\u00e9", no JSON de saída

# Simulando banco em memória
banco = {
    "alunos": {
        1: {"id": 1, "nome": "Ana Silva",   "email": "ana@email.com",   "nota": 9.5},
        2: {"id": 2, "nome": "Bruno Costa", "email": "bruno@email.com", "nota": 7.0},
        3: {"id": 3, "nome": "Carla Souza", "email": "carla@email.com", "nota": 5.5},
    },
    "proximo_id": 4
}


def validar_aluno(dados, obrigatorio=True):
    erros = {}
    if obrigatorio or "nome" in dados:
        nome = dados.get("nome")
        if not isinstance(nome, str) or len(nome.strip()) < 2:
            erros["nome"] = "Nome deve ter ao menos 2 caracteres."
    if obrigatorio or "email" in dados:
        email = dados.get("email")
        if not isinstance(email, str) or not re.fullmatch(r"[^@\s]+@[^@\s]+\.[^@\s]+", email):
            erros["email"] = "E-mail inválido."
    if "nota" in dados:
        nota = dados["nota"]
        # bool é subclasse de int: sem essa checagem, true viraria nota 1.0
        if isinstance(nota, bool) or not isinstance(nota, (int, float)):
            erros["nota"] = "Nota deve ser um número."
        elif not 0 <= nota <= 10:
            erros["nota"] = "Nota deve ser entre 0 e 10."
    return erros


@app.route("/alunos", methods=["GET"])
def listar_alunos():
    alunos   = list(banco["alunos"].values())
    nota_min = request.args.get("nota_min", type=float)   # None se inválido
    # type=bool não serve: bool("false") é True
    aprovados = request.args.get("aprovados", "").lower() in ("1", "true", "sim")

    if nota_min is not None:
        alunos = [a for a in alunos if a["nota"] >= nota_min]
    if aprovados:
        alunos = [a for a in alunos if a["nota"] >= 6.0]

    return jsonify({
        "total":  len(alunos),
        "alunos": alunos
    })


@app.route("/alunos/<int:aluno_id>", methods=["GET"])
def buscar_aluno(aluno_id):
    aluno = banco["alunos"].get(aluno_id)
    if not aluno:
        return jsonify({"erro": "Aluno não encontrado."}), 404
    return jsonify(aluno)


@app.route("/alunos", methods=["POST"])
def criar_aluno():
    dados  = request.get_json(silent=True)
    if not isinstance(dados, dict):
        return jsonify({"erro": "Body JSON obrigatório."}), 400

    erros = validar_aluno(dados)
    if erros:
        return jsonify({"erros": erros}), 422

    aluno_id = banco["proximo_id"]
    aluno = {
        "id":         aluno_id,
        "nome":       dados["nome"],
        "email":      dados["email"],
        "nota":       float(dados.get("nota", 0.0)),
        "criado_em":  datetime.now(UTC).isoformat()
    }
    banco["alunos"][aluno_id]  = aluno
    banco["proximo_id"]       += 1
    return jsonify(aluno), 201


@app.route("/alunos/<int:aluno_id>", methods=["PATCH"])
def atualizar_aluno(aluno_id):
    aluno = banco["alunos"].get(aluno_id)
    if not aluno:
        return jsonify({"erro": "Aluno não encontrado."}), 404

    dados = request.get_json(silent=True)
    if not isinstance(dados, dict):
        return jsonify({"erro": "Body JSON obrigatório."}), 400

    erros = validar_aluno(dados, obrigatorio=False)
    if erros:
        return jsonify({"erros": erros}), 422

    if "nome"  in dados: aluno["nome"]  = dados["nome"]
    if "email" in dados: aluno["email"] = dados["email"]
    if "nota"  in dados: aluno["nota"]  = float(dados["nota"])

    return jsonify(aluno)


@app.route("/alunos/<int:aluno_id>", methods=["DELETE"])
def deletar_aluno(aluno_id):
    if aluno_id not in banco["alunos"]:
        return jsonify({"erro": "Aluno não encontrado."}), 404
    del banco["alunos"][aluno_id]
    return "", 204


@app.route("/alunos/estatisticas", methods=["GET"])
def estatisticas():
    alunos = list(banco["alunos"].values())
    if not alunos:
        return jsonify({"erro": "Nenhum aluno cadastrado."}), 404

    notas     = [a["nota"] for a in alunos]
    aprovados = [a for a in alunos if a["nota"] >= 6.0]

    return jsonify({
        "total":      len(alunos),
        "media":      round(sum(notas) / len(notas), 2),
        "maior_nota": max(notas),
        "menor_nota": min(notas),
        "aprovados":  len(aprovados),
        "reprovados": len(alunos) - len(aprovados),
    })


@app.errorhandler(404)
def nao_encontrado(e):
    return jsonify({"erro": "Rota não encontrada."}), 404

@app.errorhandler(405)
def metodo_nao_permitido(e):
    return jsonify({"erro": "Método não permitido."}), 405


if __name__ == "__main__":
    app.run(debug=True, port=5000)

Testando com curl:

# Listar alunos
curl http://localhost:5000/alunos

# Criar aluno
curl -X POST http://localhost:5000/alunos \
  -H "Content-Type: application/json" \
  -d '{"nome": "Diego Lima", "email": "diego@email.com", "nota": 8.0}'

# Buscar por ID
curl http://localhost:5000/alunos/1

# Atualizar nota
curl -X PATCH http://localhost:5000/alunos/1 \
  -H "Content-Type: application/json" \
  -d '{"nota": 10.0}'

# Estatísticas
curl http://localhost:5000/alunos/estatisticas

# Deletar
curl -X DELETE http://localhost:5000/alunos/3

Cada checagem do exemplo corresponde a uma entrada que, sem ela, passa ou derruba a rota. type=bool em request.args.get não interpreta o texto: bool("false") é True, e ?aprovados=false filtraria os aprovados do mesmo jeito que ?aprovados=true. Um corpo JSON que é lista, ou um nome numérico, derrubam com 500 uma validação que só usa len() e .get(); float(True) é 1.0, porque bool é subclasse de int, e sem a checagem de tipo true viraria nota; e procurar só o "@" aceita "@" como e-mail. O datetime.utcnow() está obsoleto desde a 3.12 e gera data sem fuso; datetime.now(UTC) sai com +00:00. E o JSON de saída do Flask vem com as chaves em ordem alfabética e, por padrão, com acentos escapados ("Jos\u00e9"); o app.json.ensure_ascii = False devolve o texto legível.

O Flask entrega pouco de propósito: uma rota vira função com @app.route, o request dá acesso ao que chegou, e a função devolve o que vai sair — um texto, um jsonify com código de status, um redirecionamento. O que ele não entrega, fica por conta de quem escreve a API, e é aí que moram os erros deste artigo. Ninguém valida o corpo da requisição: JSON que é lista, número onde se esperava texto, true onde se esperava nota. Os conversores de query string falham calados, e type=bool considera verdadeiro qualquer texto não vazio. E o get_json(), desde o Flask 2.3, recusa com 415 tudo o que não vier como JSON, o que faz de silent=True a escolha certa sempre que a rota aceita mais de um formato.

Os mecanismos de organização seguem a mesma lógica. Blueprints dividem as rotas por assunto, e o url_prefix define se o endereço canônico termina em barra. Os handlers de erro escolhem pelo tipo da exceção, então um handler de Exception sem outro de HTTPException transforma 405 em 500 e ainda devolve ao cliente a mensagem interna. A configuração por classe separa os ambientes, desde que a aplicação se recuse a subir sem SECRET_KEY em vez de descobrir a falta no primeiro login. E o debug=True fica no computador de quem desenvolve, porque a página de erro dele é um console Python aberto para o navegador.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma API de matrículas em Flask funcionou bem por meses, recebendo JSON de um aplicativo móvel. A secretaria da escola ganha um formulário HTML simples que envia para a mesma rota, e todo envio falha com 415 Unsupported Media Type — antes de qualquer linha da função ser executada, segundo o log. O desenvolvedor jura que a rota trata formulário, porque há um request.form.get(...) logo depois do request.get_json(). Explique o que acontece e como a rota deveria ler o corpo.

Ver resposta

✓ Resposta: A função é executada, mas para na primeira linha. Desde o Flask 2.3, request.get_json() sem argumentos exige o cabeçalho Content-Type: application/json; um formulário HTML chega como application/x-www-form-urlencoded (ou multipart/form-data, se tiver arquivo), e o get_json() levanta ali mesmo um erro 415. As linhas de request.form e request.files nunca são alcançadas — medido com o exemplo do artigo, tanto o formulário quanto o envio de arquivo respondiam 415. Até o Flask 2.0 o mesmo código devolvia None sem reclamar, e nas versões 2.1 e 2.2 levantava 400; o 415 atual é o mais explícito dos três. A correção é decidir pelo tipo do corpo: request.get_json(silent=True) devolve None em vez de levantar, e aí a rota cai no request.form; ou, de forma explícita, if request.is_json: ... else: .... Nos dois casos, depois de ler, é preciso validar: silent=True também devolve None para JSON malformado, e um corpo JSON pode ser uma lista em vez de um objeto — isinstance(dados, dict) antes de qualquer .get() evita um 500 com AttributeError.

Exercício 2

Um painel administrativo chama GET /alunos?aprovados=false para listar os alunos que ainda não passaram, e a tela mostra justamente os aprovados. Na mesma semana, alguém digita ?nota_min=oito e recebe a lista completa, sem erro. Os dois parâmetros são lidos com request.args.get(..., type=bool) e type=float. Explique cada comportamento e como ler parâmetros de query string com segurança.

Ver resposta

✓ Resposta: O argumento type não é um parser inteligente: o Flask chama a função passada com o texto recebido. bool("false") é True, como bool("0") e bool("não") — qualquer texto não vazio é verdadeiro —, então ?aprovados=false aplicava o filtro de aprovados; medido no exemplo, false, 0 e true devolviam os mesmos 2 alunos, e só ?aprovados= (texto vazio) devolvia os 3. Já no type=float, quando a conversão levanta ValueError, o Flask engole a exceção e devolve o default, que é None: para a rota, nota_min=oito é igual a não ter filtro. Para booleanos, a leitura certa compara o texto com uma lista de valores aceitos — request.args.get("aprovados", "").lower() in ("1", "true", "sim"). Para números, quando um valor inválido deve ser recusado e não ignorado, leia o texto e converta você mesmo, respondendo 400 no ValueError; o cliente que errou o parâmetro fica sabendo, em vez de receber uma resposta plausível e errada.

Exercício 3

Uma API tem handlers para 403 e 404 e, "para garantir", um @app.errorhandler(Exception) que devolve {"erro": str(erro), "codigo": 500}. A equipe do aplicativo reclama de dois problemas: um POST enviado por engano a uma rota só de leitura volta como erro 500, e não como erro do cliente, e num dia de falha a resposta trazia o caminho completo de um arquivo de configuração do servidor. Explique os dois e reescreva a estratégia de handlers.

Ver resposta

✓ Resposta: O Flask escolhe o handler pela classe da exceção, subindo pela hierarquia até achar um registrado. O 405 é levantado como MethodNotAllowed, uma HTTPException; não há handler para 405 nem para HTTPException, então o primeiro que casa é o de Exception — que responde com código 500 fixo. Medido: o POST numa rota GET voltou 500 com o texto 405 Method Not Allowed: ... no corpo. O mesmo acontece com 400, 415 e qualquer outro código sem handler próprio, e o handler de 500 nunca é chamado, porque o de Exception chega antes. O segundo problema é o str(erro): a mensagem de uma exceção é escrita para quem depura, não para quem usa. Um open() que falha devolveu ao cliente [Errno 2] No such file or directory: '/etc/app/senha-do-banco.txt'. A estratégia correta tem duas camadas: um @app.errorhandler(HTTPException) que responde com erro.code e erro.description, preservando o código de cada erro HTTP, e um @app.errorhandler(Exception) que chama app.logger.exception(...) — o traceback completo vai para o log — e devolve ao cliente só uma mensagem neutra com 500. Handlers específicos, como o 404 personalizado, continuam valendo porque são mais próximos da classe da exceção.

Exercício 4

Uma API em Flask roda atrás de um Nginx e registra o IP de quem faz login, para bloquear tentativas repetidas. Todos os registros mostram o mesmo IP, 10.0.0.5, e o bloqueio acaba barrando todo mundo ao mesmo tempo. Um colega sugere ler direto o cabeçalho X-Forwarded-For; outro sugere o ProxyFix com x_for=2, "para garantir". Explique o sintoma e avalie as duas sugestões.

Ver resposta

✓ Resposta: request.remote_addr é o endereço de quem abriu a conexão TCP com o Flask, e atrás de um proxy reverso esse é sempre o proxy: 10.0.0.5 é o Nginx. O IP do cliente chega no X-Forwarded-For, que o proxy acrescenta — e aí está o problema da primeira sugestão: o cliente também pode mandar esse cabeçalho, e o proxy acrescenta o IP real ao fim do que recebeu. Um atacante que envia X-Forwarded-For: 1.2.3.4 chega à aplicação com 1.2.3.4, 200.9.9.9; quem lê o primeiro valor usa o IP que o atacante escolheu, e o bloqueio por IP vira brincadeira. O ProxyFix resolve lendo da direita para a esquerda, confiando só em tantos saltos quantos forem declarados. Medido com essa mesma lista: com x_for=1, o correto para um Nginx só, remote_addr virou 200.9.9.9, o IP real; com x_for=2, virou 1.2.3.4, o forjado. Declarar proxies a mais não é "garantir", é voltar a confiar no cliente. O número tem de ser exatamente o de proxies da sua infraestrutura, e a aplicação não pode ser alcançável sem passar por eles.

Exercício 5

Uma API muda o endereço de cadastro de /cadastro para /alunos/ e, para não quebrar os clientes antigos, a rota antiga passa a fazer redirect("/alunos/", 301). Os clientes antigos param de dar erro, mas nenhum aluno novo aparece no banco, e o log mostra só requisições GET /alunos/ vindas deles. Explique o que aconteceu com os POSTs e qual código de redirecionamento deveria ter sido usado.

Ver resposta

✓ Resposta: O 301 foi definido numa época em que os clientes, na prática, trocavam o método para GET ao seguir o redirecionamento, e as bibliotecas mantêm esse comportamento: medido com o requests, um POST com corpo JSON para uma rota que responde 301 chegou ao destino como GET, com corpo vazio. O cliente recebe 200 da listagem, acha que deu certo, e o cadastro nunca acontece — daí nenhum erro e nenhum aluno novo. O código certo para mudança permanente de endereço que preserva método e corpo é o 308 (e o 307 para a temporária); com ele, o mesmo teste chegou como POST com o corpo {"nome": "Ana"} intacto. O próprio Flask usa 308 quando redireciona /alunos para /alunos/ por causa da barra final. Mesmo com o código certo, vale anunciar a mudança e acompanhar o volume da rota antiga até ele zerar: um cliente que não segue redirecionamentos, como o httpx no padrão, recebe o 308 e para ali.

Comentários

Mais em Python

Operadores e Expressões
Operadores e Expressões

Os operadores parecem a parte trivial da linguagem, e guardam algumas das…

Leitura e Escrita de Arquivos
Leitura e Escrita de Arquivos

Arquivos de texto, CSV, JSON e pickle em Python, com pathlib e o with. E os…

Funções: definição, parâmetros e escopo
Funções: definição, parâmetros e escopo

Nomear um bloco de código é o primeiro passo para escrever programas que…