RocketChat/Rocket.Chat · error · Error
internal-error
Error message
internal-error
What it means
A defensive guard in answerCall(): the call was found, the WebRTC signal was processed via callServer.receiveSignal(), but re-reading the call with MediaCalls.findOneById immediately afterwards returned nothing. The call document disappeared mid-operation — normally because the call was concurrently ended and deleted — so the post-signal state validation cannot continue and 'internal-error' is thrown.
Source
Thrown at apps/meteor/server/services/media-call/service.ts:74
const call = await MediaCalls.findOneByIdAndCallee<Pick<IMediaCall, '_id'>>(
callId,
{ type: 'user', id: uid },
{ projection: { _id: 1 } },
);
if (!call) {
throw new Error('not-found');
}
const signal: ClientMediaSignalAnswer = {
type: 'answer',
...params,
};
await callServer.receiveSignal(uid, signal, { throwIfSkipped: true });
const updatedCall = await MediaCalls.findOneById(callId);
if (!updatedCall) {
throw new Error('internal-error');
}
switch (answer) {
case 'ack':
if (updatedCall.acceptedAt || updatedCall.ended) {
throw new Error('invalid-call-state');
}
break;
case 'reject':
if (!updatedCall.ended || updatedCall.endedBy?.id !== uid) {
throw new Error('invalid-call-state');
}
break;
case 'accept':
if (updatedCall.callee.contractId !== signal.contractId) {
if (updatedCall.callee.contractId) {
throw new Error('invalid-call-state');
}View on GitHub (pinned to b2c16d5842)
Solutions
- Retry the flow from a fresh lookup: if the call is truly gone, surface 'call ended' to the user instead of an error
- Check server logs for a concurrent end/timeout of the same callId to confirm the race
- If it reproduces consistently with the call still present, inspect custom code paths that delete MediaCalls documents during signaling
Defensive patterns
Strategy: try-catch
Validate before calling
// reduce (not eliminate) the window: confirm the call still exists right before answering
const call = await MediaCalls.findOneById(callId);
if (!call || call.ended) {
return handleCallEnded(callId);
}
await mediaCallService.answerCall(uid, params); Type guard
const isLiveCall = (call: IMediaCall | null): call is IMediaCall => !!call && !call.ended;
Try / catch
try {
return await mediaCallService.answerCall(uid, params);
} catch (err) {
if (err instanceof Error && err.message === 'internal-error') {
const stillExists = await MediaCalls.findOneById(params.callId);
if (!stillExists) return handleCallEnded(params.callId); // lost race with hangup: not an error for the user
}
throw err;
} Prevention
- Act on remote 'ended' events immediately so answers are not sent for terminated calls
- Keep signaling operations short to shrink race windows
- Make clients tolerant of signals referencing calls that no longer exist
When it happens
Trigger: The other party (or a timeout sweeper) ends and removes the call between the initial findOneByIdAndCallee and the follow-up read inside answerCall — a race between answering and terminating the same call.
Common situations: Caller hangs up at the same moment the callee answers; call-expiry jobs removing calls during signaling; heavy load widening the race window; custom signaling clients sending delayed answers.
Related errors
- not-found
- invalid-call-state
- error-invalid-user
- apps-engine-not-initialized
- Business hour ${department.businessHourId} not found
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/a6b5c2075cb847d1.
Report an issue: GitHub.