{"record":{"id":"0a1852363991cf58","repo":"calcom/cal.diy","slug":"calendar-credentials-not-found","errorCode":null,"errorMessage":"Calendar credentials not found","messagePattern":"Calendar credentials not found","errorType":"http","errorClass":"NotFoundException","httpStatus":404,"severity":"error","filePath":"apps/api/v2/src/platform/calendars/services/calendars.service.ts","lineNumber":199,"sourceCode":"    }\n\n    const { client_id, client_secret } = this.oAuthCalendarResponseSchema.parse(app.keys);\n\n    if (!client_id) {\n      throw new NotFoundException();\n    }\n\n    if (!client_secret) {\n      throw new NotFoundException();\n    }\n\n    return { client_id, client_secret };\n  }\n\n  async checkCalendarCredentials(credentialId: number, userId: number) {\n    const credential = await this.calendarsRepository.getCalendarCredentials(credentialId, userId);\n    if (!credential) {\n      throw new NotFoundException(\"Calendar credentials not found\");\n    }\n  }\n\n  async createAndLinkCalendarEntry(\n    userId: number,\n    externalId: string,\n    key: Prisma.InputJsonValue,\n    calendarType: keyof typeof APPS_TYPE_ID_MAPPING,\n    credentialId?: number | null\n  ) {\n    const credential = await this.credentialsRepository.upsertUserAppCredential(\n      calendarType,\n      key,\n      userId,\n      credentialId\n    );\n\n    await this.selectedCalendarsRepository.upsertSelectedCalendar(","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/apps/api/v2/src/platform/calendars/services/calendars.service.ts#L181-L217","documentation":"Thrown by CalendarsService.checkCalendarCredentials (calendars.service.ts:199) as NotFoundException (HTTP 404) when calendarsRepository.getCalendarCredentials(credentialId, userId) returns null — the credentialId does not exist or does not belong to the user. Used as the guard at the start of the disconnect endpoint.","triggerScenarios":"POST /v2/calendars/{calendar}/disconnect with a credentialId already deleted or owned by another user; double-submit of disconnect; client passing a stale id.","commonSituations":"User clicks disconnect twice; client holds a stale id after a previous disconnect; cross-user id passed accidentally.","solutions":["Use a credentialId sourced from GET /v2/calendars for the current user.","Treat a 404 on disconnect as 'already disconnected' (idempotent) in the client rather than an error.","Guard the UI: disable the disconnect button once the credential is no longer listed."],"exampleFix":"// before: client treats 404 as fatal\ntry { await api.post('/v2/calendars/google_calendar/disconnect', { id: credId }); }\ncatch (e) { throw e; }\n\n// after: treat 404 as already-disconnected\ntry { await api.post('/v2/calendars/google_calendar/disconnect', { id: credId }); }\ncatch (e) {\n  if (e.response?.status !== 404) throw e;\n  // already gone — refresh the list\n  await refreshCalendars();\n}","handlingStrategy":"try-catch","validationCode":"// Verify ownership before disconnect\nconst { data } = await api.get('/v2/calendars');\nconst owned = data.connectedCalendars.some(c => c.credentialId === credentialId);\nif (!owned) {\n  // already disconnected — nothing to do\n  return { status: 'already_disconnected' };\n}\nawait api.post('/v2/calendars/google_calendar/disconnect', { id: credentialId });","typeGuard":"function isOwnedCredential(list, credentialId) {\n  return Array.isArray(list) && list.some(c => c?.credentialId === credentialId);\n}","tryCatchPattern":"try {\n  await api.post('/v2/calendars/google_calendar/disconnect', { id: credentialId });\n} catch (e) {\n  if (e.response?.status === 404) {\n    // idempotent: already gone\n    return { status: 'already_disconnected' };\n  }\n  throw e;\n}","preventionTips":["Source the disconnect credentialId from GET /v2/calendars, not stale client state.","Treat 404 on disconnect as success (idempotent) in the client.","Disable the disconnect control once the credential is no longer listed."],"tags":["credentials","disconnect","not-found","http-404"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}