{"record":{"id":"982819d42295fe03","repo":"calcom/cal.diy","slug":"failed-to-fetch-basecamp-projects","errorCode":null,"errorMessage":"Failed to fetch Basecamp projects","messagePattern":"Failed to fetch Basecamp projects","errorType":"http","errorClass":"HttpError","httpStatus":400,"severity":"error","filePath":"packages/app-store/basecamp3/api/projects.ts","lineNumber":45,"sourceCode":"  }\n\n  let credentialKey = credential.key as BasecampToken;\n\n  if (!credentialKey.account) {\n    return { currentProject: null, projects: [] };\n  }\n\n  if (credentialKey.expires_at < Date.now()) {\n    credentialKey = (await refreshAccessToken(credential)) as BasecampToken;\n  }\n\n  const url = `${credentialKey.account.href}/projects.json`;\n  const resp = await fetch(url, {\n    headers: { \"User-Agent\": user_agent as string, Authorization: `Bearer ${credentialKey.access_token}` },\n  });\n\n  if (!resp.ok) {\n    throw new HttpError({ statusCode: 400, message: \"Failed to fetch Basecamp projects\" });\n  }\n\n  const projects = await resp.json();\n  return { currentProject: credentialKey.projectId, projects };\n}\n\nexport default defaultHandler({\n  GET: Promise.resolve({ default: defaultResponder(handler) }),\n});\n","sourceCodeStart":27,"sourceCodeEnd":55,"githubUrl":"https://github.com/calcom/cal.diy/blob/176037d0afbe572f870a3c702985e7cd83fe6c0c/packages/app-store/basecamp3/api/projects.ts#L27-L55","documentation":"Thrown by the Basecamp 3 projects endpoint after the upstream GET to `${account.href}/projects.json` returns a non-2xx response. The handler refreshes the OAuth access token when `credentialKey.expires_at < Date.now()`, then issues an authenticated Bearer request; any non-OK response from Basecamp collapses into this HttpError 400. The actual upstream status and body are discarded, so the message is generic.","triggerScenarios":"Calling GET on the Basecamp project-listing route when: (a) the access token is invalid/revoked and `refreshAccessToken` silently returned a token Basecamp rejects; (b) `credentialKey.account.href` points to the wrong Basecamp account URL; (c) the `User-Agent` header is blocked by Basecamp's API; (d) Basecamp returns 401/403/404/500 for any reason.","commonSituations":"Basecamp OAuth app credentials rotated without re-authenticating the user; the stored `account.href` is stale after a Basecamp account rename/migration; clock skew causing `expires_at` checks to use an already-invalid token; Basecamp rate-limiting (429) treated as a hard failure.","solutions":["Inspect the real upstream status: capture `resp.status` and `await resp.text()` into the thrown error message before checking `resp.ok`.","Verify the Basecamp OAuth refresh flow actually persists the new `access_token`/`expires_at` to the credential before the projects call (the reassignment `credentialKey = await refreshAccessToken(credential)` passes `credential`, not `credentialKey` — confirm refresh reads the right token).","Re-authenticate the affected user in the Basecamp integration settings to refresh `account.href` and tokens.","Confirm the `user_agent` constant matches a value Basecamp's API accepts.","Add a retry with backoff for transient 5xx/429 responses before surfacing the 400."],"exampleFix":"// before\nif (!resp.ok) {\n  throw new HttpError({ statusCode: 400, message: \"Failed to fetch Basecamp projects\" });\n}\n\n// after\nif (!resp.ok) {\n  const body = await resp.text().catch(() => \"\");\n  throw new HttpError({\n    statusCode: resp.status === 401 || resp.status === 403 ? resp.status : 502,\n    message: `Failed to fetch Basecamp projects (upstream ${resp.status}): ${body.slice(0, 200)}`,\n  });\n}","handlingStrategy":"try-catch","validationCode":"if (!credentialKey?.account?.href || !credentialKey?.access_token) {\n  throw new Error(\"Basecamp credential incomplete: account.href or access_token missing\");\n}","typeGuard":"function hasBasecampAccount(k: unknown): k is { account: { href: string }; access_token: string; expires_at: number } {\n  return !!k && typeof k === \"object\" && !!(k as any).account?.href && !!(k as any).access_token;\n}","tryCatchPattern":"try {\n  const projects = await fetchBasecampProjects(credential);\n} catch (e) {\n  if (e instanceof HttpError && e.message.includes(\"Basecamp projects\")) {\n    // prompt user to reconnect Basecamp; surface upstream status if available\n  }\n  throw e;\n}","preventionTips":["Validate the credential has account.href and access_token before the fetch.","Persist refreshed tokens immediately so a stale token isn't reused on the next call.","Log resp.status and body on failure instead of collapsing to a 400.","Use a token-refresh wrapper that throws a typed error when refresh itself fails."],"tags":["oauth","upstream-api","http","token-refresh","basecamp"],"backgroundTag":null,"analyzedSha":"176037d0afbe572f870a3c702985e7cd83fe6c0c","analyzedAt":"2026-08-12T19:12:41.464Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}