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
| HTTP | Quando ocorre | Retry? |
|---|---|---|
| 400 | Parâmetros inválidos | ❌ Sem retry |
| 401 | Autenticação ausente ou inválida | ❌ Sem retry |
| 403 | Permissão insuficiente | ❌ Sem retry |
| 404 | Recurso não encontrado | ❌ Sem retry |
| 409 | Conflito (ex: race condition em escrita) | ✅ Sim, com idempotência |
| 422 | Validação semântica falhou (regra de negócio) | ❌ Sem retry |
| 429 | Rate limit excedido | ✅ Sim, respeitando Retry-After |
| 500 | Erro interno do servidor | ✅ Sim, com backoff |
| 502 | Gateway error (upstream temporário) | ✅ Sim, com backoff |
| 503 | Serviço indisponível (manutenção ou degradação) | ✅ Sim, com backoff |
| 504 | Timeout 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
- Só retry em 429 + 5xx — 4xx (exceto 429) indicam bug do cliente
- Respeite
Retry-After— se o header está presente, use o valor - Jitter aleatório — adicione
Math.random() * 500mspara evitar sincronização entre clientes - Cap máximo — não espere mais que 30s entre tentativas
- 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-1a8b2c3d4e5fO 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;
}