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
- Send each call transition exactly once and only in order (offer → ack → accept/reject) from a single owning session
- On invalid-call-state for an ack, resync the UI from the server's current call state instead of retrying the ack
- Guard duplicate sends with an 'already acknowledged' flag in the client
- 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
- Send each call signal exactly once from a single owning session
- On invalid-call-state, resync from server state rather than retrying
- Ignore offers for calls the local state already marks accepted/ended
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
- not-found
- internal-error
- Import operation not initialized.
- The current import operation can not receive new data.
- The current import operation is already finished.
AI-assisted analysis of RocketChat/Rocket.Chat@b2c16d5842 (2026-08-18).
Data as JSON: /api/errors/fff1306f9a0ddd71.
Report an issue: GitHub.