RocketChat/Rocket.Chat · warning · Error

invalid-call-status

Error message

invalid-call-status

What it means

Thrown by VideoConferenceService.cancel when the direct call exists but is not cancellable: status !== VideoConferenceStatus.CALLING, or endedBy/endedAt is already set. Cancel means declining while the phone is still ringing; once the call was accepted, declined elsewhere, or timed out, this transition is rejected. This guard is inherently racy in real UIs.

Source

Thrown at apps/meteor/server/services/video-conference/service.ts:228

			{
				blockId: 'videoconf-info',
				type: 'section',
				text: {
					type: 'mrkdwn',
					text: `**${i18n.t('Video_Conference_Url')}**: ${call.url}`,
				},
			},
		];
	}

	public async cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise<void> {
		const call = await VideoConferenceModel.findOneById(callId);
		if (!call || !isDirectVideoConference(call)) {
			throw new Error('invalid-call');
		}

		if (call.status !== VideoConferenceStatus.CALLING || call.endedBy || call.endedAt) {
			throw new Error('invalid-call-status');
		}

		const user = await Users.findOneById(uid);
		if (!user) {
			throw new Error('failed-to-load-own-data');
		}

		await VideoConferenceModel.setDataById(callId, {
			ringing: false,
			status: VideoConferenceStatus.DECLINED,
			endedAt: new Date(),
			endedBy: {
				_id: user._id,
				name: user.name as string,
				username: user.username as string,
			},
		});

View on GitHub (pinned to e4b8178b20)

Solutions

  1. Treat invalid-call-status on cancel as benign — refresh the UI to the call's current status instead of retrying
  2. Drive the ringing modal from live call-status events so it closes as soon as status changes
  3. Debounce the decline button to kill double-submits
  4. Optionally fetch current status before cancelling, but keep the catch because the race cannot be fully prevented

Example fix

// before
await VideoConfService.cancel(uid, callId);

// after
try {
  await VideoConfService.cancel(uid, callId);
} catch (err) {
  if (err instanceof Error && err.message === 'invalid-call-status') {
    // already answered/declined/ended — just refresh call state
  } else {
    throw err;
  }
}
Defensive patterns

Strategy: try-catch

Validate before calling

const call = await VideoConferenceModel.findOneById(callId);
if (call?.status !== VideoConferenceStatus.CALLING || call.endedBy || call.endedAt) {
  // no longer ringing: refresh state instead of cancelling
}

Type guard

function isRingingCall(call: VideoConference | null): call is VideoConference {
  return Boolean(call && call.status === VideoConferenceStatus.CALLING && !call.endedBy && !call.endedAt);
}

Try / catch

try {
  await VideoConfService.cancel(uid, callId);
} catch (err) {
  if (err instanceof Error && err.message === 'invalid-call-status') return; // benign race: refresh UI
  throw err;
}

Prevention

When it happens

Trigger: Caller cancel and callee decline racing (the loser gets this error); cancelling after the caller hung up; ringing timeout already moved status away from CALLING; double-tap on the decline button firing two requests.

Common situations: Two devices acting on the same ringing call; UI retrying a timed-out cancel; stale ringing modal after the call was answered elsewhere.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


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