Skip to content

fix(baileys): media guard in getBase64FromMediaMessage misfires on protobuf instances (captioned documents never uploaded) - #2763

Open
patrickmetzner wants to merge 1 commit into
evolution-foundation:developfrom
patrickmetzner:fix/media-guard-own-property
Open

patrickmetzner wants to merge 1 commit into
evolution-foundation:developfrom
patrickmetzner:fix/media-guard-own-property

Conversation

@patrickmetzner

@patrickmetzner patrickmetzner commented Oct 23, 2026 •

Copy link
Copy Markdown

📋 Description

getBase64FromMediaMessage returns null for any message that has exactly one own key. The guard is meant to skip messages that hold nothing but messageContextInfo:

if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {

msg.message is a Baileys protobuf instance, and protobufjs's generated code declares every field on the prototype (Message.prototype.messageContextInfo = null in baileys/WAProto/index.js, from 7.0.0-rc.9 through rc14). So 'messageContextInfo' in msg.message is true for every message, and the guard reduces to "has exactly one own key".

That is the case right after the MessageSubtype unwrap a few lines above. A captioned document arrives as { documentWithCaptionMessage: { message: { documentMessage } }, messageContextInfo }. The unwrap replaces msg.message with the inner { documentMessage }, which has one own key, and the guard returns null. In messages.upsert the S3 block then logs No valid media to upload (messageContextInfo only), skipping MinIO at verbose and returns. The document is never uploaded and gets no Media row or mediaUrl, and its MESSAGES_UPSERT webhook is never sent. An uncaptioned document is { documentMessage, messageContextInfo } (two own keys), so it passes, which is why this shows up only with a caption.

The fix checks for an own property, the same way hasValidMediaContent in channel.service.ts already does:

-      if ('messageContextInfo' in msg.message && Object.keys(msg.message).length === 1) {
+      if (
+        Object.keys(msg.message).length === 1 &&
+        Object.prototype.hasOwnProperty.call(msg.message, 'messageContextInfo')
+      ) {

A message that really holds only messageContextInfo is still skipped, as #2273 intended.

This is the other half of #2684 / #2720, not a duplicate. Those PRs stop the S3 block's return from dropping the webhook. With only them merged, a captioned document would reach the webhook, but it would still never be uploaded and would carry no mediaUrl, because this guard still returns null. With only this PR merged, captioned documents are uploaded and delivered, and videos with S3_SAVE_VIDEO=false are still dropped. The fromMe LID echoes measured in #2720 (Message contains only messageContextInfo logged for real media) look like the same false positive.

🔗 Related Issue

Related: #2273 (introduced the return null, v2.3.7), #2684, #2720, #2396

🧪 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🔧 Refactoring (no functional changes)
  • ⚡ Performance improvement
  • 🧹 Code cleanup
  • 🔒 Security fix

🧪 Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced
  • Tested with different connection types (if applicable)

Observed in production on v2.3.7 (S3_ENABLED=true, DATABASE_SAVE_DATA_NEW_MESSAGE=true, group chats). Two captioned documents sent from WhatsApp Web were saved in Message with no Media row, and no webhook was posted (nothing reached the receiving endpoint, and no log line at ERROR,WARN,INFO). Seven uncaptioned documents in the same groups, five of them from WhatsApp Web, were uploaded and delivered.

Reproduced offline with the image's own baileys 7.0.0-rc.9 (also checked against rc13, develop's pin). Each shape is encoded and decoded with proto.Message, as on receipt, then put through the same unwrap and the guard:

import { proto } from 'baileys';
const MessageSubtype = ['ephemeralMessage', 'documentWithCaptionMessage', 'viewOnceMessage', 'viewOnceMessageV2'];
const ctx = { deviceListMetadataVersion: 2 };
const doc = { mimetype: 'application/pdf', mediaKey: Buffer.alloc(32, 1), directPath: '/v/x' };
const shapes = {
  'document, uncaptioned': { documentMessage: doc, messageContextInfo: ctx },
  'document, captioned': { documentWithCaptionMessage: { message: { documentMessage: { ...doc, caption: 'x' } } }, messageContextInfo: ctx },
  'messageContextInfo only': { messageContextInfo: ctx },
};
for (const [name, obj] of Object.entries(shapes)) {
  const msg = { message: proto.Message.decode(proto.Message.encode(proto.Message.fromObject(obj)).finish()) };
  for (const s of MessageSubtype) if (msg.message[s]) msg.message = msg.message[s].message;
  const before = 'messageContextInfo' in msg.message && Object.keys(msg.message).length === 1;
  const after = Object.keys(msg.message).length === 1 && Object.prototype.hasOwnProperty.call(msg.message, 'messageContextInfo');
  console.log(name, { before, after });
}
// document, uncaptioned    { before: false, after: false }
// document, captioned      { before: true,  after: false }   <- skipped today, uploaded with the fix
// messageContextInfo only  { before: true,  after: true }

The same change, applied to v2.3.7's compiled dist/main.js, makes captioned documents upload and arrive with their mediaUrl.

✅ Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have manually tested my changes thoroughly
  • I have verified the changes work with different scenarios
  • Any dependent changes have been merged and published

📝 Additional Notes

The same guard is reached from the S3 block of the API send path (sendMessageWithTyping). That is not measured here, but a captioned document sent through the API would go through the same unwrap.

Summary by Sourcery

Bug Fixes:

  • Prevent valid captioned document media from being incorrectly skipped during processing, allowing uploads, media records, URLs, and downstream webhooks to be produced.

…4FromMediaMessage

protobufjs declares every field on the prototype, so 'messageContextInfo' in msg.message is
true for any Baileys message and the guard skipped every message left with one own key after
the MessageSubtype unwrap. A captioned document (documentWithCaptionMessage) was therefore
never uploaded to S3 and, in messages.upsert, never reached the webhook.
@sourcery-ai

sourcery-ai Bot commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor
Reviewer's guide (collapsed on small PRs)

Reviewer's Guide

Fixes a false-positive media guard caused by Baileys protobuf fields living on the prototype. By requiring messageContextInfo to be an own property, the change preserves the intended context-only skip while allowing unwrapped captioned documents and other real media messages to upload and reach webhooks.

Sequence diagram for captioned document media processing

sequenceDiagram
    participant Baileys
    participant MediaGuard
    participant S3
    participant Webhook

    Baileys->>MediaGuard: getBase64FromMediaMessage
    MediaGuard->>MediaGuard: unwrap documentWithCaptionMessage
    alt messageContextInfo is an own property and only key
        MediaGuard-->>Baileys: return null
    else real media content
        MediaGuard->>S3: upload media
        S3-->>Baileys: mediaUrl
        Baileys->>Webhook: MESSAGES_UPSERT
    end
Loading

File-Level Changes

Change Details Files
Restrict the messageContextInfo-only media guard to own properties so protobuf prototype fields do not trigger it for real media messages.
  • Replace the prototype-inclusive in check with an own-property check while preserving the single-context-only skip.
  • Allow captioned documents to continue through media extraction, upload, database persistence, and webhook delivery.
  • Retain skipping for messages whose sole own field is genuinely messageContextInfo.
  • Also affects the shared guard path used by API message sending with typing.
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!

Sourcery assessment

Approved.


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant