Skip to content

Bug: Contact pushName silently overwritten with empty string on every sent message #2426

@spada23

Description

@spada23

Summary

The Contact.pushName field in the database is being silently overwritten with an empty string "" every time the instance owner sends a message. This causes contact names to disappear across all consumers (API responses, frontend, integrations).

The root cause is in whatsapp.baileys.service.ts — three code paths write to Contact.pushName without any guard against empty/undefined values, even though a proper guard already exists for Chat.name updates (line 1187).

Affected versions: v2.3.7 (and likely all v2.x versions)
Related issue: #2004


The Problem

What users see

What happens in the database

Given a contact "Maria Santos" (551199*****2@s.whatsapp.net) across 3 instances:

Instance Contact.pushName Explanation
Instance A "Maria Santos" Last operation was a received message
Instance B "" Last operation was a sent message → pushName overwritten with ""
Instance C "" Same — sent message wiped the name

The Message.pushName field on individual received messages still contains "Maria Santos", proving the data exists — it's just being discarded during the Contact upsert.


Root Cause Analysis

There are 3 bugs in src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts, all related to missing validation before writing pushName to the Contact table.

Bug 1 (Critical): Sent messages overwrite contact pushName with empty string

File: whatsapp.baileys.service.ts, line 1503

const contactRaw = {
  remoteJid: received.key.remoteJid,
  pushName: received.key.fromMe ? '' : received.key.fromMe == null ? '' : received.pushName,
  // ...
};

When fromMe = true, pushName is explicitly set to ''. This contactRaw is then passed to:

// Line 1535 (existing contact) and line 1547 (new contact)
await this.prismaRepository.contact.upsert({
  where: { remoteJid_instanceId: { ... } },
  create: contactRaw,
  update: contactRaw,  // ← overwrites Contact.pushName with ""
});

Impact: Every single sent message overwrites the recipient's Contact.pushName with an empty string. This is the most damaging bug — it means active conversations (where you send messages frequently) are the ones most likely to lose contact names.

Bug 2: contacts.update handler ignores the notify field from Baileys

File: whatsapp.baileys.service.ts, line 903

pushName: contact?.name ?? contact?.verifiedName,

Baileys stores pushName in the notify field of its Contact type, not in name:

// Baileys Contact interface (src/Types/Contact.ts)
interface Contact {
  name?: string    // Name YOU saved in YOUR contacts (locally)
  notify?: string  // Name the CONTACT set on THEIR profile (= pushName)
}

When Baileys emits contacts.update after receiving a message with pushName, it sends:

{ id: jid, notify: "Maria Santos", verifiedName: "" }

But the code reads contact.name (which is undefined in this event) and falls back to contact.verifiedName (often ""). The notify field — where the actual pushName lives — is never read.

Additionally, ?? (nullish coalescing) is used instead of || (logical OR), so an empty string "" from contact.name would NOT fall through to verifiedName.

Bug 3: No guard against overwriting valid pushName with empty values

Lines 1535 and 1547 pass the full contactRaw object to both create and update in prisma.contact.upsert(). There is no check like "only update pushName if the new value is non-empty".

Ironically, this guard already exists for Chat.name updates (line 1183-1190):

if (
  existingChat &&
  received.pushName &&
  existingChat.name !== received.pushName &&
  received.pushName.trim().length > 0 &&  // ← THIS GUARD
  !received.key.fromMe &&                  // ← AND THIS ONE
  !received.key.remoteJid.includes('@g.us')
) {
  await this.prismaRepository.chat.update({
    where: { id: existingChat.id },
    data: { name: received.pushName },
  });
}

This guard correctly:

  • Rejects empty/whitespace pushName values
  • Skips updates from sent messages (fromMe)

But the equivalent Contact writes at lines 1535 and 1547 have none of these protections.


Data Flow Diagram

WhatsApp message received with pushName "John"
  │
  ├─ Baileys emits contacts.update: { id: jid, notify: "John" }
  │   │
  │   └─ Evolution reads: contact.name ?? contact.verifiedName
  │       = undefined ?? undefined = undefined
  │       ❌ "John" (in contact.notify) is NEVER read
  │
  └─ Baileys emits messages.upsert: { pushName: "John", key: { fromMe: false } }
      │
      └─ contactRaw.pushName = "John" ✅ (correct for received messages)
         prisma.contact.upsert({ update: contactRaw }) → saves "John"

Instance owner SENDS a message to John
  │
  └─ Baileys emits messages.upsert: { key: { fromMe: true } }
      │
      └─ contactRaw.pushName = '' ❌ (hardcoded empty for fromMe)
         prisma.contact.upsert({ update: contactRaw }) → overwrites "John" with ""

Proposed Fix

Fix 1: Guard contact upsert in messages.upsert handler

Lines 1535-1539 and 1547-1551

Only include pushName in the update payload when it has a meaningful value:

// Build update data — never overwrite a valid pushName with empty
const contactUpdate: any = {
  remoteJid: contactRaw.remoteJid,
  profilePicUrl: contactRaw.profilePicUrl,
  instanceId: contactRaw.instanceId,
};

