Em 2003, na especificação EJB 2.1 do J2EE, a Sun Microsystems
formalizou um conceito que circulava em frameworks
proprietários havia anos: o method interceptor. A
ideia era separar de modo limpo o código de domínio de uma
classe Enterprise Java Bean das responsabilidades transversais
que cercavam cada chamada de método — abrir transação,
verificar permissão, registrar auditoria. Em vez de cada bean
duplicar esse boilerplate, o desenvolvedor escrevia uma classe
@Interceptor e anotava o método ou o bean inteiro
indicando que aquela classe devia interceder antes e depois de
cada chamada. O container EJB cuidava do resto.
Vinte e três anos depois, o termo interceptor sobreviveu em todo lugar onde aspect opera no nível de método ou operação, em oposição a middleware que opera no nível de request HTTP. JAX-RS chamou de filter; ASP.NET MVC manteve o nome filter também (ActionFilter, ResultFilter, ExceptionFilter); gRPC adotou interceptor diretamente; FastAPI esconde o conceito atrás do nome dependency mas o mecanismo é o mesmo. A diferença central em relação a middleware é granularidade: middleware atua quando a request entra no servidor; interceptor atua quando uma operação específica vai ser executada.
Essa diferença é mais consequente do que parece. Cross-cutting concerns que dependem de qual operação está sendo chamada — autorização baseada em recurso, validação específica do comando, transação por método, audit log com nome da operação — ficam mais limpos como interceptor do que como middleware. Concerns que se aplicam uniformemente a todo tráfego — correlation ID, logging de acesso, CORS — ficam mais limpos como middleware. Misturar as duas camadas, ou usar uma onde a outra serviria melhor, é uma das fontes mais comuns de duplicação e de bug em revisão de código de aplicação.
Este conceito articula a fronteira entre middleware e interceptor, mostra a anatomia de interceptor em três ambientes distintos (gRPC, ASP.NET Core, FastAPI), e enuncia a heurística de quando preferir cada um. O conceito 06 (proxy dinâmico) cobre a mecânica que torna interceptor possível em runtime; aqui o foco é o uso pragmático.
Middleware versus interceptor — onde a fronteira muda
A distinção é clara quando colocada lado a lado. Middleware opera no transporte: recebe a request HTTP/gRPC, conhece headers, body cru, status code, mas não conhece necessariamente qual handler vai ser executado. Interceptor opera na operação: conhece qual método/endpoint foi resolvido, qual o seu nome, suas anotações, seus parâmetros tipados, seu tipo de retorno. Em termos da arquitetura de pipeline, middleware é externo ao roteamento; interceptor é interno.
Há três sintomas práticos que indicam que um concern pertence
ao nível de interceptor, não de middleware. Primeiro:
a regra depende da operação invocada. "Operações que escrevem
precisam de transação; operações que leem, não" é uma regra
que precisa saber qual handler está sendo chamado.
Segundo: a regra precisa de tipos parseados.
"Validar que o id do path bate com o id
do body" é uma regra que precisa de id já
decodificado, não do JSON cru. Terceiro: a
regra precisa de metadados da operação. "Métodos anotados com
[Audit] registram em log especial" depende da
anotação visível no método, que middleware não conhece.
Há também três sintomas que indicam concern de middleware, não interceptor. Primeiro: a regra se aplica a todo tráfego do servidor, sem exceção (logging de acesso, correlation ID). Segundo: a regra opera sobre o transporte sem tocar no payload (CORS, HTTPS redirect, compression). Terceiro: a regra precisa atuar antes mesmo de o roteamento acontecer (rate limit por IP, bloqueio geográfico). Quando essas três condições se encontram, middleware é o lugar; quando as três do parágrafo anterior aparecem, interceptor é melhor.
gRPC interceptors — o caso canônico moderno
gRPC, lançado pelo Google em 2015 com base em ideias de Stubby (o RPC interno do Google desde os 2000s), tem o modelo de interceptor mais explícito e portável dos frameworks contemporâneos. A spec define interceptors em quatro variações: server-unary, server-stream, client-unary, client-stream. Cada uma envolve a chamada do método em uma cadeia composicional — interceptor recebe a chamada, escolhe pré-processar, delega para o próximo da cadeia (ou para o handler real), e opcionalmente pós-processa.
// Go — gRPC unary server interceptor
func AuthInterceptor(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
md, _ := metadata.FromIncomingContext(ctx)
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "no token")
}
user, err := validateToken(tokens[0])
if err != nil {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
ctx = context.WithValue(ctx, userKey, user)
return handler(ctx, req) // delega
}
// composição
srv := grpc.NewServer(
grpc.ChainUnaryInterceptor(
LoggingInterceptor,
TracingInterceptor,
AuthInterceptor,
RecoveryInterceptor,
),
)
Note as três informações que o interceptor recebe e que
middleware HTTP não receberia: info.FullMethod
contém o nome do método chamado (por exemplo,
/catalog.v1.CatalogService/ObterProduto);
req é a mensagem já desserializada no tipo correto
do método; e o handler que se delega é a função
tipada do método. Isso permite escrever interceptors que
decidem com base em qual operação está sendo invocada — algo
que middleware HTTP não consegue sem reproduzir o roteamento.
Para streaming (server-streaming, client-streaming,
bidirecional), o interceptor envolve um ServerStream
em vez de uma chamada unitária — pode interceptar cada
mensagem que entra ou sai. É mais complicado de escrever e
menos comum no dia a dia, mas é a única forma de aplicar
cross-cutting concerns a chamadas streaming.
ASP.NET Core — a galáxia de filters
ASP.NET MVC, desde a primeira versão em 2009, tem o conceito de filter — sucessor direto dos HTTP modules do ASP.NET clássico. Filters são interceptors que rodam no pipeline de execução de uma action (método de controller). ASP.NET Core herdou e organizou em cinco tipos, cada um com escopo de execução próprio:
AuthorizationFilter roda primeiro na cadeia,
antes de model binding. Decide se a request pode prosseguir
para a action. [Authorize] é implementado como
AuthorizationFilter por baixo.
ResourceFilter roda depois da autorização e antes do model binding. Útil para cache em nível de action e para short-circuit rápido (responder 304 Not Modified, por exemplo).
ActionFilter roda em torno do método da action em si — depois do model binding e antes da execução do método, e novamente depois de a action retornar. Lugar canônico para validação cross-cutting (FluentValidation), logging com parâmetros tipados, e abertura de transação por action.
ExceptionFilter captura exceções lançadas por
ActionFilters ou pela action. Diferente do middleware
UseExceptionHandler em escopo: filter atua
especificamente sobre actions, middleware atua sobre tudo.
ResultFilter roda em torno da execução do
IActionResult retornado, depois da action. Lugar
para mexer na response final (compactação, transformação de
payload).
// ActionFilter para auditoria
public class AuditFilter : IAsyncActionFilter
{
private readonly IAuditLog _log;
public AuditFilter(IAuditLog log) => _log = log;
public async Task OnActionExecutionAsync(
ActionExecutingContext ctx, ActionExecutionDelegate next)
{
var actionName = ctx.ActionDescriptor.DisplayName;
var user = ctx.HttpContext.User.Identity?.Name;
var args = ctx.ActionArguments;
// pré
var entry = await _log.BeginAsync(actionName, user, args);
var executed = await next(); // delega à action
// pós (mesmo se houve exceção, pega aqui via executed.Exception)
await _log.EndAsync(entry.Id, executed.Exception is null);
}
}
// uso por atributo
[AuditFilter, ServiceFilter(typeof(AuditFilter))]
[HttpPost]
public async Task<IActionResult> CriarPedido(CriarPedidoCmd cmd) { ... }
// ou globalmente
builder.Services.Configure<MvcOptions>(opts =>
opts.Filters.Add<AuditFilter>());
O acesso a ActionDescriptor, ActionArguments
tipados e HttpContext.User é o que permite ao
filter tomar decisões dependentes da operação invocada — algo
impossível ao nível de middleware.
Endpoint filters — a evolução minimalista do .NET 7+
Quando .NET 7 trouxe Minimal APIs em 2022, o time da Microsoft
introduziu Endpoint Filters — uma forma mais leve de
filter para o estilo minimalista, sem exigir
Controller. A interface é apenas
IEndpointFilter.InvokeAsync(context, next), com
composição via .AddEndpointFilter. Conceitualmente
é o mesmo que ActionFilter, mas a sintaxe é mais próxima de
middleware funcional.
app.MapPost("/pedidos", async (CriarPedidoCmd cmd, IPedidoService svc) =>
{
var p = await svc.Criar(cmd);
return Results.Created($"/pedidos/{p.Id}", p);
})
.AddEndpointFilter(async (ctx, next) =>
{
// pré
var sw = Stopwatch.StartNew();
var result = await next(ctx);
// pós
Log.Information("endpoint {Name} {Ms}ms",
ctx.HttpContext.GetEndpoint()?.DisplayName,
sw.ElapsedMilliseconds);
return result;
})
.RequireAuthorization("pedido:criar");
Endpoint filters resolvem uma incompatibilidade prática: actions em controllers herdam filtros de classe e atributos, mas Minimal APIs não têm classe. Endpoint filters dão o mesmo poder via composição funcional. Em projetos novos, a orientação atual da Microsoft é preferir Minimal APIs + Endpoint Filters; em projetos legados com controllers, ActionFilters continuam sendo a forma idiomática.
FastAPI Dependencies — interceptors com outro nome
FastAPI, criado por Sebastián Ramírez (tiangolo) em 2018, tem
um sistema de injeção de dependência que ao mesmo tempo
é mecanismo de interceptor. Cada
Depends(...) no parâmetro de um endpoint pode
executar lógica antes do handler, validar pré-condições,
lançar HTTPException para abortar, ou simplesmente
fornecer um valor pré-processado. Não há vocabulário de
"filter" ou "interceptor" — Tiangolo escolheu nomear pelo
mecanismo (DI) em vez do papel (interceptor), mas o efeito é
indistinguível.
from fastapi import FastAPI, Depends, HTTPException, Request, status
from sqlalchemy.ext.asyncio import AsyncSession
app = FastAPI()
# dependency: extrai e valida usuário a partir do JWT
async def current_user(request: Request) -> User:
token = request.headers.get("Authorization", "").removeprefix("Bearer ")
user = await validate_jwt(token)
if user is None:
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
return user
# factory: gera dependency que verifica permissão específica
def require_permission(perm: str):
async def checker(user: User = Depends(current_user)) -> User:
if perm not in user.permissions:
raise HTTPException(status.HTTP_403_FORBIDDEN)
return user
return checker
# dependency com escopo de transação (yield permite cleanup pós-handler)
async def db_tx(session: AsyncSession = Depends(get_session)):
async with session.begin():
yield session # transaction commits on success, rolls back on exception
@app.post("/pedidos")
async def criar_pedido(
cmd: CriarPedidoCmd,
user: User = Depends(require_permission("pedido:criar")),
tx: AsyncSession = Depends(db_tx),
) -> PedidoOut:
pedido = await service.criar(tx, cmd, user)
return PedidoOut.from_domain(pedido)
Três aspectos merecem nota. Primeiro:
Depends compõe — uma dependency pode depender de
outras (require_permission depende de
current_user). FastAPI resolve a árvore e cacheia
por request. Segundo: dependencies com
yield têm seção pós-handler, equivalente ao
"depois" do around advice. A transação acima abre antes do
handler, e fecha (commit ou rollback) depois — sem que o
handler precise saber. Terceiro: a aplicação
é explícita por endpoint, não global. Ao contrário de
middleware, FastAPI não tem "dependency global"; você marca
em cada endpoint quais usar. Isso é trade-off cultural — mais
verboso, mais legível.
O mesmo concern, três sabores de interceptor
Para fixar a equivalência entre os três, considere o caso canônico: autorização baseada em recurso. A regra é "quem está chamando pode operar sobre este recurso específico?". Nem middleware HTTP genérico (não conhece o recurso) nem filter de auth genérico (não conhece o ID) conseguem responder. O lugar é o interceptor da operação, onde o ID já está parseado.
public class ResourceAuthorizationFilter : IAsyncActionFilter
{
private readonly IPedidoRepository _repo;
public ResourceAuthorizationFilter(IPedidoRepository repo) => _repo = repo;
public async Task OnActionExecutionAsync(
ActionExecutingContext ctx, ActionExecutionDelegate next)
{
if (!ctx.ActionArguments.TryGetValue("id", out var idObj) ||
idObj is not Guid id)
{
ctx.Result = new BadRequestObjectResult("missing id");
return;
}
var userId = ctx.HttpContext.User.FindFirstValue("sub");
var pedido = await _repo.ObterAsync(id);
if (pedido is null) { ctx.Result = new NotFoundResult(); return; }
if (pedido.ClienteId != userId)
{
ctx.Result = new ForbidResult();
return;
}
await next();
}
}
// no controller
[HttpGet("{id}")]
[ServiceFilter(typeof(ResourceAuthorizationFilter))]
public async Task<Pedido> Obter(Guid id) => await _svc.ObterAsync(id);
Filter recebe ActionArguments["id"] já como
Guid tipado; conhece HttpContext.User;
curto-circuita escrevendo em ctx.Result sem
chamar next(). Verbosidade alta, controle
total.
def require_pedido_owner(
id: UUID,
user: User = Depends(current_user),
repo: PedidoRepo = Depends(get_repo),
) -> Pedido:
pedido = repo.obter(id)
if pedido is None:
raise HTTPException(status.HTTP_404_NOT_FOUND)
if pedido.cliente_id != user.id:
raise HTTPException(status.HTTP_403_FORBIDDEN)
return pedido
@app.get("/pedidos/{id}")
async def obter_pedido(pedido: Pedido = Depends(require_pedido_owner)) -> PedidoOut:
return PedidoOut.from_domain(pedido)
O Depends recebe o id do path
como parâmetro normal — FastAPI resolve. O dependency
retorna o próprio Pedido validado, então o
handler nem precisa carregar de novo. Lê-se quase como
documentação.
func ResourceAuthInterceptor(repo PedidoRepo) grpc.UnaryServerInterceptor {
return func(
ctx context.Context,
req any,
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (any, error) {
// só protege os métodos que precisam
if info.FullMethod != "/catalog.v1.PedidoService/Obter" {
return handler(ctx, req)
}
r, ok := req.(*pb.ObterPedidoReq)
if !ok {
return nil, status.Error(codes.Internal, "type mismatch")
}
user := userFromCtx(ctx)
p, err := repo.Obter(ctx, r.Id)
if err != nil { return nil, status.Error(codes.NotFound, "not found") }
if p.ClienteId != user.ID {
return nil, status.Error(codes.PermissionDenied, "forbidden")
}
ctx = context.WithValue(ctx, pedidoKey, p)
return handler(ctx, req)
}
}
Em Go, sem reflexão automática, é comum o interceptor
checar info.FullMethod e fazer type assertion
no req. Mais boilerplate por método, em troca
da explicitude que a comunidade prefere.
Composição — onde middleware termina e interceptor começa
Em uma aplicação realista, middleware e interceptor convivem. A regra prática que se vê em times maduros é uma divisão por camada de concern.
Middleware cuida de tudo que é orthogonal ao roteamento e à operação: correlation ID, logging de acesso, compressão, CORS, autenticação inicial (validar que o token é bem-formado e o usuário existe), métrica de duração de request, recoverer/exception handler. Esse pacote roda no mesmo formato para todo tráfego do servidor, sem distinguir operação.
Interceptor cuida de tudo que depende da operação: autorização baseada em recurso, validação de input tipado, transação por método, audit log com nome da operação, retry/cache específico de método, instrumentação por operação. Esse pacote depende de saber qual handler vai executar e quais são seus parâmetros.
Quando alguém pergunta em revisão "isso devia ser middleware ou interceptor?", a pergunta correta de volta é "essa regra depende de qual operação está sendo chamada?". Se sim, interceptor. Se não, middleware. Casos limítrofes existem — rate limit pode ser global (middleware) ou por usuário autenticado e por operação (interceptor) —, e aí a decisão vira de design, não de regra mecânica.
Duplicar autorização em middleware e em interceptor.
Aparece quando o time evolui o sistema sem articular a
divisão: alguém adiciona "middleware de auth" para o
servidor inteiro, e mais tarde alguém adiciona
[Authorize(Policy = "X")] em controllers, e
ninguém remove o redundante. O sintoma é que a mesma regra
está em dois lugares, com o risco de divergir — pior, com o
risco de uma camada autorizar e outra não, gerando 401 ou
403 mesmo quando a regra original aceitaria. Acerte a
divisão de uma vez: autenticação no middleware (todos
precisam estar logados), autorização por recurso no
interceptor (só quem é dono do recurso pode operar).
Anti-padrões frequentes
Interceptor para regra local. Promover a filter algo que se aplica a um único método é caro: o leitor precisa procurar em outro arquivo o que executa antes da action. Se a regra é local, deixa local. Filter ganha valor quando aplica a muitos métodos com a mesma política.
Filter com lógica de negócio. Filter que calcula imposto, decide preço, ou aplica regra de domínio espalha o domínio numa camada que devia ser plumbing. O sintoma é "para mudar a fórmula de frete eu preciso mexer no ActionFilter X". Toda regra de domínio mora no domínio; filter aplica plumbing, não regra.
Ordem de filters não articulada. Quando vários
filters se aplicam a um método, a ordem entre eles vira
crítica e raramente é documentada. ASP.NET Core resolve com a
propriedade Order em
IOrderedFilter, mas a maioria dos times nunca
usa. Caminho prático: filters globais primeiro, controller
filters depois, action filters por último; se precisar de
ordem específica entre filters da mesma escopo, articulá-la
em comentário no código.
Filter criado para evitar middleware "complicado".
Em times menos experientes, filter aparece como atalho para
"não mexer no Program.cs". É decisão por
conveniência, não por design. Vale rever — concerns de borda
pertencem a middleware mesmo que o setup seja mais cerimonial.
Antes de escolher entre middleware e interceptor, escreva a regra do concern numa frase: "para X, o sistema deve Y, antes/depois de Z". Se a frase precisa mencionar o nome de uma operação ou de um recurso para fazer sentido, é interceptor. Se a frase fala só de request HTTP genérica, é middleware. Em casos onde a frase cabe nas duas formas, prefira middleware — sai mais barato em complexidade e fica mais explícito.
Por que importa para a sua carreira
A pergunta "isso é middleware ou filter?" aparece em quase toda
revisão de PR não-trivial em aplicação web sênior. Quem não
enxerga a distinção tende a empilhar tudo em uma camada —
geralmente middleware — produzindo middleware gigantesco
cheio de if endpoint == "...":. Quem enxerga
consegue articular o critério, propor a divisão certa, e
explicar o porquê. Em entrevista de design, a pergunta "como
você organizaria autorização em uma API com cinquenta
endpoints sendo dez deles públicos?" é convite direto para
essa discussão — middleware autentica, interceptor por
recurso autoriza, e dez endpoints recebem
[AllowAnonymous]. A resposta forte mostra que a
pessoa entendeu não só a sintaxe, mas a topologia.
Como praticar
- Tradução middleware ↔ interceptor. Pegue um sistema seu (ou aberto) e faça inventário de todos os cross-cutting concerns. Para cada um, classifique: middleware ou interceptor? Justifique com a heurística acima. Identifique pelo menos um caso onde a escolha atual está errada (por conveniência ou inércia) e proponha a mudança em um PR descritivo, mesmo que você não vá implementar.
-
Implementação de autorização por recurso.
Implemente em ASP.NET Core (com ActionFilter), FastAPI (com
Depends factory), e Go gRPC (com interceptor) o mesmo
cenário: endpoint
GET /pedidos/{id}só retorna se o pedido pertence ao usuário autenticado. Compare a forma como cada framework deixa explícita a regra. Qual leitura é mais clara? Onde fica mais fácil de errar? -
Anatomia de um framework de filter. Leia o
código fonte do
FilterPipelineBuilderdo ASP.NET Core (no repo dotnet/aspnetcore) ou dosolve_dependenciesdo FastAPI. Identifique onde o framework decide a ordem de execução, como propaga contexto, e como trata exceção em filter. Documente em meia página o que aprendeu — esse é o tipo de leitura que diferencia engenheiros sêniores de plenos.
Referências para aprofundar
- docs ASP.NET Core — Filters in ASP.NET Core.
- docs ASP.NET Core — Filters in Minimal API apps (Endpoint filters).
- docs gRPC — Interceptors.
- docs FastAPI — Dependencies (tutorial completo).
- livro ASP.NET Core in Action (3ª ed.) — Andrew Lock (Manning, 2024).
- livro gRPC: Up and Running — Kasun Indrasiri & Danesh Kuruppu (O'Reilly, 2020).
- livro FastAPI — Bill Lubanovic (O'Reilly, 2023).
- artigo JSR 318 — Interceptors 1.2 specification (Java EE, 2013).
- artigo JAX-RS Filters and Interceptors — Pavel Bucek (Oracle Technical Article, 2014).
- artigo Choosing between middleware, filters and DI in ASP.NET Core — Steve Gordon (blog, 2020).
- artigo FastAPI's Depends explained from first principles — Sebastián Ramírez (palestras 2020+).
- vídeo From Middleware to Endpoint Filters in .NET 7 — Maria Naggaga (.NET Conf, 2022).