docs

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: true

Quando 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árioRetry?
HTTP 500–599Sim
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 failSim
TLS errorSim
Timeout do clienteSim

Backoff

Exponencial com jitter:

delay = min(maxDelay, baseDelay * 2^(attempt-1)) + random(0..100)ms

Defaults: 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: 5 em billing recorrente. Custo de latência irrelevante, robustez crítica.
  • maxRetries: 2-3 em checkout síncrono. Cliente esperando — melhor falhar logo e mostrar "tentar de novo".
  • Sempre passe idempotencyKey explí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ó statusZhexCardError não é retentável; ZhexAPIError é.
Esta página foi útil?

Atualizado em

Nesta página