evolution-api/src/api/integrations/event/webhook/webhook.controller.ts
Davidson Gomes cb08f6b152 Refactor WebhookController to implement retry logic for webhook requests
- Introduced a new `retryWebhookRequest` method to handle retries for failed webhook requests, allowing up to 10 attempts with a delay of 30 seconds between each.
- Updated error logging to provide detailed information on each retry attempt, including the attempt number and error details.
- Enhanced existing webhook request handling to utilize the new retry logic, improving reliability in sending webhook data.
- Modified error messages to be more informative, indicating when all retry attempts have failed.
2025-01-17 16:06:23 -03:00

234 lines
6.8 KiB
TypeScript

import { EventDto } from '@api/integrations/event/event.dto';
import { PrismaRepository } from '@api/repository/repository.service';
import { WAMonitoringService } from '@api/services/monitor.service';
import { wa } from '@api/types/wa.types';
import { configService, Log, Webhook } from '@config/env.config';
import { Logger } from '@config/logger.config';
import { BadRequestException } from '@exceptions';
import axios, { AxiosInstance } from 'axios';
import { isURL } from 'class-validator';
import { EmitData, EventController, EventControllerInterface } from '../event.controller';
export class WebhookController extends EventController implements EventControllerInterface {
private readonly logger = new Logger('WebhookController');
constructor(prismaRepository: PrismaRepository, waMonitor: WAMonitoringService) {
super(prismaRepository, waMonitor, true, 'webhook');
}
override async set(instanceName: string, data: EventDto): Promise<wa.LocalWebHook> {
if (!isURL(data.webhook.url, { require_tld: false })) {
throw new BadRequestException('Invalid "url" property');
}
if (!data.webhook?.enabled) {
data.webhook.events = [];
} else {
if (0 === data.webhook.events.length) {
data.webhook.events = EventController.events;
}
}
return this.prisma.webhook.upsert({
where: {
instanceId: this.monitor.waInstances[instanceName].instanceId,
},
update: {
enabled: data.webhook?.enabled,
events: data.webhook?.events,
url: data.webhook?.url,
headers: data.webhook?.headers,
webhookBase64: data.webhook.base64,
webhookByEvents: data.webhook.byEvents,
},
create: {
enabled: data.webhook?.enabled,
events: data.webhook?.events,
instanceId: this.monitor.waInstances[instanceName].instanceId,
url: data.webhook?.url,
headers: data.webhook?.headers,
webhookBase64: data.webhook.base64,
webhookByEvents: data.webhook.byEvents,
},
});
}
public async emit({
instanceName,
origin,
event,
data,
serverUrl,
dateTime,
sender,
apiKey,
local,
integration,
}: EmitData): Promise<void> {
if (integration && !integration.includes('webhook')) {
return;
}
const instance = (await this.get(instanceName)) as wa.LocalWebHook;
const webhookConfig = configService.get<Webhook>('WEBHOOK');
const webhookLocal = instance?.events;
const webhookHeaders = instance?.headers;
const we = event.replace(/[.-]/gm, '_').toUpperCase();
const transformedWe = we.replace(/_/gm, '-').toLowerCase();
const enabledLog = configService.get<Log>('LOG').LEVEL.includes('WEBHOOKS');
const webhookData = {
event,
instance: instanceName,
data,
destination: instance?.url || `${webhookConfig.GLOBAL.URL}/${transformedWe}`,
date_time: dateTime,
sender,
server_url: serverUrl,
apikey: apiKey,
};
if (local && instance?.enabled) {
if (Array.isArray(webhookLocal) && webhookLocal.includes(we)) {
let baseURL: string;
if (instance?.webhookByEvents) {
baseURL = `${instance?.url}/${transformedWe}`;
} else {
baseURL = instance?.url;
}
if (enabledLog) {
const logData = {
local: `${origin}.sendData-Webhook`,
url: baseURL,
...webhookData,
};
this.logger.log(logData);
}
try {
if (instance?.enabled && isURL(instance.url, { require_tld: false })) {
const httpService = axios.create({
baseURL,
headers: webhookHeaders as Record<string, string> | undefined,
});
await this.retryWebhookRequest(httpService, webhookData, `${origin}.sendData-Webhook`, baseURL, serverUrl);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook`,
message: `Todas as tentativas falharam: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: baseURL,
server_url: serverUrl,
});
}
}
}
if (webhookConfig.GLOBAL?.ENABLED) {
if (webhookConfig.EVENTS[we]) {
let globalURL = webhookConfig.GLOBAL.URL;
if (webhookConfig.GLOBAL.WEBHOOK_BY_EVENTS) {
globalURL = `${globalURL}/${transformedWe}`;
}
if (enabledLog) {
const logData = {
local: `${origin}.sendData-Webhook-Global`,
url: globalURL,
...webhookData,
};
this.logger.log(logData);
}
try {
if (isURL(globalURL)) {
const httpService = axios.create({ baseURL: globalURL });
await this.retryWebhookRequest(
httpService,
webhookData,
`${origin}.sendData-Webhook-Global`,
globalURL,
serverUrl,
);
}
} catch (error) {
this.logger.error({
local: `${origin}.sendData-Webhook-Global`,
message: `Todas as tentativas falharam: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: globalURL,
server_url: serverUrl,
});
}
}
}
}
private async retryWebhookRequest(
httpService: AxiosInstance,
webhookData: any,
origin: string,
baseURL: string,
serverUrl: string,
maxRetries = 10,
delaySeconds = 30,
): Promise<void> {
let attempts = 0;
while (attempts < maxRetries) {
try {
await httpService.post('', webhookData);
if (attempts > 0) {
this.logger.log({
local: `${origin}`,
message: `Sucesso no envio após ${attempts + 1} tentativas`,
url: baseURL,
});
}
return;
} catch (error) {
attempts++;
this.logger.error({
local: `${origin}`,
message: `Tentativa ${attempts}/${maxRetries} falhou: ${error?.message}`,
hostName: error?.hostname,
syscall: error?.syscall,
code: error?.code,
error: error?.errno,
stack: error?.stack,
name: error?.name,
url: baseURL,
server_url: serverUrl,
});
if (attempts === maxRetries) {
throw error;
}
await new Promise((resolve) => setTimeout(resolve, delaySeconds * 1000));
}
}
}
}