RocketChat/Rocket.Chat · error · Meteor.Error

error-invalid-shortcut

error-invalid-shortcut

Error message

Shortcut provided already exists

What it means

saveCannedResponse (saveCannedResponse.ts:61) enforces shortcut uniqueness across the whole CannedResponse collection: CannedResponse.findOneByShortcut must not find an existing document (or, on update, must find the same _id). Creating a response with a shortcut that already exists, or updating one to a shortcut owned by a different response, throws error-invalid-shortcut.

Solutions

  1. Pick a different, unique shortcut (e.g. prefix it: !dept-hello)
  2. On update, either keep the current shortcut or verify the target shortcut is free
  3. Pre-check with CannedResponse.findOneByShortcut before submitting the form and surface the conflict early

Example fix

// before
const responseData = { shortcut: '!hello', text: 'Hi!', scope: 'user' };
await saveCannedResponse(uid, responseData);

// after
const dup = await CannedResponse.findOneByShortcut(responseData.shortcut, { projection: { _id: 1 } });
if (dup && dup._id !== _id) {
	throw new Meteor.Error('error-invalid-shortcut', 'Shortcut provided already exists');
}
await saveCannedResponse(uid, responseData);
Defensive patterns

Strategy: validation

Validate before calling

const duplicate = await CannedResponse.findOneByShortcut(responseData.shortcut, { projection: { _id: 1 } });
if ((!_id && duplicate) || (_id && duplicate && duplicate._id !== _id)) {
	throw new Meteor.Error('error-invalid-shortcut', 'Shortcut provided already exists');
}
await saveCannedResponse(userId, responseData, _id);

Try / catch

try {
	await saveCannedResponse(userId, responseData, _id);
} catch (e) {
	if (e instanceof Meteor.Error && e.error === 'error-invalid-shortcut') {
		// prompt for a different shortcut; uniqueness is collection-wide
	}
	throw e;
}

Prevention

When it happens

Trigger: Creating a canned response whose shortcut collides with an existing one in any scope (user/department/global — the lookup is not scope-filtered); or updating response A to the shortcut currently used by response B (duplicateShortcut._id !== _id).

Common situations: Two agents independently pick !hello; import/migration scripts inserting duplicate shortcuts; renaming an existing response's shortcut to a taken value; department-specific intent foiled by the global uniqueness rule.

Related errors


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

Appendix: source

Thrown at apps/meteor/ee/server/meteor-methods/saveCannedResponse.ts:61

	const canSaveDepartment = await hasPermissionAsync(userId, 'save-department-canned-responses');
	if (!canSaveAll && !canSaveDepartment && ['department'].includes(responseData.scope)) {
		throw new Meteor.Error('error-not-allowed', 'Not allowed to modify canned responses on *department* scope', {
			method: 'saveCannedResponse',
		});
	}

	// to avoid inconsistencies
	if (responseData.scope === 'user') {
		delete responseData.departmentId;
	}
	// TODO: check if the department i'm trying to save is a department i can interact with

	// check if the response already exists and we're not updating one
	const duplicateShortcut = await CannedResponse.findOneByShortcut(responseData.shortcut, {
		projection: { _id: 1 },
	});
	if ((!_id && duplicateShortcut) || (_id && duplicateShortcut && duplicateShortcut._id !== _id)) {
		throw new Meteor.Error('error-invalid-shortcut', 'Shortcut provided already exists', {
			method: 'saveCannedResponse',
		});
	}

	if (responseData.scope === 'department' && !responseData.departmentId) {
		throw new Meteor.Error('error-invalid-department', 'Invalid department', {
			method: 'saveCannedResponse',
		});
	}

	if (
		responseData.departmentId &&
		!(await LivechatDepartment.findOneById<Pick<ILivechatDepartment, '_id'>>(responseData.departmentId, { projection: { _id: 1 } }))
	) {
		throw new Meteor.Error('error-invalid-department', 'Invalid department', {
			method: 'saveCannedResponse',
		});
	}

View on GitHub (pinned to b2c16d5842)