{"record":{"id":"ee02e4bf20fd2857","repo":"calcom/cal.diy","slug":"failed-to-fetch-project-details","errorCode":null,"errorMessage":"Failed to fetch project details","messagePattern":"Failed to fetch project details","errorType":"http","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"packages/app-store/basecamp3/api/projectMutation.ts","lineNumber":64,"sourceCode":"  let credentialKey = credential.key as BasecampToken;\n\n  if (credentialKey.expires_at < Date.now()) {\n    credentialKey = (await refreshAccessToken(credential)) as BasecampToken;\n  }\n\n  const basecampUserId = credentialKey.account.id;\n  const scheduleResponse = await fetch(\n    `https://3.basecampapi.com/${basecampUserId}/projects/${projectId}.json`,\n    {\n      headers: {\n        \"User-Agent\": user_agent as string,\n        Authorization: `Bearer ${credentialKey.access_token}`,\n      },\n    }\n  );\n\n  if (!scheduleResponse.ok) {\n    throw new HttpError({ statusCode: 400, message: \"Failed to fetch project details\" });\n  }\n\n  const scheduleJson = await scheduleResponse.json();\n  const scheduleId = scheduleJson.dock.find((dock: IDock) => dock.name === \"schedule\").id;\n\n  await prisma.credential.update({\n    where: { id: credential.id },\n    data: { key: { ...credentialKey, projectId: Number(projectId), scheduleId } },\n  });\n\n  return { message: \"Updated project successfully\" };\n}\n\nexport default defaultHandler({\n  POST: Promise.resolve({ default: defaultResponder(handler) }),\n});\n","sourceCodeStart":46,"sourceCodeEnd":81,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/basecamp3/api/projectMutation.ts#L46-L81","documentation":"Thrown when the upstream call to https://3.basecampapi.com/${basecampUserId}/projects/${projectId}.json returns non-ok. Causes: an invalid or inaccessible projectId, an expired/revoked access token, a Basecamp (37signals) API outage, or rate-limiting. Surfaced as HTTP 400 (note: the status is somewhat inaccurate since the upstream cause may be 401/403/404/429/5xx).","triggerScenarios":"projectId that does not exist or is not accessible by the Basecamp account; access token revoked by the user in Basecamp; refresh-token logic failed silently; 37signals API down or rate-limiting.","commonSituations":"Token revoked in Basecamp account settings; projectId copied from a different Basecamp account; transient 5xx or 429 from 37signals.","solutions":["Inspect scheduleResponse.status (log it before throwing) to distinguish 401/403/404/429/5xx.","Re-connect Basecamp 3 if the access token was revoked or the refresh failed.","Confirm the projectId belongs to the authenticated Basecamp account.","Retry on transient 5xx/429 with backoff."],"exampleFix":"// before\nif (!scheduleResponse.ok) {\n  throw new HttpError({ statusCode: 400, message: 'Failed to fetch project details' });\n}\n\n// after\nif (!scheduleResponse.ok) {\n  throw new HttpError({\n    statusCode: scheduleResponse.status === 401 ? 401 : 400,\n    message: `Failed to fetch project details (upstream ${scheduleResponse.status})`,\n  });\n}","handlingStrategy":"retry","validationCode":"if (!scheduleResponse.ok && scheduleResponse.status >= 500) {\n  // transient upstream failure - safe to retry with backoff\n}","typeGuard":null,"tryCatchPattern":"try {\n  // fetch basecamp project\n} catch (e) {\n  if (e instanceof HttpError && e.message === 'Failed to fetch project details') {\n    // re-connect Basecamp on 401/403; retry on 5xx\n  }\n}","preventionTips":["Log scheduleResponse.status before throwing to distinguish upstream codes.","Re-connect Basecamp 3 when the access/refresh token is revoked.","Confirm projectId belongs to the authenticated Basecamp account."],"tags":["basecamp3","upstream-api","oauth","network"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}