diff --git a/src/api/integrations/event/webhook/webhook.controller.ts b/src/api/integrations/event/webhook/webhook.controller.ts index ce709c3d..b77c7829 100644 --- a/src/api/integrations/event/webhook/webhook.controller.ts +++ b/src/api/integrations/event/webhook/webhook.controller.ts @@ -115,6 +115,7 @@ export class WebhookController extends EventController implements EventControlle const httpService = axios.create({ baseURL, headers: webhookHeaders as Record | undefined, + timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000, }); await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl); @@ -156,7 +157,10 @@ export class WebhookController extends EventController implements EventControlle try { if (isURL(globalURL)) { - const httpService = axios.create({ baseURL: globalURL }); + const httpService = axios.create({ + baseURL: globalURL, + timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000, + }); await this.retryWebhookRequest( httpService, @@ -190,12 +194,21 @@ export class WebhookController extends EventController implements EventControlle origin: string, baseURL: string, serverUrl: string, - maxRetries = 10, - delaySeconds = 30, + maxRetries?: number, + delaySeconds?: number, ): Promise { + // Obter configurações de retry das variáveis de ambiente + const webhookConfig = configService.get('WEBHOOK'); + const maxRetryAttempts = maxRetries ?? webhookConfig.RETRY?.MAX_ATTEMPTS ?? 10; + const initialDelay = delaySeconds ?? webhookConfig.RETRY?.INITIAL_DELAY_SECONDS ?? 5; + const useExponentialBackoff = webhookConfig.RETRY?.USE_EXPONENTIAL_BACKOFF ?? true; + const maxDelay = webhookConfig.RETRY?.MAX_DELAY_SECONDS ?? 300; + const jitterFactor = webhookConfig.RETRY?.JITTER_FACTOR ?? 0.2; + const nonRetryableStatusCodes = webhookConfig.RETRY?.NON_RETRYABLE_STATUS_CODES ?? [400, 401, 403, 404, 422]; + let attempts = 0; - while (attempts < maxRetries) { + while (attempts < maxRetryAttempts) { try { await httpService.post('', webhookData); if (attempts > 0) { @@ -208,13 +221,30 @@ export class WebhookController extends EventController implements EventControlle return; } catch (error) { attempts++; + + // Verificar se é um erro de timeout + const isTimeout = error.code === 'ECONNABORTED'; + + // Verificar se o erro não deve gerar retry com base no status code + if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) { + this.logger.error({ + local: `${origin}`, + message: `Erro não recuperável (${error.response.status}): ${error?.message}. Cancelando retentativas.`, + statusCode: error?.response?.status, + url: baseURL, + server_url: serverUrl, + }); + throw error; + } this.logger.error({ local: `${origin}`, - message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`, + message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`, hostName: error?.hostname, syscall: error?.syscall, code: error?.code, + isTimeout, + statusCode: error?.response?.status, error: error?.errno, stack: error?.stack, name: error?.name, @@ -222,11 +252,28 @@ export class WebhookController extends EventController implements EventControlle server_url: serverUrl, }); - if (attempts === maxRetries) { + if (attempts === maxRetryAttempts) { throw error; } - await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000)); + // Cálculo do delay com backoff exponencial e jitter + let nextDelay = initialDelay; + if (useExponentialBackoff) { + // Fórmula: initialDelay * (2^attempts) com limite máximo + nextDelay = Math.min(initialDelay * Math.pow(2, attempts - 1), maxDelay); + + // Adicionar jitter para evitar "thundering herd" + const jitter = nextDelay * jitterFactor * (Math.random() * 2 - 1); + nextDelay = Math.max(initialDelay, nextDelay + jitter); + } + + this.logger.log({ + local: `${origin}`, + message: `Aguardando ${nextDelay.toFixed(1)} segundos antes da próxima tentativa`, + url: baseURL, + }); + + await new Promise((resolve) => setTimeout(resolve, nextDelay * 1000)); } } }