0016 — Contrato de erro único (RFC 7807 / problem+json)¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11 · Decidido em: 2026-07-06
Contexto¶
A auditoria §6 (constituição 0.17.35, arquitetura destilada dos pilotos) apontou que o auth
montava corpos de erro ad-hoc — HttpResponse.status(...).body(Map.of("error", ..., "message", ...))
— espalhados por controllers e filtros (~50 pontos, ~28 códigos distintos). A norma da casa
(bi-transporte-xls, decisão 0017)
é um corpo único RFC 7807 (application/problem+json) renderizado por um
ErrorResponseProcessor que @Replaces o HateoasErrorResponseProcessor default.
Dois fatos do auth impedem uma cópia literal do modelo dos BIs:
- Códigos de máquina são load-bearing. O
error(ex.:invalid_token,user_exists,cliente_not_found) é consumido pela Central e travado em teste. O processor dos BIs deriva o código dostatus.name()— o que colapsaria os ~28 códigos (vários sob o mesmo status:missing_tokeneinvalid_tokensão ambos 401). - Os endpoints públicos (
AuthController) são consumidos por browser e hoje adicionam CORS a toda resposta — inclusive erros — viawithCors(...). UmErrorResponseProcessorglobal não adiciona CORS; sem ele, o browser bloquearia a leitura do corpo de erro.
Decisão¶
Adotar o problem+json como contrato único de erro, com três peças em comum/error:
ProblemDetail— record RFC 7807 (type,title,status,detail) + a extensãocode(o antigoerror, machine-code estável). Desde a const 0.28.0 vem da libbr.com.xadm:xadm-comum-web(br.com.xadm.comum.web.ProblemDetail), não mais do fork local — ver Atualização abaixo.ApiException extends HttpStatusException— carrega status +code. Diverge do modelo classe-por-status dos BIs de propósito (o auth tem ~28 códigos, muitos sob o mesmo status → o código é carregado pela exceção, não derivado do status). Fica local (é domínio do auth) e implementabr.com.xadm.comum.web.PortadoraDeProblema— o ponto de extensão pelo qual o processor da lib lê o machine-code (ver Atualização).UnifiedErrorResponseProcessor— renderiza os erros gerados pelo framework (validação, 404, exceções) em problem+json. Desde a const 0.28.0 vem da lib (xadm-comum-web:0.3.0): lê ocodedaPortadoraDeProblemana causa-raiz (fallbackstatus.name()) e monta a lista de campos no Bean Validation. O fork local foi removido — ver Atualização.
Fluxo por superfície:
| Superfície | Como sinaliza erro | Quem renderiza |
|---|---|---|
Filtros (AdminAuthFilter, AppUserAuthFilter, SetupAuthFilter) |
throw ApiException |
processor global |
| Controllers admin/setup/apps | throw ApiException |
processor global |
AuthController (público, browser) |
throw ApiException |
@Error local → problem+json + CORS |
O @Error(exception = ApiException.class) local no AuthController é o ponto-chave: ele
renderiza problem+json com os headers CORS (withCors), preservando o contrato do browser que
o processor global não daria. Por ser local, só vale para as rotas do próprio controller — os
demais seguem no processor global (sem CORS, correto: admin tem seu próprio CORS filter; apps/setup
não são browser).
Exclusões deliberadas (não são erros, não viram problem+json):
- Relatório 502 do
SetupController— corpo de sucesso (relatório de provisão) devolvido diretamente; respostas retornadas não passam pelo processor, então seguem preservadas. - 403
{status: PENDENTE|SPAM}do client-login — sinal de fluxo que o app consome (não uma falha); modelado comoAuthLoginService.ClientLoginResult, não comoApiException.
Consequências¶
- Contrato de wire: todo erro de API agora é
application/problem+jsoncom{type, title, status, detail, code}; ocodepreserva o machine-code antigo. Travado porPabastLoginControllerTest.erroDeLogin_saiComoProblemJsonComCors(erro público = problem+json + CORS) e pelos testes de contrato doAdminController(assertamcode). - Item 1 (§6):
AppUserAuthFiltereSetupAuthFiltermigrados para@ServerFilter+@RequestFilter— completa o "trabalho futuro" previsto na decisão 0015. - Item 4 (§6): o
AuthController(615 → ~270 linhas) virou fino — HTTP (rate limit, CORS, mapeamento) no controller, negócio no novoAuthLoginService; DTOs e o contratoFirebaseTokenValidator/FirebaseClaimsextraídos para arquivos próprios. - Item 2 (§6): trava de arquitetura
ArchitectureTest(ArchUnit) — verArchitectureTest; sem ciclos entre features,comumindependente.
Alternativas consideradas¶
- Classe-por-status (modelo literal dos BIs): rejeitado — colapsaria os ~28 machine-codes num código por status, quebrando o contrato consumido pela Central.
- Processor global adiciona CORS: rejeitado — CORS
*em todo erro estaria errado para/api/admin/**(origens específicas viaAdminApiCorsFilter) e é irrelevante para apps/setup. - Confiar no
@CrossOriginpara o CORS do erro (sem@Errorlocal): não escolhido — o comportamento do CORS filter sobre respostas de exceção não é garantido entre versões; o@Errorlocal é explícito e testado. - Converter só o processor, deixar os
Map.ofad-hoc: rejeitado pelo dono — o pedido foi contrato uniforme, não meia-adoção.
Atualização — adoção do xadm-comum-web (constituição 0.28.0)¶
Na migração 0.18.4→0.28.0 o auth passou a consumir a lib da casa (br.com.xadm:xadm-comum-web,
decisão 0019 do central)
em vez de manter o fork local do contrato de erro:
ProblemDetail: adotada (record idêntico; drop-in). Fork local removido.UnifiedErrorResponseProcessor: adotado a partir dexadm-comum-web:0.3.0. A 0.2.0 não servia (derivava ocodesempre destatus.name(), colapsando os ~28 machine-codes do auth); o gap virou feedback ao central, e a 0.3.0 fechou a pendência: o processor lê o machine-code viaPortadoraDeProblemana causa-raiz — a nossaApiExceptiona implementa — e carimba a lista de campos no Bean Validation. O fork local do processor foi removido; o contrato de erro do framework é 100% da lib.ApiException: fica local (é domínio do auth: os 28 codes), agora implementandoPortadoraDeProblema.
/health — resolvido na lib (xadm-comum-web:0.4.0). A dep traz um HealthController
@Controller("/health") (para apps sem micronaut/management). Na 0.3.0 ele colidia com o
/health do micronaut-management do auth → duas rotas GET /health → o Micronaut respondia
400 ("More than 1 route matched"), quebrando o healthcheck. Micronaut não remove a rota de um
@Controller de lib (nem @Replaces nem bean-exclude); virou feedback ao central. A 0.4.0 tornou
o HealthController condicional (@Requires(missingBeans = io.micronaut.management.endpoint.health.HealthEndpoint)):
em app com management (o auth) ele se cala, e o /health DB-aware (JDBC/disk/deadlock +
/health/liveness·/health/readiness) do management vale. endpoints.health.enabled: true.
Regressão travada em HealthRouteTest (/health == 200 {status:UP}).
Emenda 2026-09-11: depois disto o /health foi rebaixado ao flat da lib (e708fdb, 2026-08-21)
e, com a xadm-comum-web 0.6.1 (6f4138b, 2026-08-26), o HealthController/InfoController da lib
ficaram incondicionais e o management teve /health e /info desligados
(endpoints.health.enabled: false). O /health de hoje é o flat {status, versao, flavor, commit}
da lib, 200 fixo, igual em jar e native — não há mais /health/liveness·/health/readiness.
micronaut.security.enabled: false — postura do hub, não limite da lib. A lib traz
micronaut-security transitivo (para o @Secured do seu HealthController). Isso é by-design:
a casa padroniza todo app consumindo o framework de segurança (xadm-seguranca). O auth é o caso
especial (CLAUDE.md): é o emissor de JWT / provedor de JWKS — a coisa contra a qual a security
dos outros apps valida —, então não age como resource-server do framework; autentica por
@ServerFilter próprio (Admin/Setup/AppUser). Desligar a security é a postura correta do hub, não um
contorno de bug. Um app comum (com security ligado) consome a lib sem essa flag.