Skip to main content

displayed_sidebar: bankingApiSidebar

Errors & Retries

Este guia explica como a Banking API trata erros usando o padrão RFC7807, como implementar idempotência e estratégias de retry.

Padrão de Erro RFC7807

A Banking API segue o padrão RFC 7807 (Problem Details for HTTP APIs) para todas as respostas de erro. Isso garante consistência e facilita o tratamento programático.

Estrutura de Erro

Todas as respostas de erro seguem este formato:

{
"type": "https://api.bancodigital.com/errors/error-type",
"title": "Error Title",
"status": 400,
"detail": "Human-readable explanation",
"instance": "/public/gw_banking/v1/accounts/123",
"correlationId": "550e8400-e29b-41d4-a716-446655440000"
}

Campos

CampoTipoDescrição
typestring (URI)Identificador único do tipo de erro
titlestringTítulo curto e legível do erro
statusnumberCódigo HTTP do erro
detailstringExplicação detalhada do erro
instancestring (URI)URI que identifica a ocorrência específica
correlationIdstring (UUID)ID de correlação para rastreamento

Códigos HTTP

CódigoSignificadoQuando OcorreDeve Retryar?
400Bad RequestParâmetros inválidos❌ Não
401UnauthorizedToken ausente, inválido ou expirado✅ Sim (renovar token)
403ForbiddenToken válido mas sem permissão❌ Não
404Not FoundRecurso não encontrado❌ Não
429Too Many RequestsLimite de requisições excedido✅ Sim (com backoff)
500Internal Server ErrorErro interno do servidor✅ Sim (com backoff)
503Service UnavailableServiço temporariamente indisponível✅ Sim (com backoff)

Exemplos de Erros

400 - Bad Request

{
"type": "https://api.bancodigital.com/errors/invalid-parameter",
"title": "Invalid Parameter",
"status": 400,
"detail": "Account ID must be a positive number",
"instance": "/public/gw_banking/v1/accounts/invalid",
"correlationId": "550e8400-e29b-41d4-a716-446655440000"
}

Ação: Corrigir os parâmetros da requisição.

401 - Unauthorized

{
"type": "https://api.bancodigital.com/errors/token-expired",
"title": "Token Expired",
"status": 401,
"detail": "The access token has expired",
"instance": "/public/gw_banking/v1/accounts/123",
"correlationId": "550e8400-e29b-41d4-a716-446655440000"
}

Ação: Obter novo token e repetir a requisição.

404 - Not Found

{
"type": "https://api.bancodigital.com/errors/account-not-found",
"title": "Account Not Found",
"status": 404,
"detail": "Account with ID 123 not found",
"instance": "/public/gw_banking/v1/accounts/123",
"correlationId": "550e8400-e29b-41d4-a716-446655440000"
}

Ação: Verificar se o ID da conta está correto.

429 - Too Many Requests

{
"type": "https://api.bancodigital.com/errors/rate-limit-exceeded",
"title": "Rate Limit Exceeded",
"status": 429,
"detail": "Too many requests. Limit: 1000 per minute",
"instance": "/public/gw_banking/v1/accounts",
"correlationId": "550e8400-e29b-41d4-a716-446655440000"
}

Ação: Aguardar e retryar com backoff exponencial.

500 - Internal Server Error

{
"type": "https://api.bancodigital.com/errors/internal-error",
"title": "Internal Server Error",
"status": 500,
"detail": "An unexpected error occurred",
"instance": "/public/gw_banking/v1/accounts/123",
"correlationId": "550e8400-e29b-41d4-a716-446655440000"
}

Ação: Retryar com backoff exponencial. Se persistir, reportar com correlationId.

Implementação de Tratamento de Erros

Classe de Erro Customizada

