calcom/cal.diy · error · HttpError

Failed to fetch project details

Error message

Failed to fetch project details

What it means

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).

Source

Thrown at packages/app-store/basecamp3/api/projectMutation.ts:64

  let credentialKey = credential.key as BasecampToken;

  if (credentialKey.expires_at < Date.now()) {
    credentialKey = (await refreshAccessToken(credential)) as BasecampToken;
  }

  const basecampUserId = credentialKey.account.id;
  const scheduleResponse = await fetch(
    `https://3.basecampapi.com/${basecampUserId}/projects/${projectId}.json`,
    {
      headers: {
        "User-Agent": user_agent as string,
        Authorization: `Bearer ${credentialKey.access_token}`,
      },
    }
  );

  if (!scheduleResponse.ok) {
    throw new HttpError({ statusCode: 400, message: "Failed to fetch project details" });
  }

  const scheduleJson = await scheduleResponse.json();
  const scheduleId = scheduleJson.dock.find((dock: IDock) => dock.name === "schedule").id;

  await prisma.credential.update({
    where: { id: credential.id },
    data: { key: { ...credentialKey, projectId: Number(projectId), scheduleId } },
  });

  return { message: "Updated project successfully" };
}

export default defaultHandler({
  POST: Promise.resolve({ default: defaultResponder(handler) }),
});

View on GitHub (pinned to 176037d0af)

Solutions

  1. Inspect scheduleResponse.status (log it before throwing) to distinguish 401/403/404/429/5xx.
  2. Re-connect Basecamp 3 if the access token was revoked or the refresh failed.
  3. Confirm the projectId belongs to the authenticated Basecamp account.
  4. Retry on transient 5xx/429 with backoff.

Example fix

// before
if (!scheduleResponse.ok) {
  throw new HttpError({ statusCode: 400, message: 'Failed to fetch project details' });
}

// after
if (!scheduleResponse.ok) {
  throw new HttpError({
    statusCode: scheduleResponse.status === 401 ? 401 : 400,
    message: `Failed to fetch project details (upstream ${scheduleResponse.status})`,
  });
}
Defensive patterns

Strategy: retry

Validate before calling

if (!scheduleResponse.ok && scheduleResponse.status >= 500) {
  // transient upstream failure - safe to retry with backoff
}

Try / catch

try {
  // fetch basecamp project
} catch (e) {
  if (e instanceof HttpError && e.message === 'Failed to fetch project details') {
    // re-connect Basecamp on 401/403; retry on 5xx
  }
}

Prevention

When it happens

Trigger: 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.

Common situations: Token revoked in Basecamp account settings; projectId copied from a different Basecamp account; transient 5xx or 429 from 37signals.

Related errors


AI-assisted analysis of calcom/cal.diy@176037d0af (2026-08-12). Data as JSON: /api/errors/ee02e4bf20fd2857. Report an issue: GitHub.