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 POST | Sim, com Idempotency-Key | A mesma requisição, com a mesma chave. Sem a chave, você pode criar (e pagar) duas consultas. |
| Timeout ou erro de rede no GET | Sim | GET não cobra nem cria nada. |
429 RATE_LIMITED | Sim | Depois do Retry-After. |
Erro com retryable: true (409, 500, 502, 503, 504) | Sim | Com backoff e teto de tentativas. |
Erro com retryable: false (400, 401, 402, 404, 422) | Não | Corrija o pedido (codigo diz o quê). |
Consulta em processando | Não há o que repetir | Continue o polling ou espere o webhook. Não crie outra. |
Consulta em erro com retryable: true | Sim, como consulta nova | POST novo, com Idempotency-Key nova (…-t2). O valor da que falhou já foi estornado. |
Consulta em erro com retryable false ou ausente | Não automaticamente | Mostre o motivo; a decisão é humana. |
Timeouts
| Onde | Valor sugerido | Por quê |
|---|---|---|
| Cada requisição HTTP | 30 s | As respostas da API são rápidas; os endpoints síncronos de FIPE consultam a fonte na hora. |
| Polling de uma consulta | 3 a 5 s de intervalo, teto de 5 min | A base estadual pode levar cerca de 2 min numa placa fria; CRLV e ATPV-e, mais. |
| Seu endpoint de webhook | Responder em até 10 s | Acima 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
// 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.
TST0503 (erro retentável), TST0404 (definitivo), TST9999 (seu teto de espera) e placa inválida (400).