{"record":{"id":"03df1b6331821222","repo":"calcom/cal.diy","slug":"already-installed-03df1b","errorCode":null,"errorMessage":"Already installed","messagePattern":"Already installed","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/app-store/giphy/api/add.ts","lineNumber":34,"sourceCode":"    return res.status(401).json({ message: \"You must be logged in to do this\" });\n  }\n\n  const userId = req.session.user.id;\n  const appType = \"giphy_other\";\n  const teamId = Number(req.query.teamId);\n  const credentialOwner = req.query.teamId ? { teamId } : { userId: req.session.user.id };\n\n  await throwIfNotHaveAdminAccessToTeam({ teamId: teamId ?? null, userId });\n\n  try {\n    const alreadyInstalled = await prisma.credential.findFirst({\n      where: {\n        type: appType,\n        ...credentialOwner,\n      },\n    });\n    if (alreadyInstalled) {\n      throw new Error(\"Already installed\");\n    }\n    const installation = await prisma.credential.create({\n      data: {\n        type: appType,\n        key: {},\n        ...credentialOwner,\n        appId: \"giphy\",\n      },\n    });\n    if (!installation) {\n      throw new Error(\"Unable to create user credential for giphy\");\n    }\n  } catch (error: unknown) {\n    const httpError = getServerErrorFromUnknown(error);\n    return res.status(httpError.statusCode).json({ message: httpError.message });\n  }\n\n  return res.status(200).json({ url: getInstalledAppPath({ variant: \"other\", slug: \"giphy\" }) });","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/giphy/api/add.ts#L16-L52","documentation":"Thrown by the Giphy install handler when a `credential` row of type `giphy_other` already exists for the same owner (user or team). It is a plain `new Error(...)`, so `getServerErrorFromUnknown` routes it through `getHttpStatusCode`, whose switch falls through to the `default` branch because \"Already installed\" is not a known ErrorCode — yielding HTTP 500, not the 409 Conflict the message implies. The client therefore sees a 500 with body `{ message: \"Already installed\" }` for an entirely expected duplicate-install condition.","triggerScenarios":"A request to the Giphy install endpoint (`/api/integrations/giphy/add`, with or without `?teamId=`) when `prisma.credential.findFirst` already returns a `giphy_other` credential for that `userId`/`teamId`. Concretely: double-clicking Install, resubmitting after a first successful install, two concurrent requests both passing the `findFirst` check before either `create`s.","commonSituations":"User clicks Install twice before navigating away; page refresh resubmits the install form; team admin re-installs for a team that already has Giphy; race condition between two near-simultaneous install requests.","solutions":["Fix the source: throw `new HttpError({ statusCode: 409, message: \"Already installed\" })` so the client gets a correct, handlable status instead of a misleading 500.","Make the client idempotent: query installed apps before calling install and skip when Giphy is already present.","Debounce/disable the Install button after the first click to prevent double-submit.","On the client, treat a response whose message is \"Already installed\" as a no-op success and proceed to the installed-apps view."],"exampleFix":"// before\nif (alreadyInstalled) {\n  throw new Error(\"Already installed\");\n}\n// after\nimport { HttpError } from \"@calcom/lib/http-error\";\nif (alreadyInstalled) {\n  throw new HttpError({ statusCode: 409, message: \"Already installed\" });\n}","handlingStrategy":"validation","validationCode":"// Before calling the Giphy install endpoint, confirm it isn't already installed\nimport prisma from \"@calcom/prisma\";\n\nasync function ensureGiphyNotInstalled(owner: { userId: number } | { teamId: number }) {\n  const existing = await prisma.credential.findFirst({\n    where: { type: \"giphy_other\", ...owner },\n    select: { id: true },\n  });\n  return existing === null; // true => safe to install\n}\n\n// usage:\nif (await ensureGiphyNotInstalled({ userId })) {\n  await fetch(\"/api/integrations/giphy/add\", { method: \"POST\" });\n}","typeGuard":null,"tryCatchPattern":"// Treat the (currently 500) 'Already installed' response as a benign no-op\ntry {\n  const res = await fetch(\"/api/integrations/giphy/add\", { method: \"POST\" });\n  if (!res.ok) throw new Error(await res.text());\n} catch (e) {\n  if (/Already installed/.test(String(e))) {\n    // already installed — proceed to the installed-apps view\n  } else {\n    throw e;\n  }\n}","preventionTips":["Disable the Install button immediately after the first click to prevent double-submit.","Query installed apps on page load and hide Install for apps already present.","Once the source is fixed to 409, branch on status code rather than message text."],"tags":["app-store","giphy","duplicate-install","http-status","prisma"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}