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
- Before joining, load the call and require !call.endedAt && videoConfTypes.isCallManagedByApp(call)
- Treat invalid-call on join as 'call over': dismiss the ringing modal and stop timers — do not blind-retry
- Use fresh call ids from the call message / callStarted events rather than stored ones
- 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
- Treat join as racy — always handle invalid-call gracefully
- Expire ringing UIs on a timer matching the server ring timeout
- Listen for callEnded events to invalidate stored call ids
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
- invalid-call-status
- A video conference must exist to update.
- error-role-protected
- error-role-in-use
- invalid call
AI-assisted analysis of RocketChat/Rocket.Chat@e4b8178b20 (2026-08-18).
Data as JSON: /api/errors/5b42eb0ddf3204cd.
Report an issue: GitHub.