Erro da cotação de frete registrado sem o motivo
Quando a API de fretes recusava uma cotação, o log guardava só o status, e cada chamado exigia reproduzir a requisição à mão. Percebi o padrão, propus a correção e fiz o log mostrar o motivo da recusa, sem expor token e sem mudar o que o cliente vê.
- Papel
- Desenvolvedor responsável, da proposta à entrega
- Contexto
- Plataforma de e-commerce multi-tenant
- Stack
- PHP, Symfony, Guzzle, PHPUnit, PHPStan
- Time
- Solo, com validação de QA
- Período
- 2026
Case anonimizado: nomes de empresa, cliente e pessoas foram omitidos ou trocados.
01. Situação
O log dizia que a cotação falhou, mas não dizia por quê
A plataforma cota frete em um agregador de transportadoras. Quando a API recusava a cotação com HTTP 422, o método de entrega sumia do checkout e o log guardava só isto:
Client error response
[status code] 422
[reason phrase] Unprocessable Entity
O motivo da recusa, como CEP, dimensões, peso, valor declarado ou permissão do token, vem no corpo da resposta, e esse corpo não era gravado. Para cada chamado de "o frete não aparece", alguém tinha que reproduzir a requisição à mão, fora do sistema.
Percebi o padrão ao atender esses chamados, vi que o mesmo trabalho manual se repetia e abri o chamado propondo resolver na origem.
02. Investigação
Quatro detalhes somados apagavam o motivo da recusa
A primeira ideia era só ler o corpo no catch que já existia. Em vez de aplicar direto, segui o código e li o comportamento do cliente HTTP. Encontrei quatro problemas que se somavam.
O cliente HTTP lança exceção para qualquer status fora de 2xx, antes do if que verificava o status. Esse if nunca rodava num 422.
Para ter certeza, escrevi um teste que percorre o método real com uma exceção 422 de verdade, em vez de confiar na leitura.
03. Solução
Ler a resposta no lugar certo e saneá-la antes de gravar
Troquei o catch genérico por dois específicos. A recusa da API grava status, mensagem extraída e corpo da resposta. A falha de rede grava o tipo e o código do erro. Os dois relançam a exceção original, então o checkout segue igual.
Gravar o corpo de uma resposta externa no log tem riscos, e eu tratei os dois que encontrei: o token é mascarado, e o texto é truncado em 2.000 caracteres, para que uma página HTML de erro de CDN não encha o log. O extrator de mensagem que já existia no serviço de etiquetas eu transformei em um trait usado pelas duas classes, sem duplicar lógica.
Escolha
Corpo no log, nunca na mensagem da exceção
A mensagem da exceção chega ao navegador do cliente. Colocar o corpo ali exporia CEP, dimensões e valores.
Trade-off
Não corrigir o logger compartilhado
Corrigiria o problema do arquivo para todos, mas é infraestrutura do sistema inteiro. Mantive o escopo e registrei como sugestão.
Cuidado
Não elevar o nível do log
O calculador é reaproveitado no mesmo request. Subir o nível faria todas as cotações seguintes, inclusive as de sucesso, gravarem como críticas.
try {
$response = $request->send();
if ($response->getStatusCode() !== 200) {
throw new \Exception('Falha na requisição'); // nunca alcançado num 422
}
} catch (\Exception $e) {
$this->logger->logException($e);
throw $e;
}
Desenvolvi com TDD e escrevi 17 testes novos. Para provar que mover o extrator não mudou nenhum comportamento, rodei a suíte do serviço de etiquetas sem alterar o arquivo de teste: os 104 testes continuaram passando.
04. Resultado
O motivo da recusa aparece no log, e o checkout continua igual
- testes novos
- 17
- arquivos de log por erro
- 2 → 1
Um chamado de frete que some do checkout passou a ser respondido lendo o log: status, validação recusada, loja e configuração. Timeout e recusa ficam separados. Nenhuma mensagem ao cliente mudou.
Entreguei também três melhorias mapeadas durante a análise, já descritas para virarem chamados: mensagem genérica ao cliente nessa falha, saneamento dos logs antigos de etiqueta e log no comando de sincronização.