Skip to content

[types] O único err.message que sobrou no repo torna o fallback pt-BR inalcançável — editar lançamento de devedor exibe o parágrafo em inglês do React em toda falha #122

Description

@Guiroos

Onde

components/devedores/EditEntryDialog.tsx:74último site do repo com esse padrão:

$ grep -rn "err instanceof Error\|error.message" components/ app/ --include="*.tsx"
components/devedores/EditEntryDialog.tsx:74

Evidência

// components/devedores/EditEntryDialog.tsx:68-76
startTransition(async () => {
  try {
    await updateDebtEntry(result.data)
    toast.success('Lançamento atualizado.')
    onOpenChange(false)
  } catch (err) {
    toast.error(err instanceof Error ? err.message : 'Erro ao editar lançamento.')
  }
})

O ramo false do ternário é inalcançável em produção. Erro que atravessa a fronteira de Server Action é reconstruído pelo cliente RSC como um Error genérico (resolveErrorProd, evidência levantada e lida no bundle instalado em #34). err instanceof Error é portanto sempre verdadeiro, e err.message é sempre o mesmo parágrafo:

"An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error."

A string 'Erro ao editar lançamento.' está escrita, versionada, e nunca é exibida. O tipo é verdade sintática e mentira semântica pela segunda vez no mesmo catch: err é Error e .message é string, mas o ternário foi escrito assumindo que instanceof Error distingue "erro da minha action" de "outra coisa" — e ele não distingue nada, porque tudo chega como Error.

Isto não é o achado da #34, e não contradiz o que ela decidiu. A auditoria de 2026-08-13 examinou este mesmo site e o excluiu do escopo da #34, corretamente: a pergunta lá era "esta action deveria devolver ActionResult?", e a resposta é não — as falhas que updateDebtEntry lança hoje ('Lançamento não encontrado' em lib/actions/debtors.ts:205, 'Pessoa não encontrada' em :240, 'Não autorizado' de lib/auth/require-user.ts:6) são todas da classe excepcional que o doc comment de lib/actions/types.ts manda manter como throw. Aquele julgamento continua certo e não é revisitado aqui.

O que ficou sem tratamento é outra coisa: dado que essas falhas devem continuar como throw, o cliente precisa mostrar a mensagem genérica em pt-BR que ele já tem escrita — e hoje não mostra. A correção não é converter a action; é parar de ler err.message.

Verificações feitas (tentativa de falsificar o achado)

  1. É o último site mesmo? grep -rn "err instanceof Error\|error.message" components/ app/ --include="*.tsx" devolve exatamente uma linha. Os outros quatro sites que a [types] Mensagens de erro de Server Actions são mascaradas em produção — 5 componentes exibem err.message que nunca chega ao usuário #34 enumerava foram todos convertidos e hoje leem o retorno da action: FaturaPaymentDialog.tsx:94, SettleChargeDialog.tsx:75, DebtorList.tsx:46 (result.message) e CreditModeSection.tsx:45 (setError(result.message)). Achado de site único — não é multi-site.
  2. A convenção do repo é o oposto, e é esmagadora. O padrão dominante é catch {} + string pt-BR fixa: InvestmentTypeAccordion.tsx:123, InvestmentTypeCard.tsx:133, ui/row-actions.tsx, ui/delete-button.tsx e ~30 outros. Este site é a minoria, não discussão de arquitetura.
  3. Foi decisão deliberada? Não. git log --follow mostra que a linha veio por cópia no rename de EditChargeDialog.tsxEditEntryDialog.tsx (0a45677, "editar valor e observação de qualquer lançamento"), commit de feature que não menciona contrato de erro. O mesmo commit removeu a regra 'Só é possível editar cobranças em aberto' — que era a única mensagem acionável que este err.message já teve razão de tentar exibir. Sobrou o mecanismo sem o motivo.
  4. Falha do usuário mais provável, verificada no caminho real: o cliente já roda updateDebtEntrySchema.safeParse antes de chamar a action (:61), então ZodError do servidor é improvável. O que sobra e acontece de verdade é sessão expirada (requireUserId lança em require-user.ts:6; a sessão é JWT com maxAge de 24 h, lib/auth.ts:43) e falha de rede. Nos dois casos a ação corretiva é a mesma — recarregar / refazer login — e nos dois o usuário recebe o parágrafo em inglês.
  5. Limitação declarada: este ambiente está sem node_modules, então o resolveErrorProd não foi relido aqui. A evidência é a da [types] Mensagens de erro de Server Actions são mascaradas em produção — 5 componentes exibem err.message que nunca chega ao usuário #34, que o leu no bundle instalado (next@16.2.6); o mascaramento é comportamento upstream do React Server Components, não do Next, e não muda em bump de patch. Quem implementar pode reconfirmar em node_modules/next/dist/compiled/react-server-dom-webpack/cjs/react-server-dom-webpack-client.browser.production.js.

Impacto

Quem edita um lançamento em /devedores/[id] — o dialog é aberto por DebtEntryList.tsx:320, tela renderizada — e esbarra em sessão expirada ou rede instável. Em vez de "Erro ao editar lançamento.", o toast exibe um parágrafo de 220 caracteres em inglês, sobre Server Components e digest, num app que é inteiramente em português.

O usuário não tem como saber o que fazer: a mensagem não diz que a sessão caiu nem sugere recarregar. E como o dialog corretamente não fecha no catch, ele fica com o formulário aberto, reenviando e recebendo o mesmo parágrafo. Não há console.error no caminho e o projeto não tem Sentry, então essa é também a única evidência que a falha deixa — e ela aponta para lugar nenhum.

Em desenvolvimento o problema é invisível: next dev não mascara, e o toast mostra 'Lançamento não encontrado' ou 'Não autorizado', que parecem razoáveis. Só o build de produção troca a mensagem.

Cobertura

Não existe teste que pegue isso, e teste de render não é opção — não há @testing-library/react no projeto, então propor um significaria propor a infra inteira.

O caminho é um gate por string sobre o código-fonte, que é precedente versionado neste repo: __tests__/unit/row-actions.test.ts:18 faz exatamente isso (readFileSync + asserção sobre o conteúdo de components/ui/row-actions.tsx), e __tests__/unit/service-worker-registro.test.ts usa a mesma maquinaria.

Caso proposto — um it() que varre components/**/*.tsx e falha se algum arquivo contiver err.message/error.message dentro de um catch:

it('nenhum componente exibe err.message — a mensagem é mascarada em produção', () => {
  const ofensores = arquivosTsx()
    .filter((f) => /catch[\s\S]{0,200}?\b(err|error|e)\s*\.\s*message\b/.test(readFileSync(f, 'utf-8')))
  expect(ofensores).toEqual([])
})

A entrada que só a correção certa rejeita: o arquivo EditEntryDialog.tsx como está hoje. O gate falha agora (1 ofensor) e passa depois. E ele descarta as duas correções erradas mais prováveis:

⚠️ O gate precisa excluir errors.message/next.message: components/feedback/FeedbackDialog.tsx:36,75,81 usa errors.message como chave do mapa de erros de formulário, que não tem relação nenhuma com isto. A âncora em catch no regex acima já resolve; sem ela o gate nasce falso-positivo.

Proposta

Uma linha, em components/devedores/EditEntryDialog.tsx:73-75:

} catch (err) {
  console.error('[EditEntryDialog] updateDebtEntry falhou', err)
  toast.error('Erro ao editar lançamento.')
}

A string já existe no arquivo — a mudança é só deixar de descartá-la em favor de err.message.

Por que console.error e não só remover o err.message (exigência 6): sem o log, catch que ignora err vira o "handler que engole a exceção sem log nem feedback" que o CLAUDE.md proíbe — a troca resolveria a mensagem e pioraria o diagnóstico. É o mesmo remédio já aplicado em components/ui/row-actions.tsx pela correção da #35, então não é padrão novo: é o padrão do repo chegando ao último arquivo que faltava.

Não usar err.digest como substituto: é hash de mensagem + stack, instável entre builds, e não diz nada ao usuário. E não converter updateDebtEntry para ActionResult — a #34 já examinou e descartou essa rota para esta action, com o motivo registrado.

Custo estimado

P (1 arquivo, + 1 arquivo de teste se o gate entrar).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    claude-auditAchado da auditoria automáticaclaude-wipJá tem PR aberto, não pegar de novotypes

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions