CRM Oficina Martech
Guias

Erros e retry

Códigos HTTP, formato de erro flat, backoff exponencial com jitter e idempotência.

A API retorna erros em formato JSON flat (não nested). Todo erro tem error (mensagem humana) e às vezes code (string estável para máquina) e details (objeto com diagnóstico).

Formato de erro

{
  "error": "Mensagem humana legível",
  "code": "string_estavel_opcional",
  "details": { /* objeto opcional com diagnóstico */ }
}

Use o code para lógica programática. Use error para logs e mensagens de UI.

Códigos HTTP

HTTPQuando ocorreRetry?
400Parâmetros inválidos❌ Sem retry
401Autenticação ausente ou inválida❌ Sem retry
403Permissão insuficiente❌ Sem retry
404Recurso não encontrado❌ Sem retry
409Conflito (ex: race condition em escrita)✅ Sim, com idempotência
422Validação semântica falhou (regra de negócio)❌ Sem retry
429Rate limit excedido✅ Sim, respeitando Retry-After
500Erro interno do servidor✅ Sim, com backoff
502Gateway error (upstream temporário)✅ Sim, com backoff
503Serviço indisponível (manutenção ou degradação)✅ Sim, com backoff
504Timeout no gateway✅ Sim, com backoff

Estratégia de retry

Use backoff exponencial com jitter para evitar tempestades de retry.

async function fetchWithRetry(
  url: string,
  init: RequestInit,
  maxAttempts = 5,
): Promise<Response> {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const res = await fetch(url, init);

    if (res.ok) return res;
    if (![429, 500, 502, 503, 504].includes(res.status)) return res;
    if (attempt === maxAttempts) return res;

    const retryAfter = res.headers.get("Retry-After");
    const wait = retryAfter
      ? parseInt(retryAfter, 10) * 1000
      : Math.min(2 ** attempt * 100, 30_000) + Math.random() * 500;

    await new Promise((r) => setTimeout(r, wait));
  }

  throw new Error("unreachable");
}

Princípios

  1. Só retry em 429 + 5xx — 4xx (exceto 429) indicam bug do cliente
  2. Respeite Retry-After — se o header está presente, use o valor
  3. Jitter aleatório — adicione Math.random() * 500ms para evitar sincronização entre clientes
  4. Cap máximo — não espere mais que 30s entre tentativas
  5. Limite de tentativas — 5 tentativas geralmente bastam; depois disso, a falha é estrutural

Timeout sugerido por requisição: 15 segundos. Operações que excedem isso provavelmente indicam degradação — melhor falhar rápido e retry do que pendurar a conexão.

Idempotência

Para operações POST/PUT/DELETE (em desenvolvimento), use o header Idempotency-Key:

Idempotency-Key: 5a2c7e8f-d4b1-4c3a-9f5e-1a8b2c3d4e5f

O servidor cacheia o resultado por 24h. Repetir a mesma requisição com a mesma chave retorna o resultado original sem reexecutar.

Use um UUID v4 por intenção de negócio (não por tentativa). Em retry, mantenha a mesma chave.

Exemplo: tratamento completo

try {
  const res = await fetchWithRetry(url, {
    headers: { Authorization: `Bearer ${apiKey}` },
  });

  if (!res.ok) {
    const body = await res.json();

    if (body.code === "missing_scope") {
      // Falha estrutural — alerta o operador
      throw new ConfigurationError(body.error);
    }

    if (res.status === 400 && body.details) {
      // Bug no cliente — loga details para debug
      logger.error({ event: "api.bad_request", details: body.details });
      throw new BadRequestError(body.error);
    }

    throw new ApiError(`${res.status}: ${body.error}`);
  }

  return await res.json();
} catch (err) {
  if (err.name === "AbortError") {
    throw new TimeoutError("Request timed out after 15s");
  }
  throw err;
}