RocketChat/Rocket.Chat · error · Error

invalid-call

Error message

invalid-call

What it means

Thrown by VideoConferenceService.join when the call cannot be joined: findOneById returns no document, the call already has endedAt set, or videoConfTypes.isCallManagedByApp(call) is false (the call's provider is not app-managed, so joining goes through the provider URL instead). All three states collapse into this single error, so inspect the call document to distinguish them. The user lookup happens after, so this is the first join guard.

Source

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

			if (data.type === 'videoconference') {
				data.title = title;
			}

			return this.create(data, false);
		}).catch((err) => {
			logger.error({
				name: 'Error on VideoConf.start',
				err,
			});
			throw err;
		});
	}

	public async join(uid: IUser['_id'] | undefined, callId: VideoConference['_id'], options: VideoConferenceJoinOptions): Promise<string> {
		return wrapExceptions(async () => {
			const call = await VideoConferenceModel.findOneById(callId);
			if (!call || call.endedAt || !videoConfTypes.isCallManagedByApp(call)) {
				throw new Error('invalid-call');
			}

			let user: Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'> | null = null;

			if (uid) {
				user = await Users.findOneById<Pick<IUser, '_id' | 'username' | 'name' | 'avatarETag'>>(uid, {
					projection: { name: 1, username: 1, avatarETag: 1 },
				});
				if (!user) {
					throw new Error('failed-to-load-own-data');
				}
			}

			if (call.providerName === 'jitsi') {
				updateCounter({ settingsId: 'Jitsi_Click_To_Join_Count' });
			}

			return this.joinCall(call, user || undefined, options);

View on GitHub (pinned to e4b8178b20)

Solutions

  1. Before joining, load the call and require !call.endedAt && videoConfTypes.isCallManagedByApp(call)
  2. Treat invalid-call on join as 'call over': dismiss the ringing modal and stop timers — do not blind-retry
  3. Use fresh call ids from the call message / callStarted events rather than stored ones
  4. If the provider is not app-managed, join via the call's url instead of the join endpoint

Example fix

// before
const url = await VideoConfService.join(uid, callId, options);

// after
const call = await VideoConferenceModel.findOneById(callId);
if (!call || call.endedAt || !videoConfTypes.isCallManagedByApp(call)) {
  ui.dismissRinging();
  throw new Error('invalid-call');
}
const url = await VideoConfService.join(uid, callId, options);
Defensive patterns

Strategy: try-catch

Validate before calling

const call = await VideoConferenceModel.findOneById(callId);
if (!call || call.endedAt || !videoConfTypes.isCallManagedByApp(call)) {
  throw new Meteor.Error('invalid-call', 'Call is gone or not joinable');
}

Type guard

function isJoinableAppCall(call: VideoConference | null): call is VideoConference {
  return Boolean(call && !call.endedAt && videoConfTypes.isCallManagedByApp(call));
}

Try / catch

try {
  const url = await VideoConfService.join(uid, callId, options);
} catch (err) {
  if (err instanceof Error && err.message === 'invalid-call') {
    // racing with decline/cancel is normal: close the ringing UI, do not retry blindly
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: POST video-conference.join with an expired, declined, or already-ended call id; joining after the ringing timeout; reusing a call id whose provider is not app-managed (join must use call.url instead); mistyped callId.

Common situations: Callee opens the ringing popup after the caller cancelled; two devices race to accept the same call; stale call ids left in UI state after navigation.

Related errors


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