Artigos Rem soft Sistemas

Tratamento de erros HTTP na prática: status codes, RFC 7807 e como o Spring resolve isso pra você

Tratamento de erros HTTP na prática: status codes, RFC 7807 e como o Spring resolve isso pra você

Quando uma API retorna 200 OK com {“success”: false} no corpo, alguém perdeu tempo. Quando o front recebe 500 Internal Server Error sem mais nenhuma pista, alguém vai abrir um ticket. E quando ninguém na equipe consegue explicar, de cabeça, a diferença entre 401 e 403, é hora de revisar o básico.

Tratamento de erros parece um assunto pequeno — até virar a causa raiz de uma noite mal dormida. Este post junta o que precisa estar firme na cabeça de quem escreve APIs HTTP: as famílias de status codes, os pares que mais confundem, o padrão RFC 7807 (e sua atualização, a 9457), e como o Spring Boot moderno resolve tudo isso com poucas linhas.

Por que tratamento de erros importa tanto

Erros não são “casos de canto” da API. Eles definem quatro coisas críticas:

  • Contrato com o cliente. Status code e corpo dizem ao consumidor exatamente como reagir. Errar isso é introduzir bug no outro lado.
  • DX e tempo de debug. Mensagens claras economizam horas. Erros genéricos viram tickets eternos.
  • Segurança. O que você expõe num erro pode virar vetor de ataque — stack trace, SQL, e-mail, ID interno.
  • Observabilidade. Sem erros padronizados, alertas e dashboards viram adivinhação.

A boa notícia: as cinco famílias de status codes do HTTP já fazem 80% do trabalho. Basta usá-las corretamente.

As cinco famílias de status codes

Um truque mental para nunca errar a faixa: o primeiro dígito responde “de quem é a culpa?”.

  • 1xx — Informational. Processando, continue. Raríssimo escrever na mão.
  • 2xx — Success. Deu certo. Inclui 201 Created (com header Location), 202 Accepted para processamento assíncrono, 204 No Content para DELETE e PUT sem retorno.
  • 3xx — Redirection. Procure em outro lugar. Os mais úteis: 303 See Other (PRG — Post/Redirect/Get), 307 e 308 que garantem preservar o método HTTP no redirect (coisa que 301 e 302 não fazem de forma consistente).
  • 4xx — Client Error. Você (cliente) errou.
  • 5xx — Server Error. Nós (servidor) erramos. Minimize.

Se você está em dúvida entre 4xx e 5xx, pergunte: o cliente consegue corrigir esse erro? Sim → 4xx. Não → 5xx.

A confusão eterna: 401 vs 403

Esse par troca posição na cabeça de praticamente todo dev pelo menos uma vez na carreira. O nome não ajuda: 401 se chama “Unauthorized”, mas é sobre autenticação, não autorização. A diferença é simples quando você verbaliza:

401 Unauthorized: “Quem é você?” A requisição não traz credencial, ou a credencial é inválida. Token ausente, JWT expirado, assinatura quebrada, basic auth com senha errada — tudo isso é 401. A RFC pede que a resposta inclua o header WWW-Authenticate indicando o esquema esperado.

403 Forbidden: “Eu sei quem você é, mas você não pode.” A identidade foi resolvida, mas falta autorização. Usuário comum tentando rota de admin, acesso a recurso de outro tenant, API key correta cujo plano não libera aquela feature. Reenviar credencial não resolve um 403.

Casos práticos para fixar:

Cenário

Status

Requisição sem header Authorization

401

JWT expirado

401

JWT com assinatura inválida

401

Usuário logado tentando acessar /admin

403

Usuário lendo pedido de outro cliente

403 ou 404

API key correta, plano não permite a feature

403

Dica de privacidade: quando seu objetivo é esconder a existência de um recurso (listagens privadas, multi-tenant), prefira 404 em vez de 403. O 403 confirma que o recurso existe — bom prato para enumeration attacks.

Outros pares que merecem decorar

