Retry e idempotência
Auto-retry built-in no @zhexio/node ou helper fetchWithRetry — exponential backoff, mesma Idempotency-Key entre tentativas, respeito a Retry-After.
A Zhex retorna 5xx, 408, 429 e network errors idempotentes: você pode retentar com a mesma Idempotency-Key sem duplicar cobrança.
O SDK @zhexio/node faz isso por padrão; em fetch puro, é o helper canônico abaixo.
Como a Zhex sinaliza retry-safe
Cada response tem o header:
Zhex-Should-Retry: trueQuando presente (5xx e alguns 429), você pode retentar com mesma Idempotency-Key e a Zhex devolve a resposta cacheada se a chamada anterior já tiver completado, ou continua o processamento se ainda estava em andamento.
Caminho 1 — SDK
Auto-retry com idempotency reuse já é default. Você não precisa pensar:
const intent = await zhex.paymentIntents.create({
amount: 19900,
currency: 'brl',
customer: 'cus_…',
});
// SDK já tentou até 3 vezes em 429/5xx, replayando a mesma Idempotency-Key.Tunar o comportamento
// Globalmente no construtor
const zhex = new Zhex(SECRET, {
maxRetries: 5, // billing crítico
timeoutMs: 30_000, // workers podem esperar mais
});
// Ou por chamada
await zhex.refunds.create(
{ payment_intent: 'pi_…' },
{
idempotencyKey: 'refund:order-42', // explícita pra debug
maxRetries: 5,
},
);Detectar response replayada
Os erros tipados (ZhexAPIError, ZhexConnectionError, etc.) carregam requestId e code. Para inspeção fina (header Idempotent-Replayed: true, status code, body raw), use try/catch:
import { ZhexAPIError, ZhexCardError } from '@zhexio/node';
try {
await zhex.paymentIntents.create({ /* … */ });
} catch (err) {
if (err instanceof ZhexCardError) {
// exibir err.code ao cliente: 'card_declined', 'expired_card', …
} else if (err instanceof ZhexAPIError) {
// SDK já retentou e desistiu — log err.requestId, alerte oncall
} else {
throw err;
}
}Caminho 2 — Helper fetchWithRetry
Quando você não quer SDK ou está em outra linguagem:
import { setTimeout as sleep } from 'node:timers/promises';
import { randomUUID } from 'node:crypto';
type RetryOptions = {
maxAttempts?: number; // default 3
baseDelayMs?: number; // default 500
maxDelayMs?: number; // default 5_000
idempotencyKey?: string; // gerado se não passar
};
export async function fetchWithRetry(
url: string,
init: RequestInit & RetryOptions = {},
): Promise<Response> {
const {
maxAttempts = 3,
baseDelayMs = 500,
maxDelayMs = 5_000,
idempotencyKey = randomUUID(),
headers,
...rest
} = init;
// mesma key em TODAS as tentativas — crítico
const finalHeaders = {
...headers,
'Idempotency-Key': idempotencyKey,
};
let lastError: unknown;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
const res = await fetch(url, { ...rest, headers: finalHeaders });
// 2xx ou 4xx (exceto 408/429) → retorna direto
if (res.ok || (res.status < 500 && res.status !== 408 && res.status !== 429)) {
return res;
}
// Última tentativa → retorna o erro
if (attempt === maxAttempts) return res;
// Calcula delay
const retryAfter = res.headers.get('Retry-After');
const delay = retryAfter
? Number(retryAfter) * 1000
: Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1)) + Math.random() * 100;
console.warn(`[zhex] retry ${attempt}/${maxAttempts} em ${delay}ms (status ${res.status})`);
await sleep(delay);
} catch (err) {
// Network error → retry com backoff
lastError = err;
if (attempt === maxAttempts) throw err;
const delay = Math.min(maxDelayMs, baseDelayMs * 2 ** (attempt - 1)) + Math.random() * 100;
console.warn(`[zhex] retry ${attempt}/${maxAttempts} em ${delay}ms (network)`);
await sleep(delay);
}
}
throw lastError;
}Uso
const res = await fetchWithRetry('https://prometheus.zhex.io/v1/payment_intents', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ZHEX_SECRET_KEY}`,
'Content-Type': 'application/json',
},
idempotencyKey: 'order-123', // explícita: facilita debug
maxAttempts: 5, // billing crítico
body: JSON.stringify({
amount: 19900,
currency: 'brl',
customer: 'cus_…',
}),
});
const intent = await res.json();O que dispara retry
| Cenário | Retry? |
|---|---|
| HTTP 500–599 | Sim |
| HTTP 408 (timeout) | Sim |
| HTTP 429 (rate limit) | Sim, com backoff respeitando Retry-After |
| HTTP 4xx (exceto 408/429) | Não (erro de validação, não vai mudar) |
| Connection error / DNS fail | Sim |
| TLS error | Sim |
| Timeout do cliente | Sim |
Backoff
Exponencial com jitter:
delay = min(maxDelay, baseDelay * 2^(attempt-1)) + random(0..100)msDefaults: baseDelay: 500ms, maxDelay: 5_000ms. Em 3 tentativas: ~500ms, ~1s, ~2s. Em 5: até ~8s na última. Use 5+ em workers de billing crítico (assinaturas, dunning); 2-3 em endpoints user-facing.
Idempotency-Key — sempre a mesma entre retries
Esta é a regra de ouro: toda tentativa do mesmo request lógico usa a mesma Idempotency-Key. Se você gerar uma nova a cada retry, a Zhex trata como request novo — e você duplica cobrança.
// ✅ correto: key gerada uma vez, reutilizada
const key = randomUUID();
for (let i = 0; i < 3; i++) {
await fetch(url, { headers: { 'Idempotency-Key': key, ... }, ... });
}
// ❌ errado: key nova a cada tentativa = duplicação garantida
for (let i = 0; i < 3; i++) {
await fetch(url, { headers: { 'Idempotency-Key': randomUUID(), ... }, ... });
}O SDK e o fetchWithRetry cuidam disso por você.
Detectar response replayada
Quando a Zhex devolve cache da chave, manda no header:
Idempotent-Replayed: trueÚtil pra log/debug:
const res = await fetchWithRetry(url, { /* ... */ });
if (res.headers.get('Idempotent-Replayed') === 'true') {
console.log('Retry capturou response cacheada — operação só executou 1×');
}Disable retry (raros casos)
Se você tem lógica própria mais sofisticada (BullMQ + DLQ, Temporal, AWS Step Functions):
// SDK
const zhex = new Zhex(SECRET, { maxRetries: 0 });
// fetch puro
const res = await fetchWithRetry(url, { ...init, maxAttempts: 1 });Ou use fetch direto. Mas continue passando Idempotency-Key — ela protege contra retry implícito do load balancer / proxy.
Observabilidade
import * as Sentry from '@sentry/node';
try {
await zhex.paymentIntents.create({ /* … */ });
} catch (err) {
if (err instanceof ZhexAPIError) {
Sentry.captureException(err, {
tags: { zhex_request_id: err.requestId, zhex_code: err.code },
});
}
throw err;
}Em produção, alarme se taxa de erro 5xx persistente da Zhex > 5% — sinal de instabilidade na Zhex ou na sua rede.
Boas práticas
maxRetries: 5em billing recorrente. Custo de latência irrelevante, robustez crítica.maxRetries: 2-3em checkout síncrono. Cliente esperando — melhor falhar logo e mostrar "tentar de novo".- Sempre passe
idempotencyKeyexplícita em POSTs financeiros — facilita debug. - Não combine retry de transporte com retry de aplicação sem cuidado — garanta que ambos respeitem a mesma key.
- Cape o tipo de erro, não só status —
ZhexCardErrornão é retentável;ZhexAPIErroré.
Atualizado em