Garu

2026-09-10

v0.22.0 — A trava contra cobrança repetida agora vale também para o checkout

cartaocheckoutapisegurancabugfix

O que mudou

A trava agora fecha o intervalo que faltava

Na v0.21.1 a trava contra cobrança repetida era uma consulta: antes de cobrar, o sistema procurava uma cobrança igual dos últimos 60 segundos. O problema é quando essa consulta acontece. Entre ela e o momento em que a cobrança fica gravada existem duas chamadas à adquirente — registrar o cartão e criar a cobrança. Se a segunda tentativa chegasse nesse meio-tempo, não havia nada para a consulta encontrar, porque a primeira ainda não tinha terminado.

Esse intervalo não é de milissegundos. Não há tempo limite configurado nas chamadas à adquirente, então ele dura o quanto ela demorar — no incidente que originou tudo isso, tempo suficiente para o sistema de quem integra desistir aos 30 segundos e tentar de novo.

Agora a chave é reservada no banco antes de qualquer chamada externa, e o próprio Postgres recusa a segunda. A proteção existe desde o primeiro instante da tentativa, não só depois que ela termina.

A página de pagamento também está protegida

Até agora a trava valia só para quem integra via API. Na página de pagamento ela não podia valer: devolver a cobrança original ali seria entregar os dados do comprador (nome, e-mail, telefone, endereço) para qualquer um que soubesse o CPF e o link do produto — a página não tem login.

Recusar não entrega nada. Por isso agora a página também é protegida: uma segunda tentativa idêntica no cartão recebe um aviso de que a cobrança já está sendo processada, em vez de virar uma segunda cobrança real. Isso cobre o caso que o botão travado não alcançava — duas abas abertas, ou recarregar a página e enviar de novo.

Pix e boleto seguem passando na página de pagamento: uma segunda via não custa nada a quem compra (paga-se uma só), e recusar deixaria sem saída quem recarregou a página e perdeu o QR Code da tela.

Cobrança recusada libera na hora

Se a adquirente recusa o cartão, a reserva é liberada imediatamente. Quem corrigiu o número não precisa esperar nada para tentar de novo.

Falhas de rede voltaram a ser legíveis

Um detalhe interno com efeito prático no suporte: qualquer falha de rede com a adquirente aparecia nos registros como um erro genérico de programação, sem código, sem status e sem causa. O cliente HTTP engolia a falha e devolvia um objeto vazio. Agora a falha chega inteira, com código e resposta — o que muda quanto tempo se leva para entender um problema de pagamento.

Para quem integra

POST /api/v1/charges pode responder 409 quando uma cobrança idêntica (mesmo comprador, produto, forma de pagamento, valor e parcelas) já está sendo processada:

{
  "error": "charge_in_progress",
  "message": "Uma cobrança idêntica está sendo processada. Aguarde alguns segundos e tente de novo.",
  "retryAfter": 5
}

Não é falha. Espere os segundos indicados no cabeçalho Retry-After e envie de novo: a repetição devolve a cobrança original, em vez de criar outra.

No SDK @garuhq/node 5.1.0 isso chega como GaruDuplicateChargeError, com os campos code (charge_in_progress ou charge_already_processed) e retryAfterSec. O SDK não repete sozinho — repetir uma chamada de cobrança por conta própria é exatamente o tipo de mágica que a 5.0.0 tirou do caminho.

import { GaruDuplicateChargeError } from '@garuhq/node';

try {
  await garu.charges.create({ productId, paymentMethod: 'creditCard', customer, idempotencyKey });
} catch (err) {
  if (err instanceof GaruDuplicateChargeError) {
    await sleep(err.retryAfterSec * 1000);
    // A repetição devolve a cobrança original.
  }
}

Continua valendo o que dissemos na v0.21.1: a proteção de verdade é passar idempotencyKey derivado de algo estável do seu domínio. Esta trava é a rede de segurança para quando isso não acontece.