Pular para conteúdo

0015 — Filtros admin com @ServerFilter (honra @Order)

Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-07-04 · Decidido em: 2026-07-04

Contexto

O browser da Central (central.xadm.biz) passou a receber "No 'Access-Control-Allow-Origin' header is present" em toda chamada a /api/admin/**. O preflight OPTIONS respondia 401 em produção, apesar de o código estar correto e o teste AdminControllerTest.cors_options_preflight_returns200WithHeaders passar verde.

A causa foi confirmada empiricamente rodando o fat jar (o mesmo artefato que o Coolify builda) localmente: o OPTIONS caía no AdminAuthFilter (401) antes do AdminApiCorsFilter, que deveria curto-circuitar o preflight.

Os dois filtros usavam a API legada @Filter + implements HttpServerFilter, ordenados só por @Order. O HttpServerFilter legado ordena pelo método getOrder() e ignora a anotação @Order. Com ambos no default, a ordem ficava indefinida — e divergia por ambiente:

  • Classpath (teste @MicronautTest): a varredura punha o CORS antes → preflight tratado → verde.
  • Fat jar (produção): a ordem das entradas do zip invertia → Auth primeiro → 401.

Por isso o gate nunca pegou: teste e produção exercitam ordens de filtro diferentes.

Decisão

Migrar os dois filtros admin para a API @ServerFilter do Micronaut 5 (@RequestFilter / @ResponseFilter), que honra @Order de forma determinística tanto no classpath quanto no fat jar. Ordem: AdminApiCorsFilter = HIGHEST_PRECEDENCE, AdminAuthFilter = HIGHEST_PRECEDENCE + 100.

Como defesa-em-profundidade, o AdminAuthFilter exime OPTIONS (passa adiante): um preflight nunca leva Authorization, então negá-lo com 401 é sempre errado — e assim, mesmo que a ordem se inverta de novo, o preflight não quebra.

A decisão 0007 (admin protegido por filtro JWT) continua de pé; muda só a API de filtro e a garantia de ordem.

Consequências

  • Regressão travada por teste que reflete produção: AdminFilterOrderingTest verifica o invariante de ordem (uso de @ServerFilter + CORS antes do Auth) e a eximição de OPTIONS — guardas que o teste de integração antigo, dependente da sorte do classpath, não dava.
  • Respostas de erro (ex.: 401) em /api/admin/** agora também carregam Access-Control-Allow-Origin quando a origem é permitida — o browser consegue ler o erro em vez de mascará-lo como falha de CORS.
  • Os demais filtros legados (AppUserAuthFilter, SetupAuthFilter em centralapps) têm o mesmo risco latente de ordem indefinida se algum dia dividirem path com outro filtro. Não foram migrados aqui (sozinhos no path, sem sintoma) — migrar é trabalho futuro.

Alternativas consideradas

  • Sobrescrever getOrder() mantendo HttpServerFilter: corrige a ordem (verificado no fat jar), mas mantém a armadilha — o próximo filtro legado com só @Order volta a falhar em silêncio. A migração para @ServerFilter remove a classe do bug.
  • Só eximir OPTIONS no Auth, sem mexer na ordem: resolveria o preflight, mas deixaria a ordem dos dois filtros indefinida para o resto (ex.: decoração de resposta). Insuficiente sozinha.