Implementação de Retry com Backoff Exponencial para Requisições HTTP

  1. Versão Básica - Mecanismo de Retry com Atraso Exponencial


interface ParametrosRetry {
  tentativasMaximas?: number;
  atrasoBase?: number;
  timeoutRequisicao?: number;
  condicaoRetry?: (erro: any) => boolean;
}

class GerenciadorRequisicaoRetry {
  private parametros: Required<ParametrosRetry>;

  constructor(parametros: ParametrosRetry = {}) {
    this.parametros = {
      tentativasMaximas: parametros.tentativasMaximas ?? 3,
      atrasoBase: parametros.atrasoBase ?? 1000,
      timeoutRequisicao: parametros.timeoutRequisicao ?? 10000,
      condicaoRetry: parametros.condicaoRetry ?? this.condicaoRetryPadrao
    };
  }

  private condicaoRetryPadrao(erro: any): boolean {
    return !erro.response || 
           erro.code === 'ECONNABORTED' || 
           erro.response.status >= 500;
  }

  private esperar(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  private calcularAtraso(tentativaAtual: number): number {
    const atrasoExponencial = this.parametros.atrasoBase * Math.pow(2, tentativaAtual);
    const variacao = Math.random() * 1000;
    return atrasoExponencial + variacao;
  }

  async executar<T>(
    funcaoRequisicao: () => Promise<T>,
    parametrosCustomizados?: Partial<ParametrosRetry>
  ): Promise<T> {
    const config = { ...this.parametros, ...parametrosCustomizados };
    let ultimoErro: any;

    for (let tentativa = 0; tentativa <= config.tentativasMaximas; tentativa++) {
      try {
        const promessaTimeout = new Promise<never>((_, rejeitar) => {
          setTimeout(() => rejeitar(new Error('Timeout na requisição')), config.timeoutRequisicao);
        });

        const resultado = await Promise.race([funcaoRequisicao(), promessaTimeout]);
        return resultado;

      } catch (erro: any) {
        ultimoErro = erro;

        const deveRetentar = tentativa < config.tentativasMaximas && 
                           config.condicaoRetry(erro);

        if (deveRetentar) {
          const atraso = this.calcularAtraso(tentativa);
          console.warn(`Falha na requisição, retentando em ${atraso}ms (${tentativa + 1}/${config.tentativasMaximas}):`, erro.message);
          
          await this.esperar(atraso);
          continue;
        }

        break;
      }
    }

    throw ultimoErro;
  }
}

  1. Versão Avançada - Estratégias Variadas e Monitoramento de Eventos


enum EstrategiaRetry {
  EXPONENCIAL = 'exponencial',
  FIXO = 'fixo',
  LINEAR = 'linear'
}

interface ParametrosRetryAvancado {
  tentativasMaximas?: number;
  atrasoBase?: number;
  timeoutRequisicao?: number;
  estrategia?: EstrategiaRetry;
  condicaoRetry?: (erro: any) => boolean;
  aoRetentar?: (tentativa: number, erro: any, atraso: number) => void;
  aoSucesso?: (resultado: any, tentativa: number) => void;
  aoFalha?: (erro: any, tentativa: number) => void;
}

class GerenciadorRequisicaoAvancado {
  private parametros: Required<ParametrosRetryAvancado>;

  constructor(parametros: ParametrosRetryAvancado = {}) {
    this.parametros = {
      tentativasMaximas: parametros.tentativasMaximas ?? 3,
      atrasoBase: parametros.atrasoBase ?? 1000,
      timeoutRequisicao: parametros.timeoutRequisicao ?? 10000,
      estrategia: parametros.estrategia ?? EstrategiaRetry.EXPONENCIAL,
      condicaoRetry: parametros.condicaoRetry ?? this.condicaoRetryPadrao,
      aoRetentar: parametros.aoRetentar ?? (() => {}),
      aoSucesso: parametros.aoSucesso ?? (() => {}),
      aoFalha: parametros.aoFalha ?? (() => {})
    };
  }

  private condicaoRetryPadrao(erro: any): boolean {
    const errosRetentaveis = ['ECONNABORTED', 'ETIMEDOUT', 'ENOTFOUND'];

    if (erro.code && errosRetentaveis.includes(erro.code)) {
      return true;
    }

    if (erro.response) {
      return erro.response.status >= 500 || erro.response.status === 429;
    }

    return erro.message?.includes('timeout') || 
           erro.message?.includes('rede');
  }

  private calcularAtraso(tentativaAtual: number): number {
    const base = this.parametros.atrasoBase;
    
    switch (this.parametros.estrategia) {
      case EstrategiaRetry.FIXO:
        return base;

      case EstrategiaRetry.LINEAR:
        return base * (tentativaAtual + 1);

      case EstrategiaRetry.EXPONENCIAL:
      default:
        const atrasoExponencial = base * Math.pow(2, tentativaAtual);
        const variacao = Math.random() * base * 0.1;
        return atrasoExponencial + variacao;
    }
  }

