{"record":{"id":"929ab0d868923d7e","repo":"RocketChat/Rocket.Chat","slug":"error-invalid-channel","errorCode":"error-invalid-channel","errorMessage":"error-invalid-channel","messagePattern":"error-invalid-channel","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/ee/server/patches/mergeContacts.ts","lineNumber":25,"sourceCode":"import { notifyOnSettingChanged } from '../../../server/lib/notifyListener';\nimport { ContactMerger } from '../../../server/lib/omnichannel/contacts/ContactMerger';\nimport { mergeContacts } from '../../../server/lib/omnichannel/contacts/mergeContacts';\nimport { contactLogger as logger } from '../lib/omnichannel/logger';\n\nexport const runMergeContacts = async (\n\t_next: any,\n\tcontactId: string,\n\tvisitor: ILivechatContactVisitorAssociation,\n\tsession?: ClientSession,\n): Promise<ILivechatContact | null> => {\n\tconst originalContact = await LivechatContacts.findOneEnabledById(contactId, { session });\n\tif (!originalContact) {\n\t\tthrow new Error('error-invalid-contact');\n\t}\n\n\tconst channel = originalContact.channels.find((channel: ILivechatContactChannel) => isSameChannel(channel.visitor, visitor));\n\tif (!channel) {\n\t\tthrow new Error('error-invalid-channel');\n\t}\n\n\tlogger.debug({ msg: 'Getting similar contacts', contactId });\n\n\tconst similarContacts: ILivechatContact[] = await LivechatContacts.findSimilarVerifiedContacts(channel, contactId, { session });\n\n\tif (!similarContacts.length) {\n\t\tlogger.debug({ msg: 'No similar contacts found', contactId });\n\t\treturn originalContact;\n\t}\n\n\tlogger.debug({ msg: 'Found contacts to merge', contactId, count: similarContacts.length });\n\tfor await (const similarContact of similarContacts) {\n\t\tconst fields = ContactMerger.getAllFieldsFromContact(similarContact);\n\t\tawait ContactMerger.mergeFieldsIntoContact({ fields, contact: originalContact, session });\n\t}\n\n\tconst similarContactIds = similarContacts.map((c) => c._id);","sourceCodeStart":7,"sourceCodeEnd":43,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/ee/server/patches/mergeContacts.ts#L7-L43","documentation":"Thrown by the Enterprise Edition patch of LivechatContacts.mergeContacts (runMergeContacts) when the contact found by contactId is enabled, but none of its channels has a visitor association matching the visitor argument (compared with isSameChannel, i.e. visitorId + source). The merge needs the matching channel to search for similar verified contacts, so it aborts before merging. Sibling error 'error-invalid-contact' covers the case where the contact itself does not exist or is disabled.","triggerScenarios":"Calling mergeContacts(contactId, visitor, session) where visitor.visitorId/visitor.source is not present in originalContact.channels: a stale contactId captured before a previous merge moved the channel to another contact, a visitorId/source mismatch, or a concurrent verification flow that already merged that channel away.","commonSituations":"Contact-visitor association drift after earlier merges, clients re-using an old contactId, duplicate/concurrent verifyContactChannel executions for the same visitor, or a source value mismatch (e.g. custom channel source vs 'rocket.chat').","solutions":["Re-resolve the contact for the visitor at call time (e.g. via LivechatContacts find-by-visitor queries) instead of trusting a cached contactId, then retry the merge.","Before calling, verify the visitor association exists: contact.channels.some((ch) => isSameChannel(ch.visitor, visitor)).","If the contact was already merged, locate the surviving/target contact and run the merge against that id.","Check for concurrent verification runs for the same visitor and serialize them (queue or lock) to avoid races."],"exampleFix":"// before\nawait runMergeContacts(null, contactId, { visitorId, source }, session);\n\n// after\nconst contact = await LivechatContacts.findOneEnabledById(contactId, { session });\nif (!contact) throw new Error('error-invalid-contact');\nif (!contact.channels.some((ch) => isSameChannel(ch.visitor, { visitorId, source }))) {\n\t// visitor no longer tied to this contact: re-resolve the current contactId for the visitor\n\tthrow new Error('stale contactId: visitor channel not on this contact');\n}\nawait runMergeContacts(null, contactId, { visitorId, source }, session);","handlingStrategy":"validation","validationCode":"const contact = await LivechatContacts.findOneEnabledById(contactId, { session });\nconst hasChannel = !!contact?.channels.find((ch: ILivechatContactChannel) => isSameChannel(ch.visitor, visitor));\nif (!hasChannel) {\n\t// do not call mergeContacts; re-resolve the contact for this visitor first\n}","typeGuard":"import { isSameChannel } from '~/app/livechat/lib/isSameChannel';\n\nfunction contactHasVisitorChannel(\n\tcontact: ILivechatContact | null | undefined,\n\tvisitor: ILivechatContactVisitorAssociation,\n): contact is ILivechatContact {\n\treturn !!contact && contact.channels.some((ch: ILivechatContactChannel) => isSameChannel(ch.visitor, visitor));\n}","tryCatchPattern":"try {\n\tawait runMergeContacts(null, contactId, visitor, session);\n} catch (e) {\n\tif (e instanceof Error && e.message === 'error-invalid-channel') {\n\t\t// visitor no longer tied to this contact: re-resolve current contactId and retry once\n\t}\n\tthrow e;\n}","preventionTips":["Always resolve the contact from the visitor association at call time instead of caching contactId across requests","Serialize merges for the same visitor to avoid concurrent-merge races","Treat 'error-invalid-contact' and 'error-invalid-channel' as signals to refresh the contact-visitor mapping"],"tags":["livechat","contacts","merge","visitor","enterprise"],"backgroundTag":"entity-not-found","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}