Skip to content

Conversation

@Vitordotpy
Copy link
Contributor

@Vitordotpy Vitordotpy commented Dec 16, 2025

📋 Description

This PR addresses two critical issues related to data consistency and race conditions in
the WhatsApp integration (Baileys) and contact caching mechanism:

  1. Original Message Not Found (Fix):

    • Normalization: Implements strict normalization of remoteJid and
      participant in the messages.update handler to remove device suffixes (e.g.,
      :42@s.whatsapp.net -> @s.whatsapp.net). This ensures that keys used for lookups match
      the stored format.
    • Identifier Synchronization: Adds logic to update the incoming key.remoteJid
      with the value stored in the database if the message is found. This resolves mismatches
      where an update might come in with a LID while the stored message uses a phone number JID
      (or vice versa), ensuring the correct identifier is used for subsequent logic.
    • Retry Mechanism: Introduces a retry loop (3 attempts, 2s delay) when fetching
      the original message for an update. This handles race conditions where the
      messages.update event arrives before the initial messages.upsert has finished
      committing the message to the database.
  2. Unique Constraint Failed in Cache (Fix):

    • Race Condition Handling: Wraps the prismaRepository.isOnWhatsapp.create
      call in a try-catch block specifically catching P2002 (Unique Constraint Violation)
      errors on remoteJid.
    • Fallback Strategy: If a race condition is detected (another process created
      the record between the check and the insert), the code now gracefully falls back to
      updating the existing record instead of crashing.

🔗 Related Issue

Closes # (Add issue number if applicable)

🧪 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
  • Verified that "Original message not found" errors are reduced/eliminated.
  • Verified that "Unique constraint failed" errors on remoteJid no longer crash the
    process.

📸 Screenshots (if applicable)

✅ 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 race condition in saveOnWhatsappCache was causing intermittent P2002 errors in
logs, which are now handled gracefully. The messages.update fixes ensure that read
receipts and other status updates are correctly applied even when network events arrive
out of order or with slightly different JID formats.

Summary by Sourcery

Normalize WhatsApp message identifiers in update handling and add safeguards against race conditions in message lookup and contact cache writes.

Bug Fixes:

  • Ensure message update events use normalized remoteJid and participant values consistent with stored messages.
  • Add a retry mechanism when fetching the original message for updates to handle timing races with initial message persistence.
  • Synchronize key.remoteJid with the stored message identifier when they differ to prevent mismatched lookups and processing.
  • Handle Prisma P2002 unique-constraint violations in the WhatsApp contact cache by falling back to updating the existing record instead of failing.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Dec 16, 2025

Reviewer's Guide

Normalizes Baileys message update JIDs in-place, adds a retry mechanism and JID synchronization when loading original messages for updates, and hardens the WhatsApp contact cache against Prisma unique-constraint race conditions by catching P2002 and updating instead of failing.

Sequence diagram for Baileys messages.update handling with JID normalization and retry

sequenceDiagram
  participant BaileysSocket
  participant BaileysStartupService
  participant PrismaRepository
  participant DelayUtil

  BaileysSocket->>BaileysStartupService: messages.update(args)
  loop for each update in args
    BaileysStartupService->>BaileysStartupService: Normalize key.remoteJid
    BaileysStartupService->>BaileysStartupService: Normalize key.participant
    BaileysStartupService->>BaileysStartupService: Check settings.groupsIgnore and group JIDs

    alt update references original message
      BaileysStartupService->>BaileysStartupService: Determine searchId
      loop up to maxRetries (3)
        BaileysStartupService->>PrismaRepository: $queryRaw SELECT Message WHERE key->>id = searchId LIMIT 1
        PrismaRepository-->>BaileysStartupService: Message or null
        alt message found
          BaileysStartupService->>BaileysStartupService: Assign findMessage
          BaileysStartupService->>BaileysStartupService: break loop
        else message not found
          BaileysStartupService->>DelayUtil: delay(2000ms)
          DelayUtil-->>BaileysStartupService: resume
        end
      end
      alt findMessage missing
        BaileysStartupService->>BaileysStartupService: logger.warn Original message not found
        BaileysStartupService-->>BaileysStartupService: continue to next update
      else findMessage present
        alt stored key.remoteJid differs from incoming key.remoteJid
          BaileysStartupService->>BaileysStartupService: logger.verbose Updating key.remoteJid
          BaileysStartupService->>BaileysStartupService: key.remoteJid = findMessage.key.remoteJid
        end
        BaileysStartupService->>BaileysStartupService: message.messageId = findMessage.id
      end
    else no original message reference
      BaileysStartupService->>BaileysStartupService: Process update without lookup
    end
  end
Loading

Sequence diagram for saveOnWhatsappCache with P2002 race condition handling