class APIError extends Error {
constructor(
public type: string,
public title: string,
public status: number,
public detail: string,
public instance?: string,
public correlationId?: string,
) {
super(detail);
this.name = 'APIError';
}

static fromResponse(response: any): APIError {
return new APIError(
response.type,
response.title,
response.status,
response.detail,
response.instance,
response.correlationId,
);
}

shouldRetry(): boolean {
return [401, 429, 500, 503].includes(this.status);
}
}

Tratamento de Erros

async function handleRequest<T>(request: () => Promise<T>): Promise<T> {
try {
return await request();
} catch (error) {
if (axios.isAxiosError(error) && error.response) {
const apiError = APIError.fromResponse(error.response.data);

// Log com correlation ID
console.error('API Error:', {
type: apiError.type,
status: apiError.status,
correlationId: apiError.correlationId,
});

// Tratar por tipo
switch (apiError.status) {
case 400:
throw new Error(`Invalid request: ${apiError.detail}`);
case 401:
// Token expirado - renovar
throw new TokenExpiredError(apiError);
case 403:
throw new Error(`Forbidden: ${apiError.detail}`);
case 404:
throw new Error(`Not found: ${apiError.detail}`);
case 429:
throw new RateLimitError(apiError);
case 500:
case 503:
throw new ServerError(apiError);
default:
throw apiError;
}
}
throw error;
}
}

Idempotência

O que é Idempotência?

Uma operação é idempotente se executá-la múltiplas vezes produz o mesmo resultado que executá-la uma vez.

Como Usar Idempotência

Para operações que modificam estado, use o header Idempotency-Key:

const idempotencyKey = crypto.randomUUID();

const response = await axios.post(
`${apiBaseUrl}/public/gw_banking/v1/transactions`,
transactionData,
{
headers: {
'Idempotency-Key': idempotencyKey,
Authorization: `Bearer ${token}`,
},
},
);

Benefícios

  • Segurança: Evita duplicação acidental
  • Retry seguro: Pode retryar sem criar duplicatas
  • Idempotência garantida: Mesma requisição = mesma resposta

Exemplo de Uso

class TransactionService {
async createTransaction(data: TransactionData): Promise<Transaction> {
const idempotencyKey = this.getIdempotencyKey(data);

try {
return await this.api.post('/transactions', data, {
headers: { 'Idempotency-Key': idempotencyKey },
});
} catch (error) {
// Se erro 409 (Conflict), a transação já foi criada
if (error.status === 409) {
return this.getTransactionByIdempotencyKey(idempotencyKey);
}
throw error;
}
}

private getIdempotencyKey(data: TransactionData): string {
// Gerar key baseado em dados da transação
const hash = crypto
.createHash('sha256')
.update(JSON.stringify(data))
.digest('hex');
return hash;
}
}

Estratégia de Retry

Quando Retryar

  • 401 Unauthorized: Renovar token e retryar
  • 429 Too Many Requests: Aguardar e retryar
  • 500 Internal Server Error: Retryar com backoff
  • 503 Service Unavailable: Retryar com backoff
  • 400 Bad Request: Não retryar (erro do cliente)
  • 403 Forbidden: Não retryar (sem permissão)
  • 404 Not Found: Não retryar (recurso não existe)

Backoff Exponencial

async function retryWithBackoff<T>(
fn: () => Promise<T>,
maxRetries = 3,
baseDelay = 1000,
): Promise<T> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await fn();
} catch (error) {
if (attempt === maxRetries - 1) throw error;

// Backoff exponencial: 1s, 2s, 4s
const delay = baseDelay * Math.pow(2, attempt);
console.log(`Retry attempt ${attempt + 1} after ${delay}ms`);

await new Promise((resolve) => setTimeout(resolve, delay));
}
}
throw new Error('Max retries exceeded');
}

Retry com Jitter

Adicione jitter (aleatoriedade) para evitar "thundering herd":

function calculateDelay(attempt: number, baseDelay: number): number {
const exponentialDelay = baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * 0.3 * exponentialDelay; // 30% de jitter
return exponentialDelay + jitter;
}

