RocketChat/Rocket.Chat · error · Error

error-invalid-channel

error-invalid-channel

Error message

error-invalid-channel

What it means

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.

Solutions

  1. 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.
  2. Before calling, verify the visitor association exists: contact.channels.some((ch) => isSameChannel(ch.visitor, visitor)).
  3. If the contact was already merged, locate the surviving/target contact and run the merge against that id.
  4. Check for concurrent verification runs for the same visitor and serialize them (queue or lock) to avoid races.

Example fix

// before
await runMergeContacts(null, contactId, { visitorId, source }, session);

// after
const contact = await LivechatContacts.findOneEnabledById(contactId, { session });
if (!contact) throw new Error('error-invalid-contact');
if (!contact.channels.some((ch) => isSameChannel(ch.visitor, { visitorId, source }))) {
	// visitor no longer tied to this contact: re-resolve the current contactId for the visitor
	throw new Error('stale contactId: visitor channel not on this contact');
}
await runMergeContacts(null, contactId, { visitorId, source }, session);
Defensive patterns

Strategy: validation

Validate before calling

const contact = await LivechatContacts.findOneEnabledById(contactId, { session });
const hasChannel = !!contact?.channels.find((ch: ILivechatContactChannel) => isSameChannel(ch.visitor, visitor));
if (!hasChannel) {
	// do not call mergeContacts; re-resolve the contact for this visitor first
}

Type guard

import { isSameChannel } from '~/app/livechat/lib/isSameChannel';

function contactHasVisitorChannel(
	contact: ILivechatContact | null | undefined,
	visitor: ILivechatContactVisitorAssociation,
): contact is ILivechatContact {
	return !!contact && contact.channels.some((ch: ILivechatContactChannel) => isSameChannel(ch.visitor, visitor));
}

Try / catch

try {
	await runMergeContacts(null, contactId, visitor, session);
} catch (e) {
	if (e instanceof Error && e.message === 'error-invalid-channel') {
		// visitor no longer tied to this contact: re-resolve current contactId and retry once
	}
	throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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').

Understand the failure class

Background: "Not found" and "does not exist" errors: why "Task not found", "No such folder", and "Can't find" fire when a lookup comes back empty — this error's family across 14 libraries.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/929ab0d868923d7e. Report an issue: GitHub.

Appendix: source

Thrown at apps/meteor/ee/server/patches/mergeContacts.ts:25

import { notifyOnSettingChanged } from '../../../server/lib/notifyListener';
import { ContactMerger } from '../../../server/lib/omnichannel/contacts/ContactMerger';
import { mergeContacts } from '../../../server/lib/omnichannel/contacts/mergeContacts';
import { contactLogger as logger } from '../lib/omnichannel/logger';

export const runMergeContacts = async (
	_next: any,
	contactId: string,
	visitor: ILivechatContactVisitorAssociation,
	session?: ClientSession,
): Promise<ILivechatContact | null> => {
	const originalContact = await LivechatContacts.findOneEnabledById(contactId, { session });
	if (!originalContact) {
		throw new Error('error-invalid-contact');
	}

	const channel = originalContact.channels.find((channel: ILivechatContactChannel) => isSameChannel(channel.visitor, visitor));
	if (!channel) {
		throw new Error('error-invalid-channel');
	}

	logger.debug({ msg: 'Getting similar contacts', contactId });

	const similarContacts: ILivechatContact[] = await LivechatContacts.findSimilarVerifiedContacts(channel, contactId, { session });

	if (!similarContacts.length) {
		logger.debug({ msg: 'No similar contacts found', contactId });
		return originalContact;
	}

	logger.debug({ msg: 'Found contacts to merge', contactId, count: similarContacts.length });
	for await (const similarContact of similarContacts) {
		const fields = ContactMerger.getAllFieldsFromContact(similarContact);
		await ContactMerger.mergeFieldsIntoContact({ fields, contact: originalContact, session });
	}

	const similarContactIds = similarContacts.map((c) => c._id);

View on GitHub (pinned to b2c16d5842)