Pular para o conteúdo

    Capítulo 14, Intermediário

    Middleware

    O middleware é uma camada que **toda** requisição atravessa, na ida e na volta. É o lugar certo para o que vale para a aplicação inteira: medir tempo, identificar a requisição, compactar respostas.

    Como funciona

    Um middleware recebe a requisição antes de ela chegar à rota e a resposta depois de a rota responder. O que vem antes do await call_next(request) roda na ida, e o que vem depois roda na volta. O call_next passa a requisição adiante:

    intermediario/cap14_middleware.pylinhas 10 a 29
    import time
    import uuid
    
    from fastapi import FastAPI, Request
    from fastapi.testclient import TestClient
    
    app = FastAPI()
    
    
    @app.middleware("http")
    async def medir_tempo(request: Request, call_next):
        inicio = time.perf_counter()
        resposta = await call_next(request)
        resposta.headers["X-Process-Time"] = f"{time.perf_counter() - inicio:.6f}"
        return resposta
    
    
    @app.get("/")
    def home():
        return {"mensagem": "ok"}
    

    O middleware mede o tempo gasto desde que a requisição entra até a resposta sair, e o devolve em um cabeçalho. Qualquer rota da aplicação ganha isso sem mudar uma linha. O middleware é async def porque roda no laço de eventos de todas as requisições.

    Identificar cada requisição

    Um identificador de requisição deixa você ligar uma reclamação do cliente aos logs do servidor. O middleware cria um (ou reaproveita o que veio de um proxy), guarda em request.state para a rota usar e o devolve no cabeçalho:

    intermediario/cap14_middleware.pylinhas 34 a 53
    @app.middleware("http")
    async def id_da_requisicao(request: Request, call_next):
        request.state.request_id = request.headers.get("x-request-id") or str(uuid.uuid4())
        resposta = await call_next(request)
        resposta.headers["X-Request-ID"] = request.state.request_id
        return resposta
    
    
    @app.get("/quem-sou")
    def quem_sou(request: Request):
        return {"request_id": request.state.request_id}
    
    
    cliente = TestClient(app)
    resposta = cliente.get("/")
    print(resposta.status_code, float(resposta.headers["x-process-time"]) >= 0)
    
    sem_id = cliente.get("/quem-sou")
    print(len(sem_id.headers["x-request-id"]), sem_id.json()["request_id"] == sem_id.headers["x-request-id"])
    print(cliente.get("/quem-sou", headers={"x-request-id": "abc-123"}).json())
    
    Saída
    200 True
    36 True
    {'request_id': 'abc-123'}
    

    Os dois middlewares agem juntos: a rota / ganhou o cabeçalho de tempo, e a /quem-sou leu o identificador que o outro middleware deixou em request.state.

    Middlewares precisam estar prontos antes da primeira requisição

    Se você tentar adicionar um middleware depois de a aplicação já ter respondido a uma requisição, o Starlette recusa com RuntimeError: Cannot add middleware after an application has started. Em um programa real isso não aparece, porque tudo é declarado quando o módulo é importado. Por isso, neste capítulo, o TestClient só é criado depois de os middlewares estarem declarados.

    A ordem dos middlewares

    Quando há vários, o último registrado fica por fora: ele é o primeiro a ver a requisição e o último a ver a resposta. Dá para ver o desenho:

    intermediario/cap14_middleware.pylinhas 58 a 85
    ordem = []
    app_ordem = FastAPI()
    
    
    @app_ordem.middleware("http")
    async def primeiro(request: Request, call_next):
        ordem.append("primeiro: ida")
        resposta = await call_next(request)
        ordem.append("primeiro: volta")
        return resposta
    
    
    @app_ordem.middleware("http")
    async def segundo(request: Request, call_next):
        ordem.append("segundo: ida")
        resposta = await call_next(request)
        ordem.append("segundo: volta")
        return resposta
    
    
    @app_ordem.get("/")
    def raiz():
        ordem.append("rota")
        return {}
    
    
    TestClient(app_ordem).get("/")
    print(ordem)
    
    Saída
    ['segundo: ida', 'primeiro: ida', 'rota', 'primeiro: volta', 'segundo: volta']
    

    Middlewares que já vêm prontos

    O FastAPI inclui os mais comuns. O GZipMiddleware, por exemplo, comprime respostas maiores que um tamanho mínimo, quando o cliente diz que aceita:

    intermediario/cap14_middleware.pylinhas 90 a 108
    from fastapi.middleware.gzip import GZipMiddleware
    
    app_gzip = FastAPI()
    app_gzip.add_middleware(GZipMiddleware, minimum_size=500)
    
    
    @app_gzip.get("/grande")
    def grande():
        return {"texto": "repetição " * 500}
    
    
    @app_gzip.get("/pequena")
    def pequena():
        return {"texto": "oi"}
    
    
    gz = TestClient(app_gzip)
    print(gz.get("/grande", headers={"accept-encoding": "gzip"}).headers.get("content-encoding"))
    print(gz.get("/pequena", headers={"accept-encoding": "gzip"}).headers.get("content-encoding"))
    
    Saída
    gzip
    None
    

    Outros que você vai usar: o CORSMiddleware (capítulo 22) e o TrustedHostMiddleware, que recusa requisições com um cabeçalho Host inesperado.

    Middleware, dependência e tratador

    MiddlewareDependênciaTratador de exceção
    AlcanceToda requisiçãoAs rotas que a pedemQuando uma exceção específica ocorre
    Vê a respostaSimNãoCria a resposta de erro
    Bom paraTempo, ID de requisição, compressão, cabeçalhosAutenticação, banco, validações por rotaFormato único de erro
    Recebe parâmetros da rotaNãoSim, com validaçãoNão

    Cuidado com o que o middleware faz

    O middleware roda em todas as requisições, inclusive nas de /docs. Trabalho lento ou bloqueante nele atrasa a aplicação inteira. E o @app.middleware("http") é construído sobre BaseHTTPMiddleware, que tem limitações com respostas em streaming. Para o caminho crítico de aplicações muito carregadas, existe o middleware ASGI puro, mais complexo, mas sem esse custo.

    Exercício 1

    Um cabeçalho de versão

    Crie um middleware que acrescente X-App-Version: 1.0 a todas as respostas, e duas rotas para conferir.