sequenceDiagram
  participant Caller
  participant saveOnWhatsappCache
  participant PrismaRepository
  Caller->>saveOnWhatsappCache: saveOnWhatsappCache(data[])
  loop for each dataPayload in data[]
    saveOnWhatsappCache->>PrismaRepository: isOnWhatsapp.findUnique(remoteJid)
    PrismaRepository-->>saveOnWhatsappCache: existingRecord or null
    alt existingRecord exists
      saveOnWhatsappCache->>PrismaRepository: isOnWhatsapp.update(where remoteJid, dataPayload)
      PrismaRepository-->>saveOnWhatsappCache: updatedRecord
    else record does not exist
      saveOnWhatsappCache->>saveOnWhatsappCache: logger.verbose creating record
      saveOnWhatsappCache->>PrismaRepository: isOnWhatsapp.create(dataPayload)
      alt create succeeds
        PrismaRepository-->>saveOnWhatsappCache: createdRecord
      else create throws error
        PrismaRepository-->>saveOnWhatsappCache: error
        alt error.code is P2002 and error.meta.target includes remoteJid
          saveOnWhatsappCache->>saveOnWhatsappCache: logger.verbose race condition detected
          saveOnWhatsappCache->>PrismaRepository: isOnWhatsapp.update(where remoteJid, dataPayload)
          PrismaRepository-->>saveOnWhatsappCache: updatedRecord
        else other error
          saveOnWhatsappCache-->>Caller: throw error
        end
      end
    end
  end
  saveOnWhatsappCache-->>Caller: completion
Loading

Flow diagram for message lookup retry loop in messages.update

flowchart TD
  A[Start message update handling] --> B[Normalize key.remoteJid and key.participant]
  B --> C[Determine searchId]
  C --> D[Set retries = 0, maxRetries = 3]
  D --> E{retries < maxRetries}
  E -- No --> F[Log original message not found]
  F --> G[Skip this update]
  G --> H[Next update or end]
  E -- Yes --> I[Query Message by instanceId and key id]
  I --> J{Message found}
  J -- Yes --> K[Set findMessage]
  K --> L{findMessage.key.remoteJid differs from key.remoteJid}
  L -- Yes --> M[Update key.remoteJid from stored message]
  M --> N[Set message.messageId = findMessage.id]
  L -- No --> N[Set message.messageId = findMessage.id]
  N --> H
  J -- No --> O[Increment retries]
  O --> P{retries < maxRetries}
  P -- Yes --> Q[delay 2000ms]
  Q --> E
  P -- No --> F
Loading

File-Level Changes

Change Details Files
Normalize Baileys message update keys and synchronize remoteJid with stored messages, adding retries to find the original message before applying updates.
  • Mutate incoming key.remoteJid and key.participant to strip device suffixes before further processing and reuse these normalized values
  • Replace single raw Message lookup with a loop that retries the SELECT up to three times with a 2-second delay until a message is found
  • After successfully loading the original message, overwrite key.remoteJid with the stored message.key.remoteJid when they differ, logging the adjustment, then set message.messageId to the found message id
src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts
Handle race conditions in the WhatsApp onWhatsapp cache by catching Prisma unique constraint violations and falling back to an update.
  • Wrap isOnWhatsapp.create in a try/catch that inspects Prisma error code P2002 with a target including remoteJid
  • On P2002 for remoteJid, log a verbose race-condition message and call isOnWhatsapp.update by remoteJid with the same payload
  • Re-throw non-P2002 or unrelated errors so they are not silently swallowed
src/utils/onWhatsappCache.ts

Possibly linked issues

  • #: PR changes saveOnWhatsappCache to safely handle P2002 on remoteJid, fixing the reported unique constraint crash.

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

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey there - I've reviewed your changes - here's some feedback:

  • The retry loop for fetching the original message can add up to ~6 seconds of blocking per update within the for await handler; consider making the retry delay and count configurable or shortening the delay to avoid increasing end-to-end latency under load.
  • When updating key.remoteJid from the stored message, it may be worth adding a brief comment explaining why it is safe to mutate the incoming key object and how this interacts with any downstream logic that might assume the normalized (device-suffix-stripped) form.
  • In saveOnWhatsappCache, instead of checking error.code === 'P2002' on any, consider narrowing the type (e.g., to PrismaClientKnownRequestError) so the error handling is more robust to unrelated errors with a code property.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The retry loop for fetching the original message can add up to ~6 seconds of blocking per update within the `for await` handler; consider making the retry delay and count configurable or shortening the delay to avoid increasing end-to-end latency under load.
- When updating `key.remoteJid` from the stored message, it may be worth adding a brief comment explaining why it is safe to mutate the incoming key object and how this interacts with any downstream logic that might assume the normalized (device-suffix-stripped) form.
- In `saveOnWhatsappCache`, instead of checking `error.code === 'P2002'` on `any`, consider narrowing the type (e.g., to `PrismaClientKnownRequestError`) so the error handling is more robust to unrelated errors with a `code` property.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@DavidsonGomes DavidsonGomes merged commit b1b07b7 into EvolutionAPI:develop Dec 16, 2025
5 checks passed
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.

2 participants