fix(meta): handle message_echoes and guard missing contact fields#2514
fix(meta): handle message_echoes and guard missing contact fields#2514ricardoisus wants to merge 4 commits intoEvolutionAPI:mainfrom
Conversation
Reviewer's GuideNormalizes WhatsApp Cloud API webhook payloads so message_echoes/smb_message_echoes follow the existing messages[] flow, hardens contact/remoteJid resolution for messages and statuses, and prevents crashes when non-classic payloads arrive. Sequence diagram for normalized WhatsApp Cloud webhook handlingsequenceDiagram
participant MetaWebhook as MetaWebhook
participant BusinessStartupService as BusinessStartupService
participant EventHandler as EventHandler
MetaWebhook->>BusinessStartupService: handleWebhook(data)
BusinessStartupService->>BusinessStartupService: extract content = data.entry[0].changes[0].value
BusinessStartupService->>BusinessStartupService: firstMessage = messages[0] or message_echoes[0] or smb_message_echoes[0]
BusinessStartupService->>BusinessStartupService: recipient = statuses[0].recipient_id
BusinessStartupService->>BusinessStartupService: remoteId = firstMessage.to or firstMessage.from or recipient
BusinessStartupService->>BusinessStartupService: loadChatwoot()
BusinessStartupService->>BusinessStartupService: normalizedContent = normalizeWebhookContent(content)
BusinessStartupService->>EventHandler: eventHandler(normalizedContent)
alt remoteId resolved
BusinessStartupService->>BusinessStartupService: phoneNumber = createJid(remoteId)
else no remoteId
BusinessStartupService->>BusinessStartupService: phoneNumber unchanged
end
BusinessStartupService-->>MetaWebhook: success or error response
Sequence diagram for robust message and contact handlingsequenceDiagram
participant BusinessStartupService as BusinessStartupService
participant MessageHandler as MessageHandler
participant ContactRepo as ContactRepository
BusinessStartupService->>MessageHandler: handleIncoming(received)
MessageHandler->>MessageHandler: incomingContact = received.contacts[0]
alt incomingContact exists
MessageHandler->>MessageHandler: pushName = profile.name or name or wa_id
else
MessageHandler->>MessageHandler: pushName = undefined
end
alt messages present
MessageHandler->>MessageHandler: message = received.messages[0]
MessageHandler->>MessageHandler: remoteJid = createJid(message.to or message.from)
MessageHandler->>MessageHandler: key.fromMe = message.from == metadata.phone_number_id
MessageHandler->>ContactRepo: findOne(instanceId, remoteJid)
MessageHandler->>MessageHandler: contactPhone = profile.phone or wa_id or message.to or message.from
alt contactPhone exists
MessageHandler->>MessageHandler: contactRemoteJid = createJid(contactPhone)
MessageHandler->>ContactRepo: upsertContact(instanceId, contactRemoteJid, pushName)
else
MessageHandler->>MessageHandler: abort contact creation
end
end
alt statuses present
loop for each status item
MessageHandler->>MessageHandler: remoteJid = createJid(item.recipient_id or phoneNumber)
MessageHandler->>MessageHandler: fromMe = item.recipient_id ? item.recipient_id != metadata.phone_number_id : true
end
end
Class diagram for BusinessStartupService webhook normalization and contact handlingclassDiagram
class ChannelStartupService {
}
class BusinessStartupService {
- phoneNumber : string
- logger
- instanceId : string
+ handleWebhook(data) void
+ normalizeWebhookContent(content) any
+ downloadMediaMessage(message) Promise~void~
+ loadChatwoot() void
+ eventHandler(content) void
+ handleIncomingMessage(received) Promise~void~
}
ChannelStartupService <|-- BusinessStartupService
class WebhookContent {
+ messages : Message[]
+ message_echoes : Message[]
+ smb_message_echoes : Message[]
+ statuses : Status[]
+ contacts : Contact[]
+ metadata : Metadata
}
class Message {
+ id : string
+ from : string
+ to : string
+ type : string
}
class Status {
+ id : string
+ recipient_id : string
}
class Contact {
+ profile : ContactProfile
+ name : string
+ wa_id : string
}
class ContactProfile {
+ name : string
+ phone : string
}
class Metadata {
+ phone_number_id : string
}
BusinessStartupService --> WebhookContent
WebhookContent o-- Message
WebhookContent o-- Status
WebhookContent o-- Contact
WebhookContent --> Metadata
Contact o-- ContactProfile
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In
BusinessStartupService.BusinessConnect,remoteIdnow prefersfirstMessage.toover.from, which changes the previous behavior for inbound messages; please double-check that this aligns with howphoneNumberis expected to be set (business vs. customer JID) to avoid regressions in routing/ownership semantics. - The new
fromMecomputation for statuses (item.recipient_id ? recipient_id !== phone_number_id : true) ineventHandlersignificantly changes the previous logic (this.phoneNumber === received.metadata.phone_number_id); consider revisiting this condition or adding a brief comment to clarify the intended directionality to future readers.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `BusinessStartupService.BusinessConnect`, `remoteId` now prefers `firstMessage.to` over `.from`, which changes the previous behavior for inbound messages; please double-check that this aligns with how `phoneNumber` is expected to be set (business vs. customer JID) to avoid regressions in routing/ownership semantics.
- The new `fromMe` computation for statuses (`item.recipient_id ? recipient_id !== phone_number_id : true`) in `eventHandler` significantly changes the previous logic (`this.phoneNumber === received.metadata.phone_number_id`); consider revisiting this condition or adding a brief comment to clarify the intended directionality to future readers.
## Individual Comments
### Comment 1
<location path="src/api/integrations/channel/meta/whatsapp.business.service.ts" line_range="152-157" />
<code_context>
+ if (!content || typeof content !== 'object') return content;
+
+ const normalized = { ...content };
+ const echoes = normalized?.message_echoes ?? normalized?.smb_message_echoes;
+
+ if (!Array.isArray(normalized.messages) && Array.isArray(echoes) && echoes.length > 0) {
+ normalized.messages = echoes;
+ }
</code_context>
<issue_to_address>
**suggestion:** Echo selection prefers an empty `message_echoes` over a non-empty `smb_message_echoes`.
Because `??` only distinguishes `null/undefined`, this will select an empty `message_echoes` array instead of falling back to a non-empty `smb_message_echoes`. If you want to use `smb_message_echoes` when `message_echoes` is missing or empty, explicitly check `message_echoes.length` and only prefer it when it’s a non-empty array.
```suggestion
const normalized = { ...content };
const messageEchoes = Array.isArray(normalized.message_echoes)
? normalized.message_echoes
: undefined;
const smbMessageEchoes = Array.isArray(normalized.smb_message_echoes)
? normalized.smb_message_echoes
: undefined;
const echoes =
messageEchoes && messageEchoes.length > 0
? messageEchoes
: smbMessageEchoes;
if (!Array.isArray(normalized.messages) && Array.isArray(echoes) && echoes.length > 0) {
normalized.messages = echoes;
}
```
</issue_to_address>
### Comment 2
<location path="src/api/integrations/channel/meta/whatsapp.business.service.ts" line_range="774-778" />
<code_context>
}
if (received.statuses) {
for await (const item of received.statuses) {
+ const remoteJid = createJid(item?.recipient_id ?? this.phoneNumber);
const key = {
id: item.id,
- remoteJid: this.phoneNumber,
- fromMe: this.phoneNumber === received.metadata.phone_number_id,
+ remoteJid,
+ fromMe: item?.recipient_id ? item.recipient_id !== received.metadata.phone_number_id : true,
};
if (settings?.groups_ignore && key.remoteJid.includes('@g.us')) {
</code_context>
<issue_to_address>
**issue (bug_risk):** The new `fromMe` logic for statuses looks inverted and may misclassify message direction.
Previously `fromMe` was `this.phoneNumber === received.metadata.phone_number_id`; now it’s `item?.recipient_id ? item.recipient_id !== received.metadata.phone_number_id : true`, which treats statuses as `fromMe` when the recipient differs from `phone_number_id` and defaults to `true` if `recipient_id` is missing. That reverses the usual “from us” meaning. If this is meant to indicate events sent by the business, it likely should be `item?.recipient_id === received.metadata.phone_number_id` (or similar), and the default value should be revisited.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const remoteJid = createJid(item?.recipient_id ?? this.phoneNumber); | ||
| const key = { | ||
| id: item.id, | ||
| remoteJid: this.phoneNumber, | ||
| fromMe: this.phoneNumber === received.metadata.phone_number_id, | ||
| remoteJid, | ||
| fromMe: item?.recipient_id ? item.recipient_id !== received.metadata.phone_number_id : true, |
There was a problem hiding this comment.
issue (bug_risk): The new fromMe logic for statuses looks inverted and may misclassify message direction.
Previously fromMe was this.phoneNumber === received.metadata.phone_number_id; now it’s item?.recipient_id ? item.recipient_id !== received.metadata.phone_number_id : true, which treats statuses as fromMe when the recipient differs from phone_number_id and defaults to true if recipient_id is missing. That reverses the usual “from us” meaning. If this is meant to indicate events sent by the business, it likely should be item?.recipient_id === received.metadata.phone_number_id (or similar), and the default value should be revisited.
|
Thanks for the review — addressed in commit What was updated:
I also re-ran build/typecheck after these changes. |
Problem
When WhatsApp Cloud API delivers messages sent outside Evolution (official WhatsApp app), payloads can arrive as
message_echoes/smb_message_echoesinstead of classicmessages[].In production this caused parser breakage (
Cannot read properties of undefined, e.g. reading'0'/'name') and outbound-echo messages were not persisted/routed.What changed
message_echoes/smb_message_echoesinto the regular message processing flow.profile/name/phone).remoteJidresolution robust for both messages and statuses.connectToWhatsappcrashes when payload does not follow classicmessages[]structure.Real environment validation
message_echoesobserved in runtime logs.reading '0'/reading 'name'errors after deploy.Traceability
Summary by Sourcery
Handle WhatsApp Cloud API echo payloads and make webhook message/contact parsing more robust.
Bug Fixes:
Enhancements: