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
| Campo | Tipo | Descrição |
|---|---|---|
type | string (URI) | Identificador único do tipo de erro |
title | string | Título curto e legível do erro |
status | number | Código HTTP do erro |
detail | string | Explicação detalhada do erro |
instance | string (URI) | URI que identifica a ocorrência específica |
correlationId | string (UUID) | ID de correlação para rastreamento |
Códigos HTTP
| Código | Significado | Quando Ocorre | Deve Retryar? |
|---|---|---|---|
400 | Bad Request | Parâmetros inválidos | ❌ Não |
401 | Unauthorized | Token ausente, inválido ou expirado | ✅ Sim (renovar token) |
403 | Forbidden | Token válido mas sem permissão | ❌ Não |
404 | Not Found | Recurso não encontrado | ❌ Não |
429 | Too Many Requests | Limite de requisições excedido | ✅ Sim (com backoff) |
500 | Internal Server Error | Erro interno do servidor | ✅ Sim (com backoff) |
503 | Service Unavailable | Serviç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
- Getting Started - Voltar ao básico
- Balance Flow - Ver fluxo completo