Skip to main content

Tratamento de Erros

Um tratamento adequado de erros é essencial para criar aplicações robustas que usam a API Speedio. Este guia apresenta estratégias e implementações para lidar com diferentes tipos de erros.

Tipos de Erros

1. Erros de Autenticação

401 Unauthorized
error
Credenciais inválidas ou expiradasCausas comuns:
  • Username ou password incorretos
  • Credenciais expiradas
  • Header Authorization malformado
403 Forbidden
error
Acesso negado ao recursoCausas comuns:
  • Conta sem permissão para o endpoint
  • Plano não inclui a funcionalidade
  • IP bloqueado

2. Erros de Requisição

400 Bad Request
error
Requisição malformada ou parâmetros inválidosCausas comuns:
  • JSON inválido no corpo da requisição
  • Parâmetros obrigatórios ausentes
  • Formato de dados incorreto
422 Unprocessable Entity
error
Dados válidos mas não processáveisCausas comuns:
  • CNPJ com formato correto mas inválido
  • E-mail com sintaxe correta mas domínio inexistente

3. Erros de Rate Limiting

429 Too Many Requests
error
Limite de requisições excedidoHeaders importantes:
  • Retry-After: Tempo para tentar novamente
  • X-RateLimit-Remaining: Requisições restantes
  • X-RateLimit-Reset: Quando o limite reseta

4. Erros do Servidor

500 Internal Server Error
error
Erro interno do servidor
502 Bad Gateway
error
Problema no gateway/proxy
503 Service Unavailable
error
Serviço temporariamente indisponível
504 Gateway Timeout
error
Timeout no gateway

Estratégias de Tratamento

1. Hierarquia de Classes de Erro

// Classe base para erros da API Speedio
class SpeedioAPIError extends Error {
  constructor(message, status, response, originalError = null) {
    super(message);
    this.name = 'SpeedioAPIError';
    this.status = status;
    this.response = response;
    this.originalError = originalError;
    this.timestamp = new Date().toISOString();
  }
  
  toJSON() {
    return {
      name: this.name,
      message: this.message,
      status: this.status,
      timestamp: this.timestamp,
      response: this.response ? {
        status: this.response.status,
        data: this.response.data
      } : null
    };
  }
}

// Erros específicos
class AuthenticationError extends SpeedioAPIError {
  constructor(message, response) {
    super(message, 401, response);
    this.name = 'AuthenticationError';
  }
}

class AuthorizationError extends SpeedioAPIError {
  constructor(message, response) {
    super(message, 403, response);
    this.name = 'AuthorizationError';
  }
}

class ValidationError extends SpeedioAPIError {
  constructor(message, response, validationErrors = []) {
    super(message, 400, response);
    this.name = 'ValidationError';
    this.validationErrors = validationErrors;
  }
}

class RateLimitError extends SpeedioAPIError {
  constructor(message, response, retryAfter = null) {
    super(message, 429, response);
    this.name = 'RateLimitError';
    this.retryAfter = retryAfter;
  }
}

class ServerError extends SpeedioAPIError {
  constructor(message, status, response) {
    super(message, status, response);
    this.name = 'ServerError';
  }
}

class NetworkError extends SpeedioAPIError {
  constructor(message, originalError) {
    super(message, 0, null, originalError);
    this.name = 'NetworkError';
  }
}

2. Interceptador de Erros

function parseSpeedioError(error) {
  if (error.response) {
    // Erro HTTP da API
    const { status, data } = error.response;
    const message = data?.message || data?.error || 'Erro na API';
    
    switch (status) {
      case 401:
        return new AuthenticationError(message, error.response);
      
      case 403:
        return new AuthorizationError(message, error.response);
      
      case 400:
        return new ValidationError(
          message, 
          error.response,
          data?.validation_errors || []
        );
      
      case 422:
        return new ValidationError(
          'Dados não processáveis', 
          error.response,
          data?.errors || []
        );
      
      case 429:
        const retryAfter = error.response.headers['retry-after'];
        return new RateLimitError(
          message, 
          error.response,
          retryAfter ? parseInt(retryAfter) : null
        );
      
      case 500:
      case 502:
      case 503:
      case 504:
        return new ServerError(message, status, error.response);
      
      default:
        return new SpeedioAPIError(message, status, error.response);
    }
  } else if (error.request) {
    // Erro de rede
    return new NetworkError('Erro de conectividade', error);
  } else {
    // Erro de configuração
    return new SpeedioAPIError('Erro de configuração da requisição', 0, null, error);
  }
}