Implementação Completa

class RetryableRequest {
async execute<T>(
request: () => Promise<T>,
options: {
maxRetries?: number;
baseDelay?: number;
retryableStatuses?: number[];
} = {},
): Promise<T> {
const {
maxRetries = 3,
baseDelay = 1000,
retryableStatuses = [429, 500, 503],
} = options;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await request();
} catch (error) {
if (attempt === maxRetries - 1) throw error;

const status = error.response?.status;

// Não retryar se não for retryable
if (!retryableStatuses.includes(status)) {
throw error;
}

// Tratar 401 separadamente (renovar token)
if (status === 401) {
await this.renewToken();
continue; // Retryar imediatamente
}

// Backoff exponencial com jitter
const delay = this.calculateDelay(attempt, baseDelay);
await new Promise((resolve) => setTimeout(resolve, delay));
}
}

throw new Error('Max retries exceeded');
}

private calculateDelay(attempt: number, baseDelay: number): number {
const exponentialDelay = baseDelay * Math.pow(2, attempt);
const jitter = Math.random() * 0.3 * exponentialDelay;
return exponentialDelay + jitter;
}

private async renewToken(): Promise<void> {
// Implementar renovação de token
}
}

Tratamento de Rate Limiting

Detectar Rate Limit

if (error.status === 429) {
const retryAfter = error.response.headers['retry-after'];
const delay = retryAfter ? parseInt(retryAfter) * 1000 : 60000;

await new Promise((resolve) => setTimeout(resolve, delay));
// Retryar requisição
}

Implementar Rate Limiter

class RateLimiter {
private queue: Array<() => Promise<any>> = [];
private processing = false;
private lastRequestTime = 0;
private minDelay = 100; // 100ms entre requisições

async execute<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push(async () => {
try {
const result = await fn();
resolve(result);
} catch (error) {
reject(error);
}
});

this.processQueue();
});
}

private async processQueue() {
if (this.processing) return;
this.processing = true;

while (this.queue.length > 0) {
const fn = this.queue.shift();
if (!fn) break;

// Respeitar rate limit
const now = Date.now();
const timeSinceLastRequest = now - this.lastRequestTime;
if (timeSinceLastRequest < this.minDelay) {
await new Promise((resolve) =>
setTimeout(resolve, this.minDelay - timeSinceLastRequest),
);
}

this.lastRequestTime = Date.now();
await fn();
}

this.processing = false;
}
}

Exemplo Completo

import axios, { AxiosError } from 'axios';

class BankingAPIClient {
private retryableRequest = new RetryableRequest();

async getBalance(accountId: string): Promise<BalanceResponse> {
return this.retryableRequest.execute(
async () => {
const token = await this.getToken();
const response = await axios.get(
`${this.baseURL}/public/gw_banking/v1/accounts/${accountId}/balance`,
{
headers: {
Authorization: `Bearer ${token}`,
'x-correlation-id': this.generateCorrelationId(),
},
},
);
return response.data;
},
{
maxRetries: 3,
baseDelay: 1000,
retryableStatuses: [401, 429, 500, 503],
},
);
}

private async getToken(): Promise<string> {
// Implementação de obtenção de token
}

private generateCorrelationId(): string {
return crypto.randomUUID();
}
}

Boas Práticas

1. Sempre Use Correlation ID

const correlationId = crypto.randomUUID();

try {
await makeRequest(correlationId);
} catch (error) {
console.error('Error:', {
correlationId,
error: error.message,
});
// Reportar erro com correlationId
}

2. Log Estruturado

logger.error(
{
errorType: error.type,
status: error.status,
correlationId: error.correlationId,
instance: error.instance,
},
error.detail,
);

3. Monitoramento

// Métricas de erro
metrics.increment('api.errors', {
type: error.type,
status: error.status.toString(),
});

Próximos Passos