  private esperar(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async executar<T>(
    funcaoRequisicao: () => Promise<T>,
    parametrosCustomizados?: Partial<ParametrosRetryAvancado>
  ): Promise<T> {
    const config = { ...this.parametros, ...parametrosCustomizados };
    let ultimoErro: any;
    let contagemTentativas = 0;

    for (; contagemTentativas <= config.tentativasMaximas; contagemTentativas++) {
      try {
        const controlador = new AbortController();
        const idTimeout = setTimeout(() => controlador.abort(), config.timeoutRequisicao);

        let resultado: T;
        
        const resultadoRequisicao = funcaoRequisicao();
        
        if (resultadoRequisicao && typeof (resultadoRequisicao as any).catch === 'function') {
          resultado = await Promise.race([
            resultadoRequisicao,
            new Promise<never>((_, rejeitar) => 
              setTimeout(() => rejeitar(new Error('Timeout na requisição')), config.timeoutRequisicao)
            )
          ]);
        } else {
          resultado = await resultadoRequisicao;
        }

        clearTimeout(idTimeout);

        if (contagemTentativas > 0) {
          config.aoSucesso(resultado, contagemTentativas);
        }
        
        return resultado;

      } catch (erro: any) {
        clearTimeout(idTimeout);
        ultimoErro = erro;

        const deveRetentar = contagemTentativas < config.tentativasMaximas && 
                           config.condicaoRetry(erro);

        if (deveRetentar) {
          const atraso = this.calcularAtraso(contagemTentativas);
          
          config.aoRetentar(contagemTentativas + 1, erro, atraso);
          
          console.warn(`Falha na requisição, retentativa ${contagemTentativas + 1} em ${atraso}ms:`, erro.message);
          
          await this.esperar(atraso);
          continue;
        }

        break;
      }
    }

    config.aoFalha(ultimoErro, contagemTentativas);
    throw ultimoErro;
  }

  static comEstrategiaExponencial(config: Omit<ParametrosRetryAvancado, 'estrategia'> = {}) {
    return new GerenciadorRequisicaoAvancado({ ...config, estrategia: EstrategiaRetry.EXPONENCIAL });
  }

  static comEstrategiaFixa(config: Omit<ParametrosRetryAvancado, 'estrategia'> = {}) {
    return new GerenciadorRequisicaoAvancado({ ...config, estrategia: EstrategiaRetry.FIXO });
  }

  static comEstrategiaLinear(config: Omit<ParametrosRetryAvancado, 'estrategia'> = {}) {
    return new GerenciadorRequisicaoAvancado({ ...config, estrategia: EstrategiaRetry.LINEAR });
  }
}

  1. Integração com Axios


import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';

class AdaptadorAxiosRetry {
  private instanciaAxios: AxiosInstance;
  private gerenciadorRetry: GerenciadorRequisicaoAvancado;

  constructor(instanciaAxios?: AxiosInstance, parametrosRetry?: ParametrosRetryAvancado) {
    this.instanciaAxios = instanciaAxios || axios.create();
    this.gerenciadorRetry = new GerenciadorRequisicaoAvancado(parametrosRetry);
    
    this.configurarInterceptores();
  }

  private configurarInterceptores() {
    this.instanciaAxios.interceptors.request.use(
      (config) => {
        if (config.retryConfig) {
          const funcaoRequisicaoOriginal = () => this.instanciaAxios.request(config);
          
          return this.gerenciadorRetry.executar(funcaoRequisicaoOriginal, config.retryConfig)
            .then(resposta => {
              throw new axios.Cancel('Requisição tratada pela lógica de retry');
            })
            .catch(erro => {
              if (axios.isCancel(erro) && erro.message === 'Requisição tratada pela lógica de retry') {
                return Promise.reject(erro);
              }
              throw erro;
            });
        }

        return config;
      },
      (erro) => Promise.reject(erro)
    );
  }

  requisicao<T = any>(config: AxiosRequestConfig & { retryConfig?: Partial<ParametrosRetryAvancado> }): Promise<AxiosResponse<T>> {
    if (config.retryConfig) {
      const funcaoRequisicao = () => this.instanciaAxios.request(config);
      return this.gerenciadorRetry.executar(funcaoRequisicao, config.retryConfig);
    }
    
    return this.instanciaAxios.request(config);
  }

  obter<T = any>(url: string, config?: AxiosRequestConfig & { retryConfig?: Partial<ParametrosRetryAvancado> }): Promise<AxiosResponse<T>> {
    return this.requisicao({ ...config, method: 'GET', url });
  }

