Pular para o conteúdo
ZapCar
Developersv1

Resiliência e erros

Timeouts, retry com backoff, idempotência e como não pagar duas vezes pelo mesmo resultado.

Uma integração resiliente separa três coisas que parecem iguais: repetir uma requisição que não teve resposta, esperar uma consulta que ainda está processando e criar uma consulta nova depois de uma falha. Cada uma tem regra própria, e trocar uma pela outra custa dinheiro ou perde resultado.

O que repetir, e como

SituaçãoÉ seguro repetir?Como
Timeout ou erro de rede no POSTSim, com Idempotency-KeyA mesma requisição, com a mesma chave. Sem a chave, você pode criar (e pagar) duas consultas.
Timeout ou erro de rede no GETSimGET não cobra nem cria nada.
429 RATE_LIMITEDSimDepois do Retry-After.
Erro com retryable: true (409, 500, 502, 503, 504)SimCom backoff e teto de tentativas.
Erro com retryable: false (400, 401, 402, 404, 422)NãoCorrija o pedido (codigo diz o quê).
Consulta em processandoNão há o que repetirContinue o polling ou espere o webhook. Não crie outra.
Consulta em erro com retryable: trueSim, como consulta novaPOST novo, com Idempotency-Key nova (…-t2). O valor da que falhou já foi estornado.
Consulta em erro com retryable false ou ausenteNão automaticamenteMostre o motivo; a decisão é humana.

Timeouts

OndeValor sugeridoPor quê
Cada requisição HTTP30 sAs respostas da API são rápidas; os endpoints síncronos de FIPE consultam a fonte na hora.
Polling de uma consulta3 a 5 s de intervalo, teto de 5 minA base estadual pode levar cerca de 2 min numa placa fria; CRLV e ATPV-e, mais.
Seu endpoint de webhookResponder em até 10 sAcima disso a entrega conta como falha e é reenviada.

Idempotência na prática

A chave nasce do seu identificador e da tentativa: pedido-984281-t1. Ela é estável enquanto você estiver repetindo a mesma requisição e muda quando você decide fazer uma consulta nova. Grave a chave antes de chamar a API: se o processo cair, o reprocessamento lê a chave gravada em vez de gerar outra. Ver as regras.

Código

Node.jsretry com backoff, 429 e retryable
// Política de retry para chamadas à ZapCar (Node.js 18+).
const BASE = "https://api.zapcarconsulta.com.br";

// Backoff exponencial com jitter: 1 s, 2 s, 4 s… (teto de 30 s), sorteado
// entre 50% e 100% do valor para várias instâncias não repetirem juntas.
const espera = (tentativa) => {
  const teto = Math.min(30_000, 1000 * 2 ** tentativa);
  return teto / 2 + Math.random() * (teto / 2);
};
const dormir = (ms) => new Promise((r) => setTimeout(r, ms));

/**
 * Repete a MESMA requisição quando é seguro: erro de rede, timeout, 429 e
 * respostas com retryable: true. Em POST que cobra, só chame com
 * Idempotency-Key: é ela que torna a repetição segura.
 */
async function chamarComRetry(caminho, opcoes = {}, maxTentativas = 4) {
  for (let t = 0; ; t++) {
    let resp;
    try {
      resp = await fetch(BASE + caminho, {
        ...opcoes,
        headers: { Authorization: `Bearer ${process.env.ZAPCAR_API_KEY}`, ...(opcoes.headers ?? {}) },
        signal: AbortSignal.timeout(30_000),
      });
    } catch (erroDeRede) {
      // Não sabemos se o servidor processou. Com Idempotency-Key, repetir é seguro.
      if (t + 1 >= maxTentativas) throw erroDeRede;
      await dormir(espera(t));
      continue;
    }
    if (resp.status === 429 && t + 1 < maxTentativas) {
      await dormir((Number(resp.headers.get("Retry-After")) || 5) * 1000);
      continue;
    }
    const corpo = await resp.json().catch(() => ({}));
    if (resp.ok) return { status: resp.status, corpo };
    if (corpo.retryable === true && t + 1 < maxTentativas) {
      await dormir(espera(t));
      continue;
    }
    // retryable false (ou tentativas esgotadas): corrigir o pedido, não repetir.
    const erro = new Error(corpo.erro ?? `HTTP ${resp.status}`);
    Object.assign(erro, { status: resp.status, codigo: corpo.codigo, request_id: corpo.request_id });
    throw erro;
  }
}

// Uso: a chave de idempotência é estável dentro da tentativa de CONSULTA.
// const { corpo } = await chamarComRetry("/v1/consultas", {
//   method: "POST",
//   headers: { "Content-Type": "application/json", "Idempotency-Key": "pedido-984281-t1" },
//   body: JSON.stringify({ servico: "consulta", placa: "ABC1D23" }),
// });

Limite de tentativas

Retry sem teto vira prejuízo, não resiliência. Três tentativas de consulta e quatro de requisição bastam. Se o mesmo serviço falha seguidamente, pare de criar consultas novas por alguns minutos (um disjuntor do seu lado) e acompanhe a página de status.

Dica:
Teste cada linha da tabela no sandbox: TST0503 (erro retentável), TST0404 (definitivo), TST9999 (seu teto de espera) e placa inválida (400).