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

  1. Retry the flow from a fresh lookup: if the call is truly gone, surface 'call ended' to the user instead of an error
  2. Check server logs for a concurrent end/timeout of the same callId to confirm the race
  3. 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

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


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