// Only update pushName if we have a real value (not empty, not just a phone number pattern)
if (contactRaw.pushName && contactRaw.pushName.trim().length > 0) {
  contactUpdate.pushName = contactRaw.pushName;
}

await this.prismaRepository.contact.upsert({
  where: { remoteJid_instanceId: { remoteJid: contactRaw.remoteJid, instanceId: contactRaw.instanceId } },
  create: contactRaw,       // For new contacts, use whatever data we have
  update: contactUpdate,    // For existing contacts, preserve pushName if new value is empty
});

Fix 2: Read notify field in contacts.update handler

Line 903

// Before (bug):
pushName: contact?.name ?? contact?.verifiedName,

// After (fix):
pushName: contact?.notify || contact?.name || contact?.verifiedName,

Use || instead of ?? so empty strings fall through, and read notify first (where Baileys actually stores pushName from incoming messages).

Fix 3: Apply the same guard pattern to contacts.update upsert

Lines 913-918

const updateTransactions = contactsRaw.map((contact) => {
  const updateData: any = {
    remoteJid: contact.remoteJid,
    profilePicUrl: contact.profilePicUrl,
    instanceId: contact.instanceId,
  };
  if (contact.pushName && contact.pushName.trim().length > 0) {
    updateData.pushName = contact.pushName;
  }

  return this.prismaRepository.contact.upsert({
    where: { remoteJid_instanceId: { remoteJid: contact.remoteJid, instanceId: contact.instanceId } },
    create: contact,
    update: updateData,
  });
});

Why This Hasn't Been Caught Earlier

  1. Message.pushName still works: Individual messages store pushName correctly (via prepareMessage() which uses || at line 4652). So message display in chat views appears fine.

  2. The Chat.name guard masks the issue: Chat names are protected by the guard at line 1187, so the Chat table retains names even as the Contact table loses them.

  3. Initial sync uses createMany with skipDuplicates: The contacts.upsert handler (line 822) correctly uses skipDuplicates: true, so it never overwrites — but this only runs on initial connection, not on message-triggered updates.

  4. The damage is gradual: pushName is overwritten one contact at a time, each time a message is sent. It's not immediately obvious unless you compare Contact table data across time.


Verification Query

Run this against your PostgreSQL database to see the impact:

-- Count contacts with empty pushName vs total
SELECT
  i.name as instance,
  COUNT(*) as total_contacts,
  COUNT(*) FILTER (WHERE c."pushName" IS NULL OR c."pushName" = '') as empty_pushname,
  ROUND(100.0 * COUNT(*) FILTER (WHERE c."pushName" IS NULL OR c."pushName" = '') / COUNT(*), 1) as pct_empty
FROM "Contact" c
JOIN "Instance" i ON i.id = c."instanceId"
GROUP BY i.name
ORDER BY i.name;
-- Find contacts where Message.pushName exists but Contact.pushName is empty
SELECT c."remoteJid", c."pushName" as contact_pushname, m."pushName" as message_pushname, i.name as instance
FROM "Contact" c
JOIN "Instance" i ON i.id = c."instanceId"
JOIN LATERAL (
  SELECT "pushName" FROM "Message" m
  WHERE m."instanceId" = c."instanceId"
    AND m.key->>'remoteJid' = c."remoteJid"
    AND (m.key->>'fromMe')::boolean = false
    AND m."pushName" IS NOT NULL AND m."pushName" != ''
  ORDER BY "messageTimestamp" DESC LIMIT 1
) m ON true
WHERE c."pushName" IS NULL OR c."pushName" = ''
ORDER BY i.name, c."remoteJid";

Real-World Impact (Production Data)

Affected contacts per instance

Instance Total Contacts Empty pushName % Affected
Instance A ~6,300 22 0.4%
Instance B ~6,300 133 2.1%
Instance C ~6,200 105 1.7%

The instances with more sent messages (B and C) have significantly more affected contacts, confirming that outgoing messages are the primary cause of data loss.

Proof: Contact.pushName is empty but Message.pushName has the real name

These are contacts where Contact.pushName = "" but the actual name exists in their received messages:

Contact JID (masked) Contact.pushName Message.pushName (from received msgs)
5511*****022@s.whatsapp.net "" "Maria S."
5511*****929@s.whatsapp.net "" "Carlos F."
5511*****488@s.whatsapp.net "" "Ana T."
5511*****316@s.whatsapp.net "" "Pedro L."
5511*****465@s.whatsapp.net "" "Juliana R."

In all cases, the real name is present in Message.pushName from received messages — proving the data was correctly captured by Baileys but then overwritten with "" when the instance owner sent a reply.


Environment

  • Evolution API version: 2.3.7
  • Database provider: PostgreSQL
  • Baileys version: as bundled with v2.3.7
  • DATABASE_SAVE_DATA_CONTACTS: true

Workaround

Setting DATABASE_SAVE_DATA_CONTACTS=false freezes the Contact table (stops all writes) while webhooks and message saving continue to work normally. This prevents further data loss but also prevents new contacts from being created. It's a viable temporary measure until the fix is applied.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions