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 porASPNETCORE_ENVIRONMENT. - Django:
DEBUG = Truemostra pilha, variáveis locais e configurações, e na página 404 lista os padrões de URL do projeto. - Laravel:
APP_DEBUG=trueno.envmostra a página de erro detalhada. - PHP puro:
display_errorsligado 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
- Defina
NODE_ENV=productionno serviço, no Dockerfile ou na unidade do systemd, não num arquivo que só existe na sua máquina. - Registre um tratador de erro próprio, depois de todas as rotas, com quatro parâmetros.
- Logue o erro completo, com pilha e código, no servidor.
- Responda conforme o cliente: HTML para navegador, JSON para API, sem detalhes internos em nenhum dos dois.
- 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.
- 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_ENVou 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.messagepara 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-Bynem 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?
Referência: https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html