Alguns status codes próximos confundem tanto quanto 401/403:

  • 400 vs 422. 400 Bad Request é para sintaxe quebrada — JSON inválido, campo faltando estruturalmente. 422 Unprocessable Entity é para semântica — a sintaxe está OK, mas uma regra de negócio falhou (e-mail já cadastrado, idade < 18, CPF inválido).
  • 404 vs 410. 404 significa “não sei se já existiu”. 410 Gone é “existiu, foi removido de propósito” — útil para LGPD, contas deletadas, links que não vão voltar.
  • 409 vs 412. 409 Conflict é para conflito de estado (duplicidade, versão divergente). 412 Precondition Failed é para condicionais (If-Match, If-Unmodified-Since) que não bateram.
  • 429 vs 503. 429 Too Many Requests é este cliente abusando. 503 Service Unavailable é o servidor inteiro indisponível. Ambos devem incluir o header Retry-After.

O problema: cada API inventa seu próprio formato de erro

Mesmo com os status codes resolvidos, surge o segundo problema — o corpo da resposta. Sem padrão, cada API inventa o seu:

// API A

{ "error": "USR_404", "msg": "not found" }

// API B

{ "errorCode": 1004, "errorMessage": "User unknown", "success": false }

// API C (em XML, pra piorar)

<error><code>NOT_FOUND</code></error>

Resultado: cada cliente escreve um parser próprio, logs não casam entre serviços, monitoramento vira gambiarra. Foi exatamente esse caos que a IETF resolveu padronizar.

RFC 7807: Problem Details for HTTP APIs

Publicada em 2016 e atualizada pela RFC 9457 em 2023, a RFC 7807 define um formato JSON (e XML) padrão para descrever erros em APIs HTTP. A motivação, nas palavras da própria especificação, é “evitar a proliferação de formatos de erro de uso único”.

O Content-Type é application/problem+json e os campos previsíveis são seis:

Campo

O que é

Quando usar

type

URI identificando o tipo do problema (idealmente dereferenciável, apontando para uma doc)

Sempre — é seu contrato

title

Resumo curto e legível, invariante entre ocorrências do mesmo type

Sempre

status

Código HTTP, repetido aqui para facilitar log/leitura

Sempre

detail

Explicação específica desta ocorrência. Pode variar

Quando útil

instance

URI referenciando esta ocorrência específica

Quando útil

Extensões

Campos custom (traceId, errors, balance…)

À vontade

Um exemplo completo:

HTTP/1.1 422 Unprocessable Entity

Content-Type: application/problem+json

{

  "type":     "https://api.example.com/errors/insufficient-funds",

  "title":    "You do not have enough credit",

  "status":   422,

  "detail":   "Your balance is 30, but the cost is 50",

  "instance": "/account/12345/transactions/abc",

  "balance":  30,

  "traceId":  "7a9f1c..."

}

Os campos custom (balance, traceId) convivem normalmente com os padrão. O type é seu contrato com o cliente — uma vez publicado, versione com cuidado.

RFC 9457: o que mudou

A 9457 obsoleta a 7807, mas é praticamente compatível. As novidades importantes:

  • Padronização do campo errors. Antes cada um inventava o seu para validação em lote. Agora há orientação clara.
  • i18n explícito. O title é em inglês; o detail pode usar Accept-Language.
  • Segurança reforçada. Reforça o óbvio que muita gente ignora: não vaze stack trace, paths internos, SQL ou PII no detail.
  • type como URN. Aceitável quando você não quer hospedar uma página de documentação para cada erro.

Validação em lote: o array errors

Aqui está o caso que aparece em todo formulário real. Um cadastro com vários campos inválidos não merece cinco round-trips — o ideal é uma única resposta 422 listando tudo.

Imagine um POST para /pessoas com vários campos inválidos: nome com 1 caractere, sobrenome ultrapassando 200, CPF com 5 dígitos, telefone com 3. A resposta deve descrever todos os erros de uma vez:

HTTP/1.1 422 Unprocessable Entity

Content-Type: application/problem+json

{

  "type":   "https://api.exemplo.com/errors/validation",

  "title":  "Dados inválidos",

  "status": 422,

  "errors": [

    { "field": "nome",      "detail": "mínimo de 2 caracteres" },

    { "field": "sobrenome", "detail": "máximo de 200 caracteres" },

    { "field": "cpf",       "detail": "deve conter 11 dígitos" },

    { "field": "telefone",  "detail": "deve conter 11 dígitos" }

  ],

  "traceId": "7a9f1c..."

}

O front itera o array, pinta cada campo de vermelho e mostra a mensagem ao lado. Sem ping-pong, sem usuário irritado tentando descobrir o que mais quebra ao corrigir o primeiro erro.

Dica de internacionalização: traduza apenas o detail conforme Accept-Language e mantenha o field (e o type) estáveis — esses são contrato.

Como o Spring trata erros (em três níveis)

O Spring evoluiu bastante nesse assunto. Hoje há três caminhos, que se complementam:

Nível 1: ResponseStatusException

Para casos simples, lance direto no controller:

@GetMapping("/users/{id}")

public User findById(@PathVariable Long id) {

    return repository.findById(id)

        .orElseThrow(() -> new ResponseStatusException(

            HttpStatus.NOT_FOUND, "User " + id + " not found"));

}

Rápido e direto. Bom para one-offs. Ruim quando o mapeamento de exceptions começa a crescer.

Nível 2: @RestControllerAdvice com @ExceptionHandler

A partir do momento em que há mais de duas ou três exceptions de domínio, centralize:

@RestControllerAdvice

public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)

    public ResponseEntity<ErrorResponse> handle(UserNotFoundException ex) {

        ErrorResponse body = new ErrorResponse("USER_NOT_FOUND", ex.getMessage());

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(body);

    }

    @ExceptionHandler(ConstraintViolationException.class)

    public ResponseEntity<ErrorResponse> handle(ConstraintViolationException ex) {

        return ResponseEntity.unprocessableEntity().body(/* ... */);

    }

}

DRY, testável, fácil de evoluir. Toda exception passa por um único ponto de tradução para resposta HTTP.

Nível 3: ProblemDetail — RFC 7807 nativo (Spring 6 / Boot 3)

Aqui mora a maior mudança recente. Desde Spring Framework 6 e Spring Boot 3, há suporte nativo à RFC 7807 — não precisa mais escrever a classe de erro à mão:

@RestControllerAdvice

public class GlobalExceptionHandler {

    @ExceptionHandler(UserNotFoundException.class)

    public ProblemDetail handle(UserNotFoundException ex) {

        ProblemDetail pd = ProblemDetail.forStatusAndDetail(

            HttpStatus.NOT_FOUND, ex.getMessage());

        pd.setType(URI.create("https://api.example.com/errors/user-not-found"));

        pd.setTitle("User not found");

        pd.setProperty("traceId", MDC.get("traceId"));   // campo custom

        return pd;

    }

}

O Spring serializa como application/problem+json automaticamente. Resultado para o cliente:

HTTP/1.1 404 Not Found

Content-Type: application/problem+json

{

  "type":    "https://api.example.com/errors/user-not-found",

  "title":   "User not found",

  "status":  404,

  "detail":  "User 42 not found",

  "instance":"/users/42",

  "traceId": "7a9f1c..."

}

Em uma equipe que já está em Boot 3, não há motivo para inventar formato próprio. Use ProblemDetail.

Anti-padrões: o que não fazer

Os erros mais comuns que vejo em code reviews:

  • Devolver 200 com {“success”: false}. Mata todo o uso de status codes — quebra retry automático, monitoração, cache, e ferramentas que dependem do código.
  • Vazar stack trace no detail. Clássico vetor de info disclosure. Logue no servidor, mostre apenas um traceId ao cliente.
  • Mensagens genéricas (“erro interno”). Sem type nem código, o cliente não tem como reagir programaticamente.
  • Status arbitrário (599, 444…) Quebra proxies, ferramentas e clientes.
  • Mudar o significado do type. É contrato. Mudou? É breaking change — versione a URL.
  • Esquecer Retry-After em 429 e 503. Sem isso, o cliente faz spin retry e piora a carga.

Boas práticas: o que fazer

  • Use application/problem+json — o cliente sabe parsear sem doc extra.
  • Inclua traceId ou correlationId em toda resposta de erro — permite suporte fechar o ciclo em um clique.
  • Documente os type URIs — cada URI deve apontar para uma página explicando o erro e como resolver.
  • Mensagens acionáveis no detail — “e-mail must be valid” é infinitamente melhor que “validation failed“.
  • Trate validação em lote com errors[].
  • Status code e corpo precisam contar a mesma história.

Segurança: erros são vetor de ataque

Três comportamentos a evitar:

  • Não vaze diferenças entre “usuário não existe” e “senha errada” no login. Devolva 401 genérico — ajuda contra enumeration.
  • Use 404 em vez de 403 para esconder recursos privados quando faz sentido. O 403 confirma a existência.
  • Sanitize tudo que vai no detail. Nunca interpole input bruto. Reflected XSS via problem+json é coisa real.

Observabilidade: o bônus que ninguém promove

Padronizar o corpo é o que torna possível medir e alertar com qualidade. Métricas por type (errors_by_type{type=”validation”}) viram gráfico legível imediato. Correlação com tracing fica trivial. Alertas só em 5xx (não acorde ninguém por 4xx — é o cliente que errou). Dashboards por status code contam a história: pico de 429 é abuso, pico de 422 provavelmente é o front quebrado, pico de 500 é fogo na cozinha.

Bônus: curiosidades para você ser o nerd da equipe

  • 418 I’m a teapot. RFC 2324, de 1º de abril de 1998. Tentaram remover; teve protesto. Continua. O Google ainda responde 418 em google.com/teapot.
  • 451 Unavailable for Legal Reasons. Homenagem ao livro Fahrenheit 451. Use quando bloquear conteúdo por ordem judicial.
  • 402 Payment Required. Reservado em 1997, nunca padronizado. Stripe e outras gateways usam livremente.
  • 499 Client Closed Request. Não está na RFC, mas o nginx usa quando o cliente fecha a conexão antes da resposta.

Checklist antes de mergear sua API

  • [ ] Status code escolhido representa a causa real (cliente vs servidor)
  • [ ] Corpo segue application/problem+json com type, title, status, detail
  • [ ] type aponta para documentação dereferenciável (ou URN estável)
  • [ ] traceId / correlationId presente em toda resposta de erro
  • [ ] Erros de validação usam o array errors[]
  • [ ] Nada de stack trace, SQL, PII ou paths internos vaza no corpo
  • [ ] 401 vs 403 revisados — autenticação vs autorização
  • [ ] 429 e 503 incluem header Retry-After
  • [ ] Testes cobrem caminhos de erro, não só happy path
  • [ ] Documentação OpenAPI lista os erros possíveis por endpoint

Fechando

Tratamento de erros bem feito é uma daquelas coisas que ninguém elogia, mas todo mundo nota quando falta. Status code certo + corpo padronizado = API que economiza tempo da sua equipe, do seu cliente e do seu plantão.

Se sua stack é Spring Boot 3, comece pelo ProblemDetail ainda hoje. Se está em outra linguagem, a RFC 7807/9457 funciona igual — Python tem problems, .NET tem ProblemDetails, Node tem várias libs. O padrão é o ponto.

E se você lê este post no celular durante um plantão tentando entender um 503 sem Retry-After, sinto muito — e boa sorte.

Referências

Bruno Henrique S. Contente — Bacharel em Ciência da Computação e pós-graduado em Engenharia de Software (especialização). Desenvolvedor na Rem Soft, escreve sobre boas práticas, design de APIs e código limpo.

Escrito por:

Está gostando do conteúdo? Compartilhe!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Artigos Recentes

Boas práticas e Curiosidades do React Native

Olá, pessoal! Se você é um desenvolvedor React que já domina o mundo web e está pensando em expandir seus horizontes para o mobile, este artigo é para você. Vamos conversar sobre como o React Native pode ser a ponte perfeita entre seu conhecimento atual e o desenvolvimento mobile de

Leia Mais »

Do Código à Cultura: Como Criar um Ecossistema de Inovação Interna

Inovação é uma palavra que já virou parte do vocabulário diário de qualquer empresa de tecnologia. Mas, na prática, ainda existe um grande mal-entendido: inovar não é apenas criar algo novo, revolucionário ou inédito. Na maioria das vezes, inovar significa melhorar continuamente o que já existe — processos, produtos e

Leia Mais »

Wegic: A Inteligência Artificial que Transforma Negócios

Introdução: O uso da inteligência artificial deixou de ser tendência e passou a ser realidade em empresas de todos os setores. Cada vez mais, soluções inovadoras surgem para otimizar processos, aumentar a produtividade e gerar insights estratégicos. Entre essas soluções, a Wegic se destaca como uma ferramenta de IA prática,

Leia Mais »

Krayin CRM: Gestão de Relacionamento que Impulsiona Vendas

O que é Krayin CRM? Krayin CRM é uma plataforma de gestão de relacionamento com o cliente de código aberto, projetada para otimizar processos de vendas e impulsionar o crescimento do seu negócio. Código Aberto & Gratuito Construído sobre o robusto framework Laravel, oferece a liberdade e a flexibilidade de

Leia Mais »

Sobre o Autor

Mais sobre tecnologia

Gostou do Artigo?