MÓDULO 05 · CONCEITO 05 DE 14

Interceptors & filters

A camada de aspect que opera no nível do método, não do request. EJB interceptors, JAX-RS filters, gRPC interceptors, ActionFilters, Endpoint filters, FastAPI Depends — quando granularidade fina importa e por quê.

Tempo de leitura ~22 min Pré-requisito Conceito 04 (middleware HTTP) Próximo Proxy dinâmico e weaving

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.

C# — ActionFilter com acesso a parâmetros tipados
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.

Python — Depends com factory por recurso
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.

Go — gRPC interceptor consultando repositório
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.

armadilha em produção

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.

heurística do sênior

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

  1. 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.
  2. 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?
  3. Anatomia de um framework de filter. Leia o código fonte do FilterPipelineBuilder do ASP.NET Core (no repo dotnet/aspnetcore) ou do solve_dependencies do 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

  1. docs ASP.NET Core — Filters in ASP.NET Core. learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters — Documentação canônica dos cinco tipos de filter, ordem de execução, e injeção de dependência. Atualizada a cada release. Leitura obrigatória para .NET sênior.
  2. docs ASP.NET Core — Filters in Minimal API apps (Endpoint filters). learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/min-api-filters — A introdução oficial a endpoint filters. Mais curta que a de filters de MVC, mostra a forma minimalista que é o presente do framework.
  3. docs gRPC — Interceptors. grpc.io/docs/guides/interceptors — Documentação oficial cobrindo as quatro variações (server/client × unary/streaming) com exemplos em Go, Java e Python. Consistente entre linguagens, o que ajuda a separar conceito de sintaxe.
  4. docs FastAPI — Dependencies (tutorial completo). fastapi.tiangolo.com/tutorial/dependencies — Tiangolo explica o sistema de DI/interceptor em uma série de páginas curtas. A seção "Dependencies with yield" cobre o caso de aspect com cleanup pós-handler.
  5. livro ASP.NET Core in Action (3ª ed.) — Andrew Lock (Manning, 2024). Cap. 21 (Custom Filters). Lock destrincha os cinco tipos com exemplos de produção e mostra quando preferir middleware sobre filter — a melhor articulação que existe em livro.
  6. livro gRPC: Up and Running — Kasun Indrasiri & Danesh Kuruppu (O'Reilly, 2020). Cap. 6 (gRPC: Beyond the Basics) cobre interceptors em três linguagens. Mostra como o mesmo conceito vira sintaxe diferente em cada SDK gRPC.
  7. livro FastAPI — Bill Lubanovic (O'Reilly, 2023). Cap. 9 e 10 (Dependency Injection, Authentication and Authorization). Mostra o sistema de Depends do FastAPI como ferramenta de aspect, com exemplos práticos de auth por recurso.
  8. artigo JSR 318 — Interceptors 1.2 specification (Java EE, 2013). jcp.org/en/jsr/detail?id=318 — Spec original que formalizou interceptors no Java EE. Histórica, mas leitura útil para ver as raízes do vocabulário moderno.
  9. artigo JAX-RS Filters and Interceptors — Pavel Bucek (Oracle Technical Article, 2014). oracle.com/technical-resources — Distingue claramente filter (que opera em request/response) de interceptor (que opera em entity body). Confusão de vocabulário que ajuda a desfazer.
  10. artigo Choosing between middleware, filters and DI in ASP.NET Core — Steve Gordon (blog, 2020). stevejgordon.co.uk — Análise pragmática de quando usar cada um, com casos concretos. Steve é MVP da Microsoft e escreve sobre internals do framework.
  11. artigo FastAPI's Depends explained from first principles — Sebastián Ramírez (palestras 2020+). github.com/tiangolo — Coleção de talks e posts onde Tiangolo explica a motivação de não chamar de "interceptor". O frame mental dele clarifica decisões de design da biblioteca.
  12. vídeo From Middleware to Endpoint Filters in .NET 7 — Maria Naggaga (.NET Conf, 2022). YouTube — A apresentação oficial de endpoint filters quando foram lançados. Mostra a motivação de design e os trade-offs em relação aos ActionFilters tradicionais.