Em fevereiro de 2007, Christian Neukirchen publicou no blog dele um
texto curto que apresentava uma especificação de duas páginas
chamada Rack. A proposta era trivial: padronizar
a interface entre servidores web Ruby (Mongrel, WEBrick, Thin) e
frameworks Ruby (Rails, Sinatra, Camping). Cada framework tinha
até então conector próprio para cada servidor, e a combinatória
virava insustentável. Rack definiu uma convenção minimalista —
uma aplicação Rack é um objeto que responde a call(env)
e devolve [status, headers, body]. Em três anos, todo
framework Ruby falava Rack; em cinco, a ideia tinha viajado para
Python (WSGI já existia, mas o estilo composicional Rack
influenciou Werkzeug e Django middleware), JavaScript (Connect,
depois Express), .NET (OWIN, depois Katana, depois ASP.NET Core),
e Go (a interface http.Handler de 2009 já tinha
essa filosofia desde a primeira hora).
O que viajou junto com Rack não foi só a interface. Foi também
o padrão de empilhamento — middleware, no vocabulário
que ficou. Cada camada da pilha recebe a request, opcionalmente
transforma, delega para a próxima camada via uma função
chamada next (ou call, ou
ServeHTTP), e opcionalmente transforma o retorno.
A pilha inteira é montada uma vez no startup, e cada request
atravessa toda ela na ida e na volta. É decorator do conceito
anterior, aplicado ao caso particular de processamento HTTP.
Em 2026, a convenção é tão universal que parece ter sido
sempre assim. ASP.NET Core, FastAPI, Express, chi, gin,
actix-web, Phoenix em Elixir, Spring WebFlux, Axum em Rust —
todos usam a mesma estrutura de pipeline com next.
Vale entender o porquê do sucesso e os detalhes que a literatura
casual ignora — especialmente ordem de empilhamento, fluxo de
retorno, e short-circuit, que são onde mais se erra na prática.
Este conceito formaliza o middleware como caso particular de decorator e mostra a anatomia em três frameworks contemporâneos. Os concerns específicos que mais aparecem em pipelines — logging, observabilidade, auth, retry, cache — vêm nos conceitos seguintes; aqui o foco é o pipeline em si, sua mecânica e suas armadilhas.
O onion model — request entrando, response saindo
A imagem mais usada para descrever middleware é a da cebola.
Imagine uma cebola com várias camadas concêntricas. A request
chega na camada mais externa, atravessa as camadas em direção
ao centro, atinge o handler de domínio (o "miolo"), e a
response viaja de volta atravessando as mesmas camadas em
ordem inversa. Cada camada — cada middleware — pode atuar tanto
na ida (antes de chamar next) quanto na volta
(depois de next retornar).
Essa estrutura tem três consequências práticas que merecem atenção. Primeira: ordem importa, e a ordem é simétrica — o que entra primeiro sai por último. Um middleware de logging registrado antes do middleware de auth vê requests não-autenticadas; registrado depois, só vê as que passaram pela auth. Isso é decisão de design, não detalhe de implementação.
Segunda: cada camada pode interromper o fluxo
retornando sem chamar next. Esse é o short-circuit
— middleware de auth retorna 401 direto sem
delegar para a camada interna; middleware de cache retorna
response cacheada sem reexecutar o handler; rate limiter
retorna 429 e nunca toca o domínio. Short-circuit é
o que diferencia middleware de chain genérico — não é só
transformar dado e passar adiante, é poder não passar adiante.
Terceira: o que acontece depois de
next só executa se o miolo (e todas as camadas
internas) retornarem sem exceção não-tratada. Isso significa
que o lugar certo para tratar erro genericamente é uma camada
externa que envolve next em
try, e o lugar certo para registrar duração total
é o middleware mais externo possível. Posicionamento errado
desse tipo de concern produz métrica que não captura erro, ou
log de erro que vaza por cima do tratamento genérico.
A mecânica em ASP.NET Core
ASP.NET Core formalizou middleware na sua reescrita de 2016
após a abordagem OWIN/Katana de 2013. O modelo central é uma
RequestDelegate — função que recebe
HttpContext e retorna Task. Cada
middleware é uma função que recebe a próxima
RequestDelegate da cadeia e retorna outra que
compõe comportamento próprio com a delegação.
// Program.cs — ASP.NET Core 8/9/10
var app = WebApplication.CreateBuilder(args).Build();
// middleware "lambda" inline
app.Use(async (HttpContext ctx, RequestDelegate next) => {
var sw = Stopwatch.StartNew();
await next(ctx); // delega para o próximo
var ms = sw.ElapsedMilliseconds;
app.Logger.LogInformation(
"request {Method} {Path} {Status} {Ms}ms",
ctx.Request.Method, ctx.Request.Path,
ctx.Response.StatusCode, ms);
});
// middleware com classe + UseMiddleware (preferível em produção)
app.UseMiddleware<CorrelationIdMiddleware>();
app.UseMiddleware<ExceptionHandlerMiddleware>();
// middleware embutido do framework
app.UseAuthentication();
app.UseAuthorization();
// roteamento e endpoints (o "miolo")
app.MapControllers();
await app.RunAsync();
A ordem em que se chama app.Use é a ordem de
empilhamento. ASP.NET Core documenta uma ordem recomendada
explicitamente — exception handler primeiro (mais externo),
depois HSTS, HTTPS redirection, static files, routing, CORS,
authentication, authorization, custom, endpoint. Cada item
dessa lista tem motivo: exception handler precisa estar fora
para capturar tudo; static files precisa estar antes de
routing para não rodar pipeline completo para arquivo
estático; CORS antes de auth porque OPTIONS preflight não tem
credencial; auth antes de authorization porque você não pode
autorizar sem identificar.
Em produção, prefere-se classe sobre lambda — torna o
middleware testável isoladamente, injeta dependências via
construtor, e permite reutilização. A interface
IMiddleware existe para isso desde 2.1, mas
muitos times usam o padrão "convencional" (classe com método
InvokeAsync), que é mais flexível porque permite
DI per-request.
ASGI e o middleware Python moderno
Python tem uma história mais sinuosa. WSGI (PEP 3333, 2010, revisão de PEP 333 de 2003) foi a primeira convenção composicional pythônica e influenciou Rack — mas WSGI é síncrono e não suporta WebSocket nem long polling. ASGI, especificado por Andrew Godwin entre 2016 e 2019 (versão 3.0 atual), é o sucessor async-first. Frameworks modernos — FastAPI, Starlette, Django desde 3.0 — falam ASGI nativo.
Middleware ASGI segue a mesma estrutura de Rack/WSGI: um
callable que recebe scope, receive,
send e a próxima aplicação ASGI. FastAPI dá
sintaxe mais alta — você registra middleware via
app.middleware("http") ou
app.add_middleware(...), e o framework lida com
o protocolo ASGI por baixo.
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from starlette.middleware.base import BaseHTTPMiddleware
import time
import structlog
log = structlog.get_logger()
app = FastAPI()
# middleware via decorator (sintaxe FastAPI)
@app.middleware("http")
async def timing_middleware(request: Request, call_next):
started = time.perf_counter()
response = await call_next(request) # equivalente a next()
elapsed_ms = (time.perf_counter() - started) * 1000
log.info(
"request",
method=request.method,
path=request.url.path,
status=response.status_code,
duration_ms=round(elapsed_ms, 2),
)
return response
# middleware via classe ASGI (mais portável entre frameworks)
class CorrelationIdMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
cid = request.headers.get("X-Correlation-ID") or new_id()
request.state.correlation_id = cid
response = await call_next(request)
response.headers["X-Correlation-ID"] = cid
return response
# composição (ordem de add é ordem de empilhamento — primeiro adicionado = mais externo)
app.add_middleware(CorrelationIdMiddleware)
app.add_middleware(CORSMiddleware, allow_origins=["*"])
Atenção a uma sutileza importante de FastAPI/Starlette:
add_middleware empilha em ordem inversa à intuição.
O primeiro middleware adicionado é o mais externo, e
o último adicionado é o mais próximo do handler. Isso
bate com o modelo de Starlette por baixo (cada middleware
"embrulha" a app que já existia), mas confunde quem vem do
ASP.NET Core, onde a ordem de Use é a ordem em
que a request encontra cada camada. Documentação interna que
explicita a ordem efetiva é prática que evita bug.
Go — a convenção func(http.Handler) http.Handler
Go nunca teve framework HTTP dominante porque a biblioteca
padrão entrega a parte difícil. net/http define
http.Handler como interface mínima, e a
comunidade convergiu na convenção
func(http.Handler) http.Handler para middleware
— exatamente o decorator do conceito anterior aplicado ao
caso HTTP. Bibliotecas como chi, gorilla/mux
e echo só fornecem açúcar para essa convenção:
router com Use para registrar middleware,
sub-routers para escopos diferentes, e helpers para erro e
logging.
package main
import (
"context"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
type ctxKey string
const correlationKey ctxKey = "correlation_id"
// middleware: correlation id
func WithCorrelationID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
cid := r.Header.Get("X-Correlation-ID")
if cid == "" {
cid = uuid.NewString()
}
ctx := context.WithValue(r.Context(), correlationKey, cid)
w.Header().Set("X-Correlation-ID", cid)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// middleware: timing + structured log
func WithTiming(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
rw := &statusRecorder{ResponseWriter: w, status: 200}
started := time.Now()
next.ServeHTTP(rw, r)
slog.InfoContext(r.Context(), "request",
"method", r.Method,
"path", r.URL.Path,
"status", rw.status,
"duration_ms", time.Since(started).Milliseconds(),
)
})
}
type statusRecorder struct {
http.ResponseWriter
status int
}
func (s *statusRecorder) WriteHeader(code int) {
s.status = code
s.ResponseWriter.WriteHeader(code)
}
func main() {
r := chi.NewRouter()
r.Use(WithCorrelationID)
r.Use(WithTiming)
// outros middlewares...
r.Get("/health", func(w http.ResponseWriter, _ *http.Request) {
w.Write([]byte(`{"status":"ok"}`))
})
http.ListenAndServe(":8080", r)
}
Note três detalhes idiomáticos. Primeiro: para
capturar o status code da response (que http.ResponseWriter
não expõe diretamente), envolve-se a writer em um
statusRecorder — um decorator de
ResponseWriter, decorator dentro de decorator.
Segundo: para passar correlation ID adiante,
usa-se context.Context — o conceito 13 do módulo
anterior cobre isso em profundidade. Terceiro:
ordem de r.Use é ordem de empilhamento "do
externo para o interno", como em ASP.NET Core (e diferente
de FastAPI). Cultura local.
Short-circuit — quando next não é chamado
Short-circuit é o que torna middleware útil para concerns de segurança e otimização. Os três casos canônicos:
Auth e autorização: middleware verifica token,
se inválido escreve 401 ou 403 e
retorna sem chamar next. O handler de domínio
jamais é executado, e — crucialmente — middlewares posteriores
ao auth também não. Se o middleware de log de payload está
depois do auth, requests não-autenticadas não vazam
payload em log; se está antes, vazam.
Cache de response: middleware verifica chave
do cache; se hit, escreve a response cacheada e retorna sem
chamar next. O handler interno e todos os
middlewares posteriores ao cache não rodam. Isso é vantagem —
eficiência — mas é também perigo: métricas que dependem de
executar o handler ficam zeradas para hit de cache. Daí a
escolha do conceito anterior sobre ordem de
Logging(Cached(...)) versus
Cached(Logging(...)).
Rate limiting: middleware verifica taxa por
cliente; se excedida, escreve 429 e retorna. A
mesma lógica de short-circuit. Aqui o cuidado é não rate-limitar
por cliente errado — rate limiter antes do
middleware que extrai identidade limita por IP; depois,
por usuário autenticado.
Tratamento de erro — o middleware mais externo
A regra prática vinda de quase qualquer framework: middleware de
tratamento de erro vai na camada mais externa do pipeline. A
razão é o onion model — só o try mais externo
captura exceção que escapou de qualquer middleware interno. Se
o handler de erro está em uma camada interna, exceções
lançadas por middleware externo a ele jamais são tratadas.
A forma canônica em ASP.NET Core é
app.UseExceptionHandler(...) como primeira chamada.
Em FastAPI, é app.add_exception_handler(...) —
Starlette posiciona automaticamente como mais externo. Em Go
com chi, é convenção registrar o middleware de
recover/error-mapper como o primeiro r.Use. O
objetivo é o mesmo em todos: garantir que qualquer caminho
de exceção, em qualquer middleware ou handler, resulte em
response HTTP bem-formada com status apropriado, sem vazar
stack trace.
Middleware com efeito colateral entre requests. Um caso recorrente é middleware que mantém estado em variável global — contador, mapa de cache, lista. Em servidor concorrente, várias goroutines/threads/tasks executam o middleware simultaneamente, e o estado vira race condition. O sintoma é log esporádico estranho ou métrica que zera sozinha. Regra: middleware deve ser stateless ou usar estrutura concorrente explícita (sync.Map em Go, ConcurrentDictionary em C#, threading.Lock em Python). E nunca, jamais, escrever em variável compartilhada sem sincronização achando que "request é rápida demais para colidir".
O mesmo pipeline, três sintaxes
Para concretizar a equivalência, considere um pipeline mínimo com três middlewares: correlation ID, timing/log, error handler. As três versões abaixo fazem a mesma coisa em três ecossistemas, e dá para ler em paralelo.
var app = builder.Build();
// camada mais externa (registra primeiro)
app.UseExceptionHandler(errorApp => {
errorApp.Run(async ctx => {
ctx.Response.StatusCode = 500;
await ctx.Response.WriteAsJsonAsync(new { error = "internal" });
});
});
app.Use(async (ctx, next) => { // correlation ID
var cid = ctx.Request.Headers["X-Correlation-ID"]
.FirstOrDefault() ?? Guid.NewGuid().ToString();
ctx.Response.Headers["X-Correlation-ID"] = cid;
using (LogContext.PushProperty("correlation_id", cid))
await next();
});
app.Use(async (ctx, next) => { // timing
var sw = Stopwatch.StartNew();
await next();
Log.Information("request {Method} {Path} {Status} {Ms}ms",
ctx.Request.Method, ctx.Request.Path,
ctx.Response.StatusCode, sw.ElapsedMilliseconds);
});
app.MapControllers();
await app.RunAsync();
Ordem natural: Use chamado primeiro = camada
mais externa. Exception handler é o primeiro porque deve
envolver tudo. Correlation ID antes de timing porque o
log de timing precisa ter o ID em escopo.
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
import time, structlog, uuid
log = structlog.get_logger()
app = FastAPI()
class TimingMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
started = time.perf_counter()
response = await call_next(request)
log.info("request",
method=request.method, path=request.url.path,
status=response.status_code,
duration_ms=round((time.perf_counter() - started) * 1000, 2))
return response
class CorrelationIdMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
cid = request.headers.get("X-Correlation-ID") or str(uuid.uuid4())
structlog.contextvars.bind_contextvars(correlation_id=cid)
try:
response = await call_next(request)
finally:
structlog.contextvars.clear_contextvars()
response.headers["X-Correlation-ID"] = cid
return response
# último add = mais interno; primeiro add = mais externo
app.add_middleware(TimingMiddleware) # mais interno dos dois
app.add_middleware(CorrelationIdMiddleware) # mais externo
@app.exception_handler(Exception)
async def all_errors(request, exc):
return JSONResponse(status_code=500, content={"error": "internal"})
FastAPI/Starlette empilham na ordem inversa de
add_middleware. Para garantir que o
correlation ID está em escopo durante o timing, ele precisa
ser adicionado depois do timing — porque vira a
camada externa. Isso é peculiaridade que merece comentário
no código.
r := chi.NewRouter()
r.Use(middleware.Recoverer) // mais externo: captura panics
r.Use(WithCorrelationID)
r.Use(WithTiming)
r.Get("/produtos/{id}", h.Obter)
http.ListenAndServe(":8080", r)
chi tem ordem natural: primeiro Use é mais
externo. middleware.Recoverer da própria chi
captura panic e converte em 500 —
equivalente ao exception handler. Para tipos de erro
específicos (404, 409), em Go a
convenção é o handler retornar erro tipado e um middleware
mapper traduzir, em vez de exceção.
Testando middleware isoladamente
Middleware bem desenhado é testável fora do framework HTTP. A
técnica em todas as linguagens é a mesma: passar um handler
"fake" como next, montar um request sintético, e
verificar comportamento — status retornado, headers
manipulados, log emitido, contexto propagado.
// Go — teste isolado de WithCorrelationID
func TestCorrelationIDMiddleware_addsHeader(t *testing.T) {
var receivedCtxID string
fakeNext := http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
receivedCtxID = r.Context().Value(correlationKey).(string)
})
handler := WithCorrelationID(fakeNext)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/x", nil)
handler.ServeHTTP(rec, req)
if rec.Header().Get("X-Correlation-ID") == "" {
t.Fatal("expected X-Correlation-ID header to be set")
}
if receivedCtxID == "" {
t.Fatal("expected correlation id propagated via context")
}
}
A virtude da estrutura: WithCorrelationID não
depende de chi, de servidor, de banco. É função pura sobre
http.Handler. A mesma estética vale em ASP.NET
Core (testar via HttpContext sintético ou
WebApplicationFactory) e em FastAPI (testar via
TestClient ou montando BaseHTTPMiddleware
diretamente). Middleware que precisa de "rodar o servidor"
para ser testado é sinal de design ruim.
Anti-padrões frequentes
Middleware blocante em runtime async. Em
FastAPI, registrar middleware que faz chamada síncrona de I/O
(request HTTP bloqueante, query síncrona ao banco) bloqueia o
event loop e prejudica todas as requests concorrentes.
Especialmente comum em projetos migrando de Flask. Solução:
sempre usar versão async dos clients, e quando absolutamente
necessário, embrulhar em asyncio.to_thread.
Middleware que lê o body sem reescrevê-lo. O
body de uma request HTTP é stream consumível uma vez. Se um
middleware lê o body para inspecioná-lo (logging, validação,
hashing) e não o reinjeta, o handler interno encontra body
vazio. ASP.NET Core tem EnableBuffering() para
isso; FastAPI documenta o problema e oferece
request._body como cache; em Go, é preciso
substituir r.Body por um io.NopCloser
sobre os bytes lidos.
Middleware muito grande. Quando um middleware passa de cinquenta linhas, geralmente está fazendo várias coisas — auth + load do usuário + verificação de feature flag + logging em uma só camada. Quebra em vários middlewares pequenos torna a pilha legível e testável; o custo de cada middleware adicional é mínimo, e o ganho de clareza é alto.
Ordem decidida por trial-and-error. Em times
onde ninguém articula a ordem do pipeline, ela vira folclore:
"mexe e roda os testes pra ver". Times maduros têm um
diagrama do pipeline em algum
docs/architecture.md com cada middleware e a
justificativa da posição. Investimento de uma hora que paga
anos.
A ordem do pipeline conta uma história sobre o sistema. Recoverer/exception-handler é o primeiro porque o sistema nunca pode vazar erro cru. Correlation ID vem cedo porque tudo depois precisa carregar o ID. Auth vem antes de autorização, antes de qualquer log de payload, antes de qualquer endpoint de domínio. Métricas e tracing vêm na camada que mede o que importa — geralmente perto do roteamento, mas dentro da auth para não medir requests não-autenticadas como tráfego válido. Cada decisão é articulável. Se você não consegue justificar a posição de cada middleware em uma frase, a ordem está acidental.
Por que importa para a sua carreira
Pipeline de middleware é a primeira coisa que se lê em um sistema novo de aplicação web. Quem entende a estrutura consegue mapear em poucos minutos quais cross-cutting concerns a equipe escolheu, em que ordem aplicou, e onde estão os pontos de extensão. Em entrevista de design, "como você organizaria o pipeline HTTP de uma API nova?" é uma das perguntas mais comuns para vagas backend, e a resposta forte tem três camadas: enumera os concerns (auth, log, métrica, cache, error mapping, rate limit), justifica a ordem com base no onion model, e menciona pelo menos um caso de short-circuit como ferramenta. Em revisão de código, ver pipeline com ordem acidental e propor a reordenação justificada é um dos atos arquiteturais mais altos por unidade de esforço — pequena mudança, alto impacto.
Como praticar
-
Pipeline mínimo nas três linguagens. Monte
em ASP.NET Core, FastAPI e Go (com chi) o mesmo pipeline:
recoverer/exception, correlation ID, timing+log, auth fake
(qualquer header
X-Userautentica), rate limit simples (10 req/s por IP). Documente em comentário ou README a ordem escolhida e o motivo de cada posição. Compare quão verbosa fica cada implementação — esse é o exercício que faz você sentir a cultura de cada ecossistema. - Diagnóstico de pipeline existente. Pegue um projeto seu (ou um aberto que você usa) com pipeline HTTP de cinco ou mais middlewares. Liste todos em ordem. Para cada um, escreva: o que faz, por que está nessa posição, o que aconteceria se fosse movido uma posição para cima/baixo. Identifique pelo menos um caso onde a ordem está justificada por inércia, não por design. Proponha a mudança.
-
Teste isolado de middleware. Escolha um
middleware não-trivial — algo que faça mais que adicionar
header, idealmente algo que dependa do contexto da request
(validação de token, rate limiter por usuário). Escreva
teste unitário sem subir o servidor, fornecendo um
nextfake e uma request sintética. Cubra: caso de sucesso, short-circuit (quando o middleware não chamanext), e caminho de erro (quando onextlança exceção). Mais que 80% de cobertura nesse tipo de middleware é mensagem clara de boa decomposição.
Referências para aprofundar
- artigo Introducing Rack — Christian Neukirchen (2007).
- docs ASP.NET Core Middleware.
- docs ASGI Specification 3.0 — Andrew Godwin (2019).
- docs FastAPI — Middleware.
- docs chi — composable router for HTTP services.
- livro ASP.NET Core in Action (3ª ed.) — Andrew Lock (Manning, 2024).
- livro Architecture Patterns with Python — Harry Percival & Bob Gregory (O'Reilly, 2020).
- livro Let's Go Further — Alex Edwards (auto-publicado, 2024).
- artigo The OWIN Specification — Microsoft / community (2013).
- artigo How does ASP.NET Core middleware really work? — Andrew Lock (blog, 2019).
- artigo Why Express middleware is awesome — Evan Hahn (2014).
- vídeo Mat Ryer — How I write HTTP services in Go (Gophercon, 2019; revisado 2024).