// Interceptador para Axios
axios.interceptors.response.use(
  response => response,
  error => {
    throw parseSpeedioError(error);
  }
);

3. Retry com Backoff Exponencial

class RetryManager {
  constructor(options = {}) {
    this.maxRetries = options.maxRetries || 3;
    this.baseDelay = options.baseDelay || 1000; // 1 segundo
    this.maxDelay = options.maxDelay || 30000; // 30 segundos
    this.backoffFactor = options.backoffFactor || 2;
    this.retryableErrors = options.retryableErrors || [
      'NetworkError',
      'ServerError',
      'RateLimitError'
    ];
  }
  
  async executeWithRetry(fn, context = {}) {
    let lastError;
    
    for (let attempt = 1; attempt <= this.maxRetries + 1; attempt++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error;
        
        // Não tentar novamente se não for um erro "retryável"
        if (!this.isRetryableError(error)) {
          throw error;
        }
        
        // Não tentar novamente se é a última tentativa
        if (attempt > this.maxRetries) {
          break;
        }
        
        const delay = this.calculateDelay(attempt, error);
        
        console.warn(`❌ Tentativa ${attempt}/${this.maxRetries + 1} falhou: ${error.message}`);
        console.info(`⏳ Aguardando ${delay / 1000}s antes da próxima tentativa...`);
        
        await this.sleep(delay);
      }
    }
    
    throw lastError;
  }
  
  isRetryableError(error) {
    return this.retryableErrors.includes(error.name);
  }
  
  calculateDelay(attempt, error) {
    // Para RateLimitError, respeitar o Retry-After
    if (error.name === 'RateLimitError' && error.retryAfter) {
      return error.retryAfter * 1000;
    }
    
    // Backoff exponencial com jitter
    const exponentialDelay = this.baseDelay * Math.pow(this.backoffFactor, attempt - 1);
    const jitter = Math.random() * 0.1 * exponentialDelay; // 10% de jitter
    
    return Math.min(exponentialDelay + jitter, this.maxDelay);
  }
  
  sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// Uso do RetryManager
const retryManager = new RetryManager({
  maxRetries: 3,
  baseDelay: 1000,
  maxDelay: 30000
});

