Back

Como Prevenir Envios Duplos de Formulários

Como Prevenir Envios Duplos de Formulários

Um usuário clica em “Enviar” no seu formulário de checkout. Nada acontece imediatamente. Ele clica novamente. Agora você tem dois pedidos, duas cobranças e um cliente frustrado.

Este cenário ocorre constantemente em aplicações em produção. Usuários clicam duas vezes por hábito, redes apresentam latência imprevisível e dedos impacientes tocam repetidamente. O resultado: entradas duplicadas no banco de dados, transações falhadas e tickets de suporte.

Este artigo aborda como prevenir envios duplos de formulários usando proteção em camadas—combinando estratégias client-side com idempotência server-side para construir fluxos de envio que permanecem resilientes sob condições do mundo real.

Pontos-Chave

  • Desabilitar apenas o botão de envio é insuficiente—JavaScript pode falhar, usuários podem enviar via teclado, e bots contornam o frontend completamente.
  • Rastreie o estado de envio com atributos data para proteger contra envios por clique e teclado.
  • Sempre reabilite os controles do formulário em caso de erro para que os usuários possam tentar novamente sem recarregar a página.
  • Tokens de idempotência server-side são a proteção definitiva, garantindo que requisições duplicadas nunca produzam efeitos colaterais duplicados.
  • Proteção efetiva requer defesa em profundidade: medidas client-side melhoram a UX, enquanto deduplicação server-side garante correção.

Por Que Proteção de Camada Única Falha

Confiar apenas em desabilitar um botão de envio parece direto. Mas considere estes cenários:

  • JavaScript falha ao carregar ou executar
  • Usuários enviam via teclado (tecla Enter) antes que seu handler execute
  • Requisições de rede expiram e usuários recarregam a página
  • Bots ou scripts contornam seu frontend completamente

Medidas client-side melhoram a experiência do usuário, mas não podem garantir proteção. Validação server-side permanece essencial porque requisições podem se originar de qualquer lugar—navegadores, curl, aplicativos móveis ou atores maliciosos.

Boas práticas efetivas de envio de formulários requerem defesa em profundidade.

Estratégias Client-Side para Evitar Requisições Duplicadas em Formulários Web

Rastreie o Estado de Envio

Em vez de simplesmente desabilitar botões, rastreie se um formulário está sendo enviado no momento:

document.querySelectorAll('form').forEach(form => {
  form.addEventListener('submit', (e) => {
    if (form.dataset.submitting === 'true') {
      e.preventDefault();
      return;
    }
    form.dataset.submitting = 'true';
  });
});

Esta abordagem lida com cliques em botões e envios por teclado. Frameworks modernos frequentemente fornecem este estado automaticamente—o hook useFormStatus do React e padrões similares em outras bibliotecas expõem estados pendentes que você pode usar diretamente.

Forneça Feedback Visual Claro

Usuários reenviam quando não têm confiança de que sua ação foi registrada. Substitua incerteza por clareza:

  • Desabilite o botão de envio e mostre um indicador de carregamento
  • Altere o texto do botão para “Enviando…” ou exiba um spinner
  • Considere desabilitar o formulário inteiro para prevenir modificações nos campos
form[data-submitting="true"] button[type="submit"] {
  opacity: 0.6;
  cursor: not-allowed;
  pointer-events: none;
}

Trate Erros com Elegância

Um erro comum: desabilitar o botão permanentemente após uma falha no envio. Sempre reabilite os controles do formulário quando erros ocorrerem para que os usuários possam tentar novamente:

async function handleSubmit(form) {
  form.dataset.submitting = 'true';
  try {
    await submitForm(form);
  } catch (error) {
    form.dataset.submitting = 'false'; // Permite nova tentativa
    showError(error.message);
  }
}

Aplique Debounce em Envios Rápidos

Para formulários usando validação assíncrona ou processamento complexo, debouncing previne envios em rajada de usuários impacientes ou teclas Enter mantidas pressionadas:

let submitTimeout;
form.addEventListener('submit', (e) => {
  if (submitTimeout) {
    e.preventDefault();
    return;
  }
  submitTimeout = setTimeout(() => {
    submitTimeout = null;
  }, 2000);
});

Idempotência Server-Side: A Camada de Proteção Definitiva

Medidas client-side reduzem envios duplicados. Idempotência server-side elimina seu impacto.

Envio de Formulário Idempotente com Tokens

Gere um token único ao renderizar o formulário. Inclua-o como um campo oculto:

<input type="hidden" name="idempotency_key" value="abc123-unique-token">

No servidor, verifique se você já processou este token. Se sim, retorne a resposta em cache. Se não, processe a requisição e armazene o token.

Este padrão—às vezes chamado de deduplicação de requisições—garante que mesmo se requisições idênticas chegarem múltiplas vezes, apenas uma produz efeitos colaterais.

Por Que Isso Importa para Prevenir Requisições de API Duplicadas

Processadores de pagamento como Stripe exigem chaves de idempotência exatamente por este motivo. Falhas de rede, retentativas e timeouts podem fazer com que a mesma requisição chegue múltiplas vezes. Sem idempotência, você pode cobrar um cliente duas vezes.

O mesmo princípio se aplica a qualquer operação que altera estado: criar registros, enviar emails ou disparar workflows.

Juntando Tudo

Proteção efetiva camada estas estratégias:

  1. Frontend: Rastreie estado, desabilite controles, mostre feedback
  2. Frontend: Reabilite em erros, aplique debounce em envios rápidos
  3. Backend: Valide tokens de idempotência, deduplique requisições
  4. Backend: Retorne respostas consistentes para envios duplicados

Nenhuma técnica única é suficiente. Tratamento client-side melhora a UX, enquanto idempotência server-side garante correção.

Conclusão

Para prevenir envios duplos de formulários de forma confiável, combine feedback visual imediato com deduplicação de requisições server-side. Trate medidas client-side como melhorias de UX, não controles de segurança. Projete seus fluxos de envio assumindo que requisições chegarão múltiplas vezes—porque sob condições reais de rede, elas chegarão. Proteção em camadas não é opcional: é a única abordagem que se sustenta quando usuários, redes e casos extremos conspiram contra você.

Perguntas Frequentes

Não. Desabilitar o botão só funciona quando JavaScript carrega e executa corretamente. Usuários ainda podem enviar via tecla Enter, e bots ou scripts contornam o frontend completamente. Você precisa de idempotência server-side como proteção para garantir que requisições duplicadas nunca produzam efeitos colaterais duplicados.

Uma chave de idempotência é um token único gerado quando o formulário é renderizado e enviado junto com o envio. O servidor verifica se já processou aquele token. Se sim, retorna a resposta anterior em vez de processar a requisição novamente. Isso previne registros, cobranças ou outros efeitos colaterais duplicados.

Use ambos. Rastreamento de estado de envio com um atributo data protege contra cliques repetidos e envios por teclado durante um único ciclo de vida de requisição. Debouncing adiciona um cooldown baseado em tempo que captura reenvios em rajada. Juntos eles cobrem mais casos extremos do que qualquer técnica isolada.

Redefina o flag de estado de envio quando um erro ocorrer. Se você está usando um atributo data como dataset.submitting, defina-o de volta para false no seu bloco catch. Isso reabilita os controles do formulário para que os usuários possam corrigir quaisquer problemas e enviar novamente sem precisar recarregar a página.

Understand every bug

Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay