Skip to content

Commit 9d7b5cd

Browse files
committed
[FIX] Sistema de webhooks: melhoria com timeout e retentativas configuráveis
1 parent 75a7140 commit 9d7b5cd

3 files changed

Lines changed: 92 additions & 8 deletions

File tree

.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,16 @@ WEBHOOK_EVENTS_TYPEBOT_CHANGE_STATUS=false
173173
WEBHOOK_EVENTS_ERRORS=false
174174
WEBHOOK_EVENTS_ERRORS_WEBHOOK=
175175

176+
# Webhook timeout and retry configuration
177+
WEBHOOK_REQUEST_TIMEOUT_MS=60000
178+
WEBHOOK_RETRY_MAX_ATTEMPTS=10
179+
WEBHOOK_RETRY_INITIAL_DELAY_SECONDS=5
180+
WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF=true
181+
WEBHOOK_RETRY_MAX_DELAY_SECONDS=300
182+
WEBHOOK_RETRY_JITTER_FACTOR=0.2
183+
# Comma separated list of HTTP status codes that should not trigger retries
184+
WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES=400,401,403,404,422
185+
176186
# Name that will be displayed on smartphone connection
177187
CONFIG_SESSION_PHONE_CLIENT=Evolution API
178188
# Browser Name = Chrome | Firefox | Edge | Opera | Safari

src/api/integrations/event/webhook/webhook.controller.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ export class WebhookController extends EventController implements EventControlle
115115
const httpService = axios.create({
116116
baseURL,
117117
headers: webhookHeaders as Record<string, string> | undefined,
118+
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
118119
});
119120

120121
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
@@ -156,7 +157,10 @@ export class WebhookController extends EventController implements EventControlle
156157

157158
try {
158159
if (isURL(globalURL)) {
159-
const httpService = axios.create({ baseURL: globalURL });
160+
const httpService = axios.create({
161+
baseURL: globalURL,
162+
timeout: webhookConfig.REQUEST?.TIMEOUT_MS ?? 30000,
163+
});
160164

161165
await this.retryWebhookRequest(
162166
httpService,
@@ -190,12 +194,21 @@ export class WebhookController extends EventController implements EventControlle
190194
origin: string,
191195
baseURL: string,
192196
serverUrl: string,
193-
maxRetries = 10,
194-
delaySeconds = 30,
197+
maxRetries?: number,
198+
delaySeconds?: number,
195199
): Promise<void> {
200+
// Obter configurações de retry das variáveis de ambiente
201+
const webhookConfig = configService.get<Webhook>('WEBHOOK');
202+
const maxRetryAttempts = maxRetries ?? webhookConfig.RETRY?.MAX_ATTEMPTS ?? 10;
203+
const initialDelay = delaySeconds ?? webhookConfig.RETRY?.INITIAL_DELAY_SECONDS ?? 5;
204+
const useExponentialBackoff = webhookConfig.RETRY?.USE_EXPONENTIAL_BACKOFF ?? true;
205+
const maxDelay = webhookConfig.RETRY?.MAX_DELAY_SECONDS ?? 300;
206+
const jitterFactor = webhookConfig.RETRY?.JITTER_FACTOR ?? 0.2;
207+
const nonRetryableStatusCodes = webhookConfig.RETRY?.NON_RETRYABLE_STATUS_CODES ?? [400, 401, 403, 404, 422];
208+
196209
let attempts = 0;
197210

198-
while (attempts < maxRetries) {
211+
while (attempts < maxRetryAttempts) {
199212
try {
200213
await httpService.post('', webhookData);
201214
if (attempts > 0) {
@@ -209,24 +222,58 @@ export class WebhookController extends EventController implements EventControlle
209222
} catch (error) {
210223
attempts++;
211224

225+
// Verificar se é um erro de timeout
226+
const isTimeout = error.code === 'ECONNABORTED';
227+
228+
// Verificar se o erro não deve gerar retry com base no status code
229+
if (error?.response?.status && nonRetryableStatusCodes.includes(error.response.status)) {
230+
this.logger.error({
231+
local: `${origin}`,
232+
message: `Erro não recuperável (${error.response.status}): ${error?.message}. Cancelando retentativas.`,
233+
statusCode: error?.response?.status,
234+
url: baseURL,
235+
server_url: serverUrl,
236+
});
237+
throw error;
238+
}
239+
212240
this.logger.error({
213241
local: `${origin}`,
214-
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
242+
message: `Tentativa ${attempts}/${maxRetryAttempts} falhou: ${isTimeout ? 'Timeout da requisição' : error?.message}`,
215243
hostName: error?.hostname,
216244
syscall: error?.syscall,
217245
code: error?.code,
246+
isTimeout,
247+
statusCode: error?.response?.status,
218248
error: error?.errno,
219249
stack: error?.stack,
220250
name: error?.name,
221251
url: baseURL,
222252
server_url: serverUrl,
223253
});
224254

225-
if (attempts === maxRetries) {
255+
if (attempts === maxRetryAttempts) {
226256
throw error;
227257
}
228258

229-
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
259+
// Cálculo do delay com backoff exponencial e jitter
260+
let nextDelay = initialDelay;
261+
if (useExponentialBackoff) {
262+
// Fórmula: initialDelay * (2^attempts) com limite máximo
263+
nextDelay = Math.min(initialDelay * Math.pow(2, attempts - 1), maxDelay);
264+
265+
// Adicionar jitter para evitar "thundering herd"
266+
const jitter = nextDelay * jitterFactor * (Math.random() * 2 - 1);
267+
nextDelay = Math.max(initialDelay, nextDelay + jitter);
268+
}
269+
270+
this.logger.log({
271+
local: `${origin}`,
272+
message: `Aguardando ${nextDelay.toFixed(1)} segundos antes da próxima tentativa`,
273+
url: baseURL,
274+
});
275+
276+
await new Promise((resolve) => setTimeout(resolve, nextDelay * 1000));
230277
}
231278
}
232279
}

src/config/env.config.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,21 @@ export type CacheConfLocal = {
229229
TTL: number;
230230
};
231231
export type SslConf = { PRIVKEY: string; FULLCHAIN: string };
232-
export type Webhook = { GLOBAL?: GlobalWebhook; EVENTS: EventsWebhook };
232+
export type Webhook = {
233+
GLOBAL?: GlobalWebhook;
234+
EVENTS: EventsWebhook;
235+
REQUEST?: {
236+
TIMEOUT_MS?: number;
237+
};
238+
RETRY?: {
239+
MAX_ATTEMPTS?: number;
240+
INITIAL_DELAY_SECONDS?: number;
241+
USE_EXPONENTIAL_BACKOFF?: boolean;
242+
MAX_DELAY_SECONDS?: number;
243+
JITTER_FACTOR?: number;
244+
NON_RETRYABLE_STATUS_CODES?: number[];
245+
};
246+
};
233247
export type Pusher = { ENABLED: boolean; GLOBAL?: GlobalPusher; EVENTS: EventsPusher };
234248
export type ConfigSessionPhone = { CLIENT: string; NAME: string; VERSION: string };
235249
export type QrCode = { LIMIT: number; COLOR: string };
@@ -543,6 +557,19 @@ export class ConfigService {
543557
ERRORS: process.env?.WEBHOOK_EVENTS_ERRORS === 'true',
544558
ERRORS_WEBHOOK: process.env?.WEBHOOK_EVENTS_ERRORS_WEBHOOK || '',
545559
},
560+
REQUEST: {
561+
TIMEOUT_MS: Number.parseInt(process.env?.WEBHOOK_REQUEST_TIMEOUT_MS) || 30000,
562+
},
563+
RETRY: {
564+
MAX_ATTEMPTS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_ATTEMPTS) || 10,
565+
INITIAL_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_INITIAL_DELAY_SECONDS) || 5,
566+
USE_EXPONENTIAL_BACKOFF: process.env?.WEBHOOK_RETRY_USE_EXPONENTIAL_BACKOFF !== 'false',
567+
MAX_DELAY_SECONDS: Number.parseInt(process.env?.WEBHOOK_RETRY_MAX_DELAY_SECONDS) || 300,
568+
JITTER_FACTOR: Number.parseFloat(process.env?.WEBHOOK_RETRY_JITTER_FACTOR) || 0.2,
569+
NON_RETRYABLE_STATUS_CODES: process.env?.WEBHOOK_RETRY_NON_RETRYABLE_STATUS_CODES?.split(',').map(Number) || [
570+
400, 401, 403, 404, 422,
571+
],
572+
},
546573
},
547574
CONFIG_SESSION_PHONE: {
548575
CLIENT: process.env?.CONFIG_SESSION_PHONE_CLIENT || 'Evolution API',

0 commit comments

Comments
 (0)