{"record":{"id":"60b44049d19936ea","repo":"RocketChat/Rocket.Chat","slug":"invalid-call-status","errorCode":null,"errorMessage":"invalid-call-status","messagePattern":"invalid-call-status","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"apps/meteor/server/services/video-conference/service.ts","lineNumber":228,"sourceCode":"\t\t\t{\n\t\t\t\tblockId: 'videoconf-info',\n\t\t\t\ttype: 'section',\n\t\t\t\ttext: {\n\t\t\t\t\ttype: 'mrkdwn',\n\t\t\t\t\ttext: `**${i18n.t('Video_Conference_Url')}**: ${call.url}`,\n\t\t\t\t},\n\t\t\t},\n\t\t];\n\t}\n\n\tpublic async cancel(uid: IUser['_id'], callId: VideoConference['_id']): Promise<void> {\n\t\tconst call = await VideoConferenceModel.findOneById(callId);\n\t\tif (!call || !isDirectVideoConference(call)) {\n\t\t\tthrow new Error('invalid-call');\n\t\t}\n\n\t\tif (call.status !== VideoConferenceStatus.CALLING || call.endedBy || call.endedAt) {\n\t\t\tthrow new Error('invalid-call-status');\n\t\t}\n\n\t\tconst user = await Users.findOneById(uid);\n\t\tif (!user) {\n\t\t\tthrow new Error('failed-to-load-own-data');\n\t\t}\n\n\t\tawait VideoConferenceModel.setDataById(callId, {\n\t\t\tringing: false,\n\t\t\tstatus: VideoConferenceStatus.DECLINED,\n\t\t\tendedAt: new Date(),\n\t\t\tendedBy: {\n\t\t\t\t_id: user._id,\n\t\t\t\tname: user.name as string,\n\t\t\t\tusername: user.username as string,\n\t\t\t},\n\t\t});\n","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/e4b8178b205510181a96ceefee043d0abcd13e5a/apps/meteor/server/services/video-conference/service.ts#L210-L246","documentation":"Thrown by VideoConferenceService.cancel when the direct call exists but is not cancellable: status !== VideoConferenceStatus.CALLING, or endedBy/endedAt is already set. Cancel means declining while the phone is still ringing; once the call was accepted, declined elsewhere, or timed out, this transition is rejected. This guard is inherently racy in real UIs.","triggerScenarios":"Caller cancel and callee decline racing (the loser gets this error); cancelling after the caller hung up; ringing timeout already moved status away from CALLING; double-tap on the decline button firing two requests.","commonSituations":"Two devices acting on the same ringing call; UI retrying a timed-out cancel; stale ringing modal after the call was answered elsewhere.","solutions":["Treat invalid-call-status on cancel as benign — refresh the UI to the call's current status instead of retrying","Drive the ringing modal from live call-status events so it closes as soon as status changes","Debounce the decline button to kill double-submits","Optionally fetch current status before cancelling, but keep the catch because the race cannot be fully prevented"],"exampleFix":"// before\nawait VideoConfService.cancel(uid, callId);\n\n// after\ntry {\n  await VideoConfService.cancel(uid, callId);\n} catch (err) {\n  if (err instanceof Error && err.message === 'invalid-call-status') {\n    // already answered/declined/ended — just refresh call state\n  } else {\n    throw err;\n  }\n}","handlingStrategy":"try-catch","validationCode":"const call = await VideoConferenceModel.findOneById(callId);\nif (call?.status !== VideoConferenceStatus.CALLING || call.endedBy || call.endedAt) {\n  // no longer ringing: refresh state instead of cancelling\n}","typeGuard":"function isRingingCall(call: VideoConference | null): call is VideoConference {\n  return Boolean(call && call.status === VideoConferenceStatus.CALLING && !call.endedBy && !call.endedAt);\n}","tryCatchPattern":"try {\n  await VideoConfService.cancel(uid, callId);\n} catch (err) {\n  if (err instanceof Error && err.message === 'invalid-call-status') return; // benign race: refresh UI\n  throw err;\n}","preventionTips":["Never retry cancel on invalid-call-status — refetch the status instead","Debounce decline buttons","Drive ringing UI from a call-status stream, not one-shot state"],"tags":["video-conference","call-state","race-condition"],"backgroundTag":"invalid-state-transition","analyzedSha":"e4b8178b205510181a96ceefee043d0abcd13e5a","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}