async function buscarEmpresaComRetry(cnpj) {
  return await retryManager.executeWithRetry(async () => {
    const response = await axios.get(
      'https://api-get-leads.speedio.com.br/search_enriched_leads/cnpj',
      {
        params: { cnpjs: JSON.stringify([cnpj]) },
        headers: {
          'Authorization': `Basic ${auth}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    return response.data[0] || null;
  });
}

4. Circuit Breaker

class CircuitBreaker {
  constructor(options = {}) {
    this.failureThreshold = options.failureThreshold || 5;
    this.recoveryTimeout = options.recoveryTimeout || 60000; // 1 minuto
    this.monitoringPeriod = options.monitoringPeriod || 120000; // 2 minutos
    
    this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
    this.failureCount = 0;
    this.lastFailureTime = null;
    this.successCount = 0;
    
    this.resetCounters();
  }
  
  async execute(fn) {
    if (this.state === 'OPEN') {
      if (Date.now() - this.lastFailureTime >= this.recoveryTimeout) {
        this.state = 'HALF_OPEN';
        console.info('🔄 Circuit breaker em estado HALF_OPEN, testando recuperação...');
      } else {
        throw new Error('Circuit breaker OPEN - serviço indisponível');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  onSuccess() {
    this.failureCount = 0;
    
    if (this.state === 'HALF_OPEN') {
      this.successCount++;
      
      // Após 3 sucessos consecutivos, voltar ao estado CLOSED
      if (this.successCount >= 3) {
        this.state = 'CLOSED';
        this.successCount = 0;
        console.info('✅ Circuit breaker FECHADO - serviço recuperado');
      }
    }
  }
  
  onFailure() {
    this.failureCount++;
    this.lastFailureTime = Date.now();
    
    if (this.state === 'HALF_OPEN') {
      this.state = 'OPEN';
      this.successCount = 0;
      console.warn('🔴 Circuit breaker ABERTO - falha durante teste de recuperação');
    } else if (this.failureCount >= this.failureThreshold) {
      this.state = 'OPEN';
      console.warn(`🔴 Circuit breaker ABERTO - ${this.failureCount} falhas consecutivas`);
    }
  }
  
  getStatus() {
    return {
      state: this.state,
      failureCount: this.failureCount,
      successCount: this.successCount,
      lastFailureTime: this.lastFailureTime,
      nextRetryTime: this.state === 'OPEN' ? 
        this.lastFailureTime + this.recoveryTimeout : null
    };
  }
  
  resetCounters() {
    setInterval(() => {
      if (this.state === 'CLOSED') {
        this.failureCount = 0;
      }
    }, this.monitoringPeriod);
  }
}

// Integração com RetryManager
class ResilientClient {
  constructor() {
    this.retryManager = new RetryManager({
      maxRetries: 3,
      baseDelay: 1000
    });
    
    this.circuitBreaker = new CircuitBreaker({
      failureThreshold: 5,
      recoveryTimeout: 60000
    });
  }
  
  async request(fn) {
    return await this.circuitBreaker.execute(async () => {
      return await this.retryManager.executeWithRetry(fn);
    });
  }
  
  getHealthStatus() {
    return {
      circuitBreaker: this.circuitBreaker.getStatus(),
      timestamp: new Date().toISOString()
    };
  }
}

const client = new ResilientClient();

async function buscarEmpresaResilient(cnpj) {
  return await client.request(async () => {
    const response = await axios.get(
      'https://api-get-leads.speedio.com.br/search_enriched_leads/cnpj',
      {
        params: { cnpjs: JSON.stringify([cnpj]) },
        headers: {
          'Authorization': `Basic ${auth}`,
          'Content-Type': 'application/json'
        }
      }
    );
    
    return response.data[0] || null;
  });
}

5. Logs Estruturados de Erro

class ErrorLogger {
  constructor(options = {}) {
    this.logLevel = options.logLevel || 'error';
    this.includeStackTrace = options.includeStackTrace !== false;
    this.maxStackLines = options.maxStackLines || 10;
  }
  
  logError(error, context = {}) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level: 'ERROR',
      error: {
        name: error.name,
        message: error.message,
        status: error.status || null,
        ...(error instanceof SpeedioAPIError && {
          response: error.response,
          originalError: error.originalError?.message
        })
      },
      context,
      ...(this.includeStackTrace && error.stack && {
        stack: error.stack.split('\n').slice(0, this.maxStackLines)
      })
    };
    
    console.error(JSON.stringify(logEntry, null, 2));
    
    // Enviar para serviço de monitoramento (ex: Sentry, DataDog)
    this.sendToMonitoring(logEntry);
  }
  
  logRecovery(error, attempt, success = false) {
    const logEntry = {
      timestamp: new Date().toISOString(),
      level: success ? 'INFO' : 'WARN',
      event: success ? 'ERROR_RECOVERY' : 'RETRY_ATTEMPT',
      error: {
        name: error.name,
        message: error.message,
        status: error.status
      },
      attempt,
      success
    };
    
    console.log(JSON.stringify(logEntry));
  }
  
  sendToMonitoring(logEntry) {
    // Implementar integração com Sentry, DataDog, etc.
    // Exemplo com Sentry:
    // Sentry.captureException(error, { extra: context });
  }
}

const errorLogger = new ErrorLogger();

// Integrar com o cliente resiliente
class LoggingResilientClient extends ResilientClient {
  async request(fn, context = {}) {
    try {
      const result = await super.request(fn);
      return result;
    } catch (error) {
      errorLogger.logError(error, context);
      throw error;
    }
  }
}

Estratégias Avançadas

1. Fallback e Degradação Graceful

class FallbackManager {
  constructor() {
    this.fallbackStrategies = new Map();
  }
  
  registerFallback(operation, fallbackFn) {
    this.fallbackStrategies.set(operation, fallbackFn);
  }
  
  async executeWithFallback(operation, primaryFn, context = {}) {
    try {
      return await primaryFn();
    } catch (error) {
      console.warn(`❌ Operação principal falhou: ${error.message}`);
      
      const fallback = this.fallbackStrategies.get(operation);
      if (fallback) {
        console.info('🔄 Executando estratégia de fallback...');
        try {
          return await fallback(context, error);
        } catch (fallbackError) {
          console.error(`❌ Fallback também falhou: ${fallbackError.message}`);
          throw error; // Lançar erro original
        }
      }
      
      throw error;
    }
  }
}

const fallbackManager = new FallbackManager();

// Registrar fallbacks
fallbackManager.registerFallback('buscar_empresa', async (context, error) => {
  // Retornar dados do cache local se disponível
  const cachedData = localStorage.getItem(`empresa_${context.cnpj}`);
  if (cachedData) {
    console.info('📋 Usando dados do cache local');
    return JSON.parse(cachedData);
  }
  
  // Retornar dados básicos do CNPJ se possível
  if (context.cnpj) {
    console.info('📊 Retornando dados básicos do CNPJ');
    return {
      cnpj: context.cnpj,
      status: 'dados_limitados',
      source: 'fallback'
    };
  }
  
  throw error;
});

// Uso
async function buscarEmpresaComFallback(cnpj) {
  return await fallbackManager.executeWithFallback(
    'buscar_empresa',
    () => buscarEmpresaResilient(cnpj),
    { cnpj }
  );
}

2. Health Check e Monitoring

class HealthMonitor {
  constructor(options = {}) {
    this.checkInterval = options.checkInterval || 30000; // 30 segundos
    this.healthThreshold = options.healthThreshold || 0.8; // 80% de sucesso
    this.windowSize = options.windowSize || 50; // Últimas 50 requisições
    
    this.requestHistory = [];
    this.isHealthy = true;
    this.lastHealthCheck = null;
    
    this.startMonitoring();
  }
  
  recordRequest(success, responseTime, error = null) {
    const record = {
      timestamp: Date.now(),
      success,
      responseTime,
      error: error ? error.name : null
    };
    
    this.requestHistory.push(record);
    
    // Manter apenas as últimas N requisições
    if (this.requestHistory.length > this.windowSize) {
      this.requestHistory.shift();
    }
    
    this.checkHealth();
  }
  
  checkHealth() {
    if (this.requestHistory.length < 10) {
      return; // Não temos dados suficientes
    }
    
    const recentRequests = this.requestHistory.slice(-this.windowSize);
    const successRate = recentRequests.filter(r => r.success).length / recentRequests.length;
    
    const wasHealthy = this.isHealthy;
    this.isHealthy = successRate >= this.healthThreshold;
    this.lastHealthCheck = Date.now();
    
    if (wasHealthy !== this.isHealthy) {
      const status = this.isHealthy ? 'SAUDÁVEL' : 'DEGRADADO';
      console.log(`🏥 Status da API mudou para: ${status} (taxa de sucesso: ${(successRate * 100).toFixed(1)}%)`);
    }
  }
  
  getHealthStatus() {
    const recentRequests = this.requestHistory.slice(-this.windowSize);
    const successCount = recentRequests.filter(r => r.success).length;
    const avgResponseTime = recentRequests.reduce((sum, r) => sum + r.responseTime, 0) / recentRequests.length;
    
    return {
      healthy: this.isHealthy,
      successRate: recentRequests.length > 0 ? successCount / recentRequests.length : 0,
      averageResponseTime: avgResponseTime || 0,
      totalRequests: this.requestHistory.length,
      recentRequests: recentRequests.length,
      lastHealthCheck: this.lastHealthCheck,
      timestamp: Date.now()
    };
  }
  
  startMonitoring() {
    setInterval(() => {
      const status = this.getHealthStatus();
      
      if (!status.healthy) {
        console.warn('⚠️ API em estado degradado:', status);
      }
      
      // Enviar métricas para sistema de monitoramento
      this.sendMetrics(status);
    }, this.checkInterval);
  }
  
  sendMetrics(status) {
    // Enviar para Prometheus, DataDog, etc.
    // prometheus.register.getSingleMetric('api_health').set(status.healthy ? 1 : 0);
    // prometheus.register.getSingleMetric('api_success_rate').set(status.successRate);
  }
}

const healthMonitor = new HealthMonitor();

// Integrar com cliente resiliente
class MonitoredResilientClient extends LoggingResilientClient {
  async request(fn, context = {}) {
    const startTime = Date.now();
    
    try {
      const result = await super.request(fn, context);
      const responseTime = Date.now() - startTime;
      
      healthMonitor.recordRequest(true, responseTime);
      return result;
    } catch (error) {
      const responseTime = Date.now() - startTime;
      
      healthMonitor.recordRequest(false, responseTime, error);
      throw error;
    }
  }
  
  getHealthStatus() {
    return {
      ...super.getHealthStatus(),
      health: healthMonitor.getHealthStatus()
    };
  }
}

Implementação Completa

Cliente Final com Todas as Funcionalidades

class SpeedioClient {
  constructor(options = {}) {
    this.username = options.username || process.env.SPEEDIO_USERNAME;
    this.password = options.password || process.env.SPEEDIO_PASSWORD;
    this.baseUrl = options.baseUrl || 'https://api-get-leads.speedio.com.br';
    
    if (!this.username || !this.password) {
      throw new Error('Credenciais Speedio são obrigatórias');
    }
    
    this.auth = Buffer.from(`${this.username}:${this.password}`).toString('base64');
    
    // Componentes
    this.retryManager = new RetryManager(options.retry);
    this.circuitBreaker = new CircuitBreaker(options.circuitBreaker);
    this.fallbackManager = new FallbackManager();
    this.healthMonitor = new HealthMonitor(options.health);
    this.errorLogger = new ErrorLogger(options.logging);
    
    this.setupInterceptors();
    this.registerFallbacks();
  }
  
  setupInterceptors() {
    axios.interceptors.response.use(
      response => response,
      error => {
        throw parseSpeedioError(error);
      }
    );
  }
  
  registerFallbacks() {
    this.fallbackManager.registerFallback('buscar_cnpj', async (context, error) => {
      // Implementar estratégia de fallback para busca de CNPJ
      const cachedData = this.getFromCache(`cnpj_${context.cnpj}`);
      if (cachedData) {
        return cachedData;
      }
      throw error;
    });
  }
  
  async buscarCNPJ(cnpjs, options = {}) {
    const context = { operation: 'buscar_cnpj', cnpjs };
    
    return await this.executeRequest(async () => {
      const response = await axios.get(`${this.baseUrl}/search_enriched_leads/cnpj`, {
        params: { cnpjs: JSON.stringify(cnpjs) },
        headers: this.getHeaders(),
        timeout: options.timeout || 10000
      });
      
      return response.data;
    }, context);
  }
  
  async validarDados(dados, options = {}) {
    const context = { operation: 'validar_dados', count: dados.length };
    
    return await this.executeRequest(async () => {
      const response = await axios.post(`${this.baseUrl}/validate`, 
        { data: dados },
        {
          headers: this.getHeaders(),
          timeout: options.timeout || 15000
        }
      );
      
      return response.data;
    }, context);
  }
  
  async executeRequest(fn, context = {}) {
    const startTime = Date.now();
    
    try {
      const result = await this.fallbackManager.executeWithFallback(
        context.operation,
        () => this.circuitBreaker.execute(
          () => this.retryManager.executeWithRetry(fn)
        ),
        context
      );
      
      const responseTime = Date.now() - startTime;
      this.healthMonitor.recordRequest(true, responseTime);
      
      return result;
    } catch (error) {
      const responseTime = Date.now() - startTime;
      
      this.healthMonitor.recordRequest(false, responseTime, error);
      this.errorLogger.logError(error, context);
      
      throw error;
    }
  }
  
  getHeaders() {
    return {
      'Authorization': `Basic ${this.auth}`,
      'Content-Type': 'application/json',
      'User-Agent': 'Speedio-Client/1.0'
    };
  }
  
  getStatus() {
    return {
      circuitBreaker: this.circuitBreaker.getStatus(),
      health: this.healthMonitor.getHealthStatus(),
      timestamp: new Date().toISOString()
    };
  }
  
  // Métodos de cache (implementar conforme necessário)
  getFromCache(key) {
    // Implementar cache (localStorage, Redis, etc.)
    return null;
  }
  
  setCache(key, data, ttl = 3600) {
    // Implementar cache
  }
}

// Uso do cliente completo
const client = new SpeedioClient({
  retry: {
    maxRetries: 3,
    baseDelay: 1000
  },
  circuitBreaker: {
    failureThreshold: 5,
    recoveryTimeout: 60000
  },
  health: {
    checkInterval: 30000,
    healthThreshold: 0.8
  },
  logging: {
    logLevel: 'error',
    includeStackTrace: true
  }
});

// Exemplos de uso
try {
  const empresas = await client.buscarCNPJ(['21071712000171']);
  console.log('✅ Empresas encontradas:', empresas.length);
} catch (error) {
  if (error instanceof AuthenticationError) {
    console.error('❌ Problema de autenticação - verifique credenciais');
  } else if (error instanceof RateLimitError) {
    console.error(`❌ Rate limit - tente novamente em ${error.retryAfter}s`);
  } else {
    console.error('❌ Erro inesperado:', error.message);
  }
}

// Monitorar status
setInterval(() => {
  const status = client.getStatus();
  if (!status.health.healthy) {
    console.warn('⚠️ API em estado degradado');
  }
}, 60000);

Próximos Passos

Importante: O tratamento de erros robusto é fundamental para aplicações em produção. Implemente pelo menos retry com backoff exponencial e circuit breaker para garantir a resiliência da sua aplicação.