RocketChat/Rocket.Chat · error · Error

error-invalid-contact

Error message

error-invalid-contact

What it means

Thrown by the mergeContacts patch (runMergeContacts) when LivechatContacts.findOneEnabledById(contactId, { session }) returns null — the supplied contactId does not map to an enabled contact record. Because mergeContacts is a transactional operation that needs a primary contact to merge others into, a missing source is fatal. It is raised inside a ClientSession so it participates in transaction rollback.

Source

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

import { License } from '@rocket.chat/license';
import { LivechatContacts, LivechatRooms, Settings } from '@rocket.chat/models';
import type { ClientSession } from 'mongodb';

import { isSameChannel } from '../../../app/livechat/lib/isSameChannel';
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) {

View on GitHub (pinned to f9d3ec372b)

Solutions

  1. Re-fetch the contact by id and confirm enabled === true before invoking mergeContacts.
  2. If the contact no longer exists, abort the merge flow and surface a user-facing 'contact not found' state.
  3. Make sure the contactId passed is the primary (target) contact, not a similar/secondary one.
  4. Run the merge inside the same session that loaded the contact to avoid TOCTOU deletion.

Example fix

// before
await mergeContacts(maybeDeletedId, visitor, session);

// after
const contact = await LivechatContacts.findOneEnabledById(contactId, { session });
if (!contact) return null;
await mergeContacts(contactId, visitor, session);
Defensive patterns

Strategy: validation

Validate before calling

async function contactEnabled(contactId: string, session?: ClientSession): Promise<boolean> {
  const c = await LivechatContacts.findOneEnabledById(contactId, { session });
  return Boolean(c);
}

Try / catch

try {
  await mergeContacts(contactId, visitor, session);
} catch (e) {
  if (e.message === 'error-invalid-contact') {
    // contact was deleted/disabled — abort merge and notify
  } else throw e;
}

Prevention

When it happens

Trigger: Calling mergeContacts (or the upstream verifyContactChannel flow that triggers it) with a contactId that was deleted, disabled (enabled:false), or never existed; concurrent merge that already deleted this contact.

Common situations: Contact was merged/deleted by another request between lookup and merge; visitor/contact association was cleared; the contactId was extracted from a stale room record; test fixture did not insert the contact.

Related errors


AI-assisted analysis of RocketChat/Rocket.Chat@f9d3ec372b (2026-08-12). Data as JSON: /api/errors/f1d2c3bc22a5ec3d. Report an issue: GitHub.