Programação Por Equipe DevBras

Capa do artigo: Erro 500 em produção: uma frase e um código para a pessoa, a pilha fica no log

Erro 500 em produção: uma frase e um código para a pessoa, a pilha fica no log

Configure o tratamento de erro para mostrar mensagem genérica com código de acompanhamento, guardar o stack trace só no servidor e testar com a configuração real de produção.

A pessoa clica em "Salvar cadastro" e recebe uma página com SyntaxError: Unexpected token, o caminho /srv/loja/node_modules/body-parser/lib/types/json.js e mais vinte linhas de pilha. Ela não sabe se o cadastro foi salvo nem se pode tentar de novo. Quem tem outra intenção acaba de aprender onde a aplicação está instalada, qual biblioteca ela usa e como provocar um erro.

A resposta curta: em produção, o erro inesperado vira uma frase simples, um código de acompanhamento e um caminho de volta. O stack trace completo vai para o log do servidor, ligado ao mesmo código. Desligue o modo de depuração por variável de ambiente, nunca "só por hoje", e teste forçando um erro com a configuração de produção, não com a do notebook.

Por que a pilha vaza

Quase todo framework tem dois modos de erro. No modo de desenvolvimento, mostra tudo, porque você é o único que vê a tela. No modo de produção, mostra uma página genérica. O vazamento acontece quando o modo de desenvolvimento chega ao servidor sem ninguém perceber.

O Express é um exemplo direto: o tratador de erro padrão inclui o stack trace na resposta quando a variável NODE_ENV não vale production. Se o serviço sobe sem essa variável, que é o padrão de um node server.js qualquer, cada erro mostra a pilha. Nada no código mudou; só faltou uma variável no ambiente.

O mesmo mecanismo existe em outras plataformas, com nomes diferentes:

  • ASP.NET Core: a página de exceção de desenvolvedor aparece quando o ambiente é Development, definido por ASPNETCORE_ENVIRONMENT.
  • Django: DEBUG = True mostra pilha, variáveis locais e configurações, e na página 404 lista os padrões de URL do projeto.
  • Laravel: APP_DEBUG=true no .env mostra a página de erro detalhada.
  • PHP puro: display_errors ligado imprime avisos e erros fatais no meio do HTML, tema do post sobre erro de PHP na tela.
  • Flask: o depurador interativo do Werkzeug permite executar código no servidor pelo navegador; ele nunca pode estar ativo em produção.

O que a pessoa precisa ver

A página de erro boa responde três perguntas: o que aconteceu, em linguagem humana; o que fazer agora; e como pedir ajuda. Algo como: "Não conseguimos salvar seu cadastro agora. Seus dados não foram perdidos nesta tela; tente de novo em alguns instantes. Se o problema continuar, informe o código 3f9c1a2e ao suporte." E um botão para voltar ao formulário.

O código é o identificador da requisição, o mesmo que aparece em cada linha de log daquele pedido. O post sobre correlation ID mostra como gerar e propagar esse id; aqui ele é o elo entre a frase genérica da tela e a pilha completa do servidor.

Como montar o tratamento de erro no Express

  1. Defina NODE_ENV=production no serviço, no Dockerfile ou na unidade do systemd, não num arquivo que só existe na sua máquina.
  2. Registre um tratador de erro próprio, depois de todas as rotas, com quatro parâmetros.
  3. Logue o erro completo, com pilha e código, no servidor.
  4. Responda conforme o cliente: HTML para navegador, JSON para API, sem detalhes internos em nenhum dos dois.
  5. Diferencie erro do cliente de erro do servidor. Um JSON malformado é 400, não 500, e pode ter uma mensagem útil sem revelar a implementação.
  6. Remova cabeçalhos que anunciam a tecnologia, como X-Powered-By.
const express = require('express');
const pino = require('pino');

const logger = pino();
const app = express();
app.disable('x-powered-by');
app.use(express.json());

// ... rotas da aplicação ...

app.use((err, req, res, next) => {
  const codigo = res.get('X-Request-Id') || 'sem-codigo';
  const status = err.status >= 400 && err.status < 500 ? err.status : 500;

  logger.error({ err, codigo, rota: req.originalUrl, status }, 'erro na requisição');

  if (res.headersSent) return next(err);

  const titulo = status === 500
    ? 'Não conseguimos concluir sua solicitação agora.'
    : 'Não entendemos os dados enviados.';

  if (req.accepts(['html', 'json']) === 'json') {
    return res.status(status).type('application/problem+json').json({
      type: 'about:blank',
      title: titulo,
      status,
      codigo
    });
  }

  res.status(status).render('erro', { titulo, codigo });
});

Alguns pontos do código. O pino serializa o objeto passado na chave err com mensagem e pilha, então o log fica completo. O err.status é preenchido por bibliotecas como o próprio parser de JSON do Express para erros do cliente; qualquer outro erro vira 500. A resposta de API segue o formato de "problem details" (RFC 9457), que define campos como type, title e status e aceita campos extras como o codigo. Se a resposta já começou a ser enviada, o tratador repassa ao padrão do Express, que encerra a conexão.

O que fica no log

{"level":50,"codigo":"3f9c1a2e-7b41-4c0e-9d55-0a8e6b2f1c47","rota":"/cadastro","status":500,
 "err":{"type":"Error","message":"connect ETIMEDOUT 10.0.2.15:5432","stack":"Error: connect ETIMEDOUT ..."},
 "msg":"erro na requisição"}

Tudo o que a página escondeu está aqui: a mensagem, a pilha e o endereço do banco que não respondeu. Só que do lado de cá. Cuide para não registrar o corpo inteiro da requisição, que pode ter senha ou documento, como explica o post de log sem senha.

Testando com a configuração de produção

O teste que importa é feito no ambiente real, ou numa cópia com as mesmas variáveis. Um JSON quebrado é o jeito mais simples de forçar um erro sem criar rota especial:

$ curl -s -i -X POST https://loja.exemplo/api/cadastro \
    -H "Content-Type: application/json" -d '{ruim'
HTTP/1.1 400 Bad Request
Content-Type: application/problem+json; charset=utf-8
X-Request-Id: 9b1e0c44-2f6a-4d8e-a1c3-5e7f90b2d613

{"type":"about:blank","title":"Não entendemos os dados enviados.","status":400,"codigo":"9b1e0c44-..."}

Procure na resposta qualquer caminho de arquivo, nome de pasta como node_modules, versão de biblioteca ou trecho de SQL. Se aparecer, o ambiente não está em modo de produção. Repita com uma URL inexistente, porque algumas plataformas mostram a lista de rotas na página 404 em modo de depuração.

Erros comuns

  • Ligar o modo de depuração "só por hoje" para investigar: esquecem de desligar, e o erro seguinte mostra configuração e caminhos para qualquer visitante.
  • Confiar que o deploy define a variável: o serviço sobe sem NODE_ENV ou com o ambiente padrão, e o framework assume desenvolvimento.
  • Página de erro sem código: o suporte recebe "deu erro" e não consegue achar nada no log.
  • Tratar tudo como 500: erro de validação vira alerta de servidor, polui o monitoramento e esconde as falhas reais.
  • Mandar err.message para a tela: a mensagem de um driver de banco pode trazer host, porta, nome de tabela ou trecho da consulta.
  • Testar só no notebook: lá a pilha aparecer é esperado, e o teste não diz nada sobre o servidor.

Checklist

  • A variável de ambiente de produção está definida no serviço, não só no seu terminal.
  • Um JSON malformado devolve 400 sem caminho de arquivo nem nome de biblioteca.
  • Uma falha interna devolve 500 com frase genérica e código de acompanhamento.
  • O mesmo código aparece no log, junto com a pilha completa.
  • A resposta não tem X-Powered-By nem outro cabeçalho que anuncie versão.
  • A página 404 não lista rotas nem configurações.
  • A página de erro tem um caminho de volta para a pessoa.

Mande agora um JSON quebrado para uma rota do seu sistema em produção: a resposta mostra algum caminho de arquivo, nome de biblioteca ou trecho de consulta?

Avalie este artigo

Referência: https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html