RocketChat/Rocket.Chat · error · Error

invalid-call-state

Error message

invalid-call-state

What it means

In answerCall(), answer='ack' is the callee's acknowledgement that the call offer was delivered, and it is only valid while the call is still pending: neither acceptedAt nor ended may be set on the call. invalid-call-state here means the call was already accepted or already terminated when the ack arrived.

Source

Thrown at apps/meteor/server/services/media-call/service.ts:80

			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');
					}
					throw new Error('internal-error');
				}
				break;
		}

		return updatedCall;

View on GitHub (pinned to b2c16d5842)

Solutions

  1. Send each call transition exactly once and only in order (offer → ack → accept/reject) from a single owning session
  2. On invalid-call-state for an ack, resync the UI from the server's current call state instead of retrying the ack
  3. Guard duplicate sends with an 'already acknowledged' flag in the client
  4. Close or serialize call sessions across tabs so only one answers

Example fix

// before
socket.on('offer', () => {
  sendAnswer({ callId, answer: 'ack' });
  // ...a second handler fires later:
  sendAnswer({ callId, answer: 'ack' }); // duplicate -> invalid-call-state
});

// after
let acked = false;
socket.on('offer', () => {
  if (acked) return; // exactly-once
  acked = true;
  sendAnswer({ callId, answer: 'ack' });
});
Defensive patterns

Strategy: try-catch

Validate before calling

const call = await MediaCalls.findOneById(callId);
if (call && !call.acceptedAt && !call.ended) {
  await mediaCallService.answerCall(uid, { callId, answer: 'ack', /* ... */ });
}

Type guard

const isCallPending = (call: IMediaCall | null): call is IMediaCall =>
  !!call && !call.acceptedAt && !call.ended;

Try / catch

try {
  await mediaCallService.answerCall(uid, { callId, answer: 'ack', /* ... */ });
} catch (err) {
  if (err instanceof Error && err.message === 'invalid-call-state') {
    // ack is pointless now: resync from the server's call state, do not resend
    return resyncCallState(callId);
  }
  throw err;
}

Prevention

When it happens

Trigger: Sending 'ack' after an 'accept' or 'reject' was already processed for the same call; an ack arriving after the caller cancelled; a duplicate ack whose second copy lands after the call state moved on.

Common situations: Unstable networks reordering or duplicating signals; client signaling ack and accept in quick succession; retries of 'lost' acks after the call progressed; parallel call sessions in two tabs.

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@b2c16d5842 (2026-08-18). Data as JSON: /api/errors/fff1306f9a0ddd71. Report an issue: GitHub.