From c60f8e06a9d0e70685ea2040d73715459de69469 Mon Sep 17 00:00:00 2001 From: Matheus Pastorini Date: Wed, 12 Aug 2026 17:10:29 -0300 Subject: [PATCH] fix(baileys): always emit MESSAGES_UPSERT for media even when S3 upload is skipped or fails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the messages.upsert handler, the S3 upload block returned early from the whole handler in two cases (video upload disabled; getBase64FromMediaMessage returns null), aborting before sendDataWebhook(Events.MESSAGES_UPSERT). The message is persisted but the webhook carrying its content is never emitted. This silently drops media messages whose upload is skipped or fails — notably fromMe media sent from another device, where getBase64FromMediaMessage cannot fetch the file (its mediaKey belongs to that device). Measured at ~94% media loss for fromMe messages on a production deployment (S3 enabled). Restructure the block so it skips only the upload, never the handler, so the webhook is always delivered regardless of the storage outcome. The inline comment ("returning early from this block") shows the original intent was to skip the upload only; another method in the same file already uses throw for the equivalent case. --- .../whatsapp/whatsapp.baileys.service.ts | 91 ++++++++++--------- 1 file changed, 49 insertions(+), 42 deletions(-) diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 22839fd451..708b4a4489 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -1576,52 +1576,59 @@ export class BaileysStartupService extends ChannelStartupService { if (isMedia) { if (this.configService.get('S3').ENABLE) { try { + // Skip only the S3 upload here — NOT the whole handler. The + // previous `return` statements exited messages.upsert before + // sendDataWebhook(Events.MESSAGES_UPSERT) below, silently dropping + // every media message whose S3 upload was skipped or failed — + // notably `fromMe` media sent from another device, where + // getBase64FromMediaMessage cannot fetch the file (its mediaKey + // belongs to that device). Webhook delivery must not depend on the + // outcome of the storage step. if (isVideo && !this.configService.get('S3').SAVE_VIDEO) { this.logger.warn('Video upload is disabled. Skipping video upload.'); - // Skip video upload by returning early from this block - return; - } - - const message: any = received; - - // Verificação adicional para garantir que há conteúdo de mídia real - const hasRealMedia = this.hasValidMediaContent(message); - - if (!hasRealMedia) { - this.logger.warn('Message detected as media but contains no valid media content'); } else { - const media = await this.getBase64FromMediaMessage({ message }, true); - - if (!media) { - this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO'); - return; + const message: any = received; + + // Verificação adicional para garantir que há conteúdo de mídia real + const hasRealMedia = this.hasValidMediaContent(message); + + if (!hasRealMedia) { + this.logger.warn('Message detected as media but contains no valid media content'); + } else { + const media = await this.getBase64FromMediaMessage({ message }, true); + + if (!media) { + this.logger.verbose('No valid media to upload (messageContextInfo only), skipping MinIO'); + } else { + const { buffer, mediaType, fileName, size } = media; + const mimetype = mimeTypes.lookup(fileName).toString(); + const fullName = join( + `${this.instance.id}`, + received.key.remoteJid, + mediaType, + `${Date.now()}_${fileName}`, + ); + await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { + 'Content-Type': mimetype, + }); + + await this.prismaRepository.media.create({ + data: { + messageId: msg.id, + instanceId: this.instanceId, + type: mediaType, + fileName: fullName, + mimetype, + }, + }); + + const mediaUrl = await s3Service.getObjectUrl(fullName); + + (messageRaw.message as any).mediaUrl = mediaUrl; + + await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); + } } - - const { buffer, mediaType, fileName, size } = media; - const mimetype = mimeTypes.lookup(fileName).toString(); - const fullName = join( - `${this.instance.id}`, - received.key.remoteJid, - mediaType, - `${Date.now()}_${fileName}`, - ); - await s3Service.uploadFile(fullName, buffer, size.fileLength?.low, { 'Content-Type': mimetype }); - - await this.prismaRepository.media.create({ - data: { - messageId: msg.id, - instanceId: this.instanceId, - type: mediaType, - fileName: fullName, - mimetype, - }, - }); - - const mediaUrl = await s3Service.getObjectUrl(fullName); - - (messageRaw.message as any).mediaUrl = mediaUrl; - - await this.prismaRepository.message.update({ where: { id: msg.id }, data: messageRaw }); } } catch (error) { this.logger.error(['Error on upload file to minio', error?.message, error?.stack]);