  enviar<T = any>(url: string, dados?: any, config?: AxiosRequestConfig & { retryConfig?: Partial<ParametrosRetryAvancado> }): Promise<AxiosResponse<T>> {
    return this.requisicao({ ...config, method: 'POST', url, data: dados });
  }
}

  1. Exemplos de Uso


// Instância com configuração personalizada
const gerenciadorRetry = new GerenciadorRequisicaoAvancado({
  tentativasMaximas: 3,
  atrasoBase: 1000,
  timeoutRequisicao: 5000,
  aoRetentar: (tentativa, erro, atraso) => {
    console.log(`Retentativa ${tentativa}, causa: ${erro.message}`);
  }
});

// Uso com Fetch API
async function buscarDadosComRetry() {
  try {
    const resposta = await gerenciadorRetry.executar(() => 
      fetch('https://api.exemplo.com/dados').then(res => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json();
      })
    );
    
    console.log('Dados obtidos:', resposta);
  } catch (erro) {
    console.error('Todas as retentativas falharam:', erro);
  }
}

// Integração com Axios
const adaptadorAxios = new AdaptadorAxiosRetry(axios, {
  tentativasMaximas: 3,
  estrategia: EstrategiaRetry.EXPONENCIAL
});

async function buscarComAxiosRetry() {
  try {
    const resposta = await adaptadorAxios.obter('https://api.exemplo.com/dados', {
      retryConfig: {
        tentativasMaximas: 2,
        condicaoRetry: (erro) => {
          return !erro.response || erro.response.status >= 500;
        }
      }
    });
    
    console.log('Resposta:', resposta.data);
  } catch (erro) {
    console.error('Requisição falhou:', erro);
  }
}

// Instâncias com diferentes estratégias
const retryFixo = GerenciadorRequisicaoAvancado.comEstrategiaFixa({
  tentativasMaximas: 5,
  atrasoBase: 2000
});

const retryLinear = GerenciadorRequisicaoAvancado.comEstrategiaLinear({
  tentativasMaximas: 3,
  atrasoBase: 1000
});

  1. Versão com React Hook


import { useState, useCallback } from 'react';

export function usarRequisicaoRetry(config?: ParametrosRetryAvancado) {
  const [carregando, setCarregando] = useState(false);
  const [erro, setErro] = useState<any>(null);
  const [dados, setDados] = useState<any>(null);
  const [contagemTentativas, setContagemTentativas] = useState(0);

  const gerenciadorRetry = new GerenciadorRequisicaoAvancado({
    ...config,
    aoRetentar: (contagem, erro, atraso) => {
      setContagemTentativas(contagem);
      config?.aoRetentar?.(contagem, erro, atraso);
    },
    aoSucesso: (resultado, contagem) => {
      setContagemTentativas(0);
      config?.aoSucesso?.(resultado, contagem);
    },
    aoFalha: (erro, contagem) => {
      setContagemTentativas(contagem);
      config?.aoFalha?.(erro, contagem);
    }
  });

  const executar = useCallback(async <T>(funcaoRequisicao: () => Promise<T>) => {
    setCarregando(true);
    setErro(null);
    
    try {
      const resultado = await gerenciadorRetry.executar(funcaoRequisicao);
      setDados(resultado);
      return resultado;
    } catch (erroCapturado) {
      setErro(erroCapturado);
      throw erroCapturado;
    } finally {
      setCarregando(false);
    }
  }, [gerenciadorRetry]);

  return {
    executar,
    carregando,
    erro,
    dados,
    contagemTentativas,
    resetar: () => {
      setCarregando(false);
      setErro(null);
      setDados(null);
      setContagemTentativas(0);
    }
  };
}

// Exemplo de uso em componente React
function ComponenteExemplo() {
  const { executar, carregando, erro, dados, contagemTentativas } = usarRequisicaoRetry({
    tentativasMaximas: 3
  });

  const buscarDados = async () => {
    try {
      await executar(() => 
        fetch('/api/dados').then(res => res.json())
      );
    } catch (err) {
      // Tratamento de erro
    }
  };

  return (
    <div>
      <button onClick={buscarDados} disabled={carregando}>
        {carregando ? `Carregando... (Tentativa ${contagemTentativas})` : 'Obter Dados'}
      </button>
      {erro && <div>Erro: {erro.message}</div>}
      {dados && <div>Dados: {JSON.stringify(dados)}</div>}
    </div>
  );
}

Características Principais

  • Múltiplas estratégias de retry: exponencial, fixo e linear
  • Condições inteligentes: retentativa automática em erros de rede, timeout e status 5xx
  • Controle de timeout: timeout individual por requisição
  • Monitoramento de eventos: calbacks para retry, sucesso e falha
  • Variação aleatória: evita efeito manada
  • Suporte a TypeScript: definições completas de tipos
  • Integração com frameworks: Axios, Fetch e hooks React

Tags: TypeScript Axios React HTTP Retry

Publicado em 6-9 05:42 por Thomas