mirror of
https://github.com/EvolutionAPI/evolution-api.git
synced 2025-07-24 17:38:40 -06:00
fix(webhook): implementar timeout configurável e sistema de retentativas inteligente
Resolve #1325 - Adiciona configuração de timeout via variáveis de ambiente - Implementa backoff exponencial com jitter para retentativas - Detecta erros permanentes para evitar retentativas desnecessárias - Corrige bug de duplicação de webhooks - Melhora logs para diagnóstico
This commit is contained in:
parent
b89f1144b4
commit
81a7ff8214
10
.env.example
10
.env.example
@ -173,6 +173,16 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
|
|||||||
WEBHOOK_EVENTS_ERRORS=false
|
WEBHOOK_EVENTS_ERRORS=false
|
||||||
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
|
||||||
|
|
||||||
|
# Webhook timeout and retry configuration
|
||||||
|
WEBHOOK_REQUEST_TIMEOUT_MS=60000
|
||||||
|
WEBHOOK_RETRY_MAX_ATTEMPTS=10
|
||||||
|
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
|
||||||
|
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
|
||||||
|
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
|
||||||
|
WEBHOOK_RETRY_JITTER_FACTOR=0.2
|
||||||
|
# Comma separated list of HTTP status codes that should not trigger retries
|
||||||
|
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
|
||||||
|
|
||||||
# Name that will be displayed on smartphone connection
|
# Name that will be displayed on smartphone connection
|
||||||
CONFIG_SESSION_PHONE_CLIENT=Evolution API
|
CONFIG_SESSION_PHONE_CLIENT=Evolution API
|
||||||
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
|
# Browser Name = Chrome | Firefox | Edge | Opera | Safari
|
||||||
|
@ -115,6 +115,7 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
const httpService = axios.create({
|
const httpService = axios.create({
|
||||||
baseURL,
|
baseURL,
|
||||||
headers: webhookHeaders as Record<string, string> | undefined,
|
headers: webhookHeaders as Record<string, string> | undefined,
|
||||||
|
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
|
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
|
||||||
@ -156,7 +157,10 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (regex.test(globalURL)) {
|
if (regex.test(globalURL)) {
|
||||||
const httpService = axios.create({ baseURL: globalURL });
|
const httpService = axios.create({
|
||||||
|
baseURL: globalURL,
|
||||||
|
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
|
||||||
|
});
|
||||||
|
|
||||||
await this.retryWebhookRequest(
|
await this.retryWebhookRequest(
|
||||||
httpService,
|
httpService,
|
||||||
@ -190,12 +194,20 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
origin: string,
|
origin: string,
|
||||||
baseURL: string,
|
baseURL: string,
|
||||||
serverUrl: string,
|
serverUrl: string,
|
||||||
maxRetries = 10,
|
maxRetries?: number,
|
||||||
delaySeconds = 30,
|
delaySeconds?: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
const webhookConfig = configService.get<Webhook>('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;
|
let attempts = 0;
|
||||||
|
|
||||||
while (attempts < maxRetries) {
|
while (attempts < maxRetryAttempts) {
|
||||||
try {
|
try {
|
||||||
await httpService.post('', webhookData);
|
await httpService.post('', webhookData);
|
||||||
if (attempts > 0) {
|
if (attempts > 0) {
|
||||||
@ -208,13 +220,28 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
attempts++;
|
attempts++;
|
||||||
|
|
||||||
|
const isTimeout = error.code === 'ECONNABORTED';
|
||||||
|
|
||||||
|
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({
|
this.logger.error({
|
||||||
local: `${origin}`,
|
local: `${origin}`,
|
||||||
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
|
message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`,
|
||||||
hostName: error?.hostname,
|
hostName: error?.hostname,
|
||||||
syscall: error?.syscall,
|
syscall: error?.syscall,
|
||||||
code: error?.code,
|
code: error?.code,
|
||||||
|
isTimeout,
|
||||||
|
statusCode: error?.response?.status,
|
||||||
error: error?.errno,
|
error: error?.errno,
|
||||||
stack: error?.stack,
|
stack: error?.stack,
|
||||||
name: error?.name,
|
name: error?.name,
|
||||||
@ -222,11 +249,25 @@ export class WebhookController extends EventController implements EventControlle
|
|||||||
server_url: serverUrl,
|
server_url: serverUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (attempts === maxRetries) {
|
if (attempts === maxRetryAttempts) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
|
let nextDelay = initialDelay;
|
||||||
|
if (useExponentialBackoff) {
|
||||||
|
nextDelay = Math.min(initialDelay * Math.pow(2, attempts - 1), maxDelay);
|
||||||
|
|
||||||
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -229,7 +229,21 @@ export type CacheConfLocal = {
|
|||||||
TTL: number;
|
TTL: number;
|
||||||
};
|
};
|
||||||
export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
|
export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
|
||||||
export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
|
export type Webhook = {
|
||||||
|
GLOBAL?: GlobalWebhook;
|
||||||
|
EVENTS: EventsWebhook;
|
||||||
|
REQUEST?: {
|
||||||
|
TIMEOUT_MS?: number;
|
||||||
|
};
|
||||||
|
RETRY?: {
|
||||||
|
MAX_ATTEMPTS?: number;
|
||||||
|
INITIAL_DELAY_SECONDS?: number;
|
||||||
|
USE_EXPONENTIAL_BACKOFF?: boolean;
|
||||||
|
MAX_DELAY_SECONDS?: number;
|
||||||
|
JITTER_FACTOR?: number;
|
||||||
|
NON_RETRYABLE_STATUS_CODES?: number[];
|
||||||
|
};
|
||||||
|
};
|
||||||
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
|
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
|
||||||
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
|
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
|
||||||
export type QrCode = { LIMIT: number; COLOR: string };
|
export type QrCode = { LIMIT: number; COLOR: string };
|
||||||
@ -543,6 +557,17 @@ export class ConfigService {
|
|||||||
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
|
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
|
||||||
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
|
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
|
||||||
},
|
},
|
||||||
|
REQUEST: {
|
||||||
|
TIMEOUT_MS: Number.parseInt(process.env?.WEBHOOK_REQUEST_TIMEOUT_MS) || 30000,
|
||||||
|
},
|
||||||
|
RETRY: {
|
||||||
|
MAX_ATTEMPTS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_ATTEMPTS) || 10,
|
||||||
|
INITIAL_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_INITIAL_DELAY_SECONDS) || 5,
|
||||||
|
USE_EXPONENTIAL_BACKOFF: process.env?.WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF !== 'false',
|
||||||
|
MAX_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_DELAY_SECONDS) || 300,
|
||||||
|
JITTER_FACTOR: Number.parseFloat(process.env?.WEBHOOK_RETRY_JITTER_FACTOR) || 0.2,
|
||||||
|
NON_RETRYABLE_STATUS_CODES: process.env?.WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES?.split(',').map(Number) || [400, 401, 403, 404, 422],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
CONFIG_SESSION_PHONE: {
|
CONFIG_SESSION_PHONE: {
|
||||||
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
|
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',
|
||||||
|
Loading…
Reference in New Issue
Block a user