{"record":{"id":"d70106a1051cc392","repo":"BloopAI/vibe-kanban","slug":"failed-to-list-organizations-res-status","errorCode":null,"errorMessage":"Failed to list organizations (${res.status})","messagePattern":"Failed to list organizations \\((.+?)\\)","errorType":"http","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/remote-web/src/shared/lib/api.ts","lineNumber":199,"sourceCode":"  }\n\n  return res;\n}\n\nexport async function logout(): Promise<void> {\n  try {\n    await authenticatedFetch(`${API_BASE}/v1/oauth/logout`, {\n      method: \"POST\",\n    });\n  } finally {\n    await clearTokens();\n  }\n}\n\nexport async function listOrganizations(): Promise<ListOrganizationsResponse> {\n  const res = await authenticatedFetch(`${API_BASE}/v1/organizations`);\n  if (!res.ok) {\n    throw new Error(`Failed to list organizations (${res.status})`);\n  }\n  return res.json();\n}\n\nexport async function getIdentity(): Promise<IdentityResponse> {\n  const res = await authenticatedFetch(`${API_BASE}/v1/identity`);\n  if (!res.ok) {\n    throw new Error(`Failed to fetch identity (${res.status})`);\n  }\n  return res.json();\n}\n\nexport async function listOrganizationProjects(\n  organizationId: string,\n): Promise<Project[]> {\n  const params = new URLSearchParams({\n    organization_id: organizationId,\n  });","sourceCodeStart":181,"sourceCodeEnd":217,"githubUrl":"https://github.com/BloopAI/vibe-kanban/blob/4deb7eca8f381f7cbc1f9d15515a9ab8f8009053/packages/remote-web/src/shared/lib/api.ts#L181-L217","documentation":"listOrganizations fetches GET {API_BASE}/v1/organizations through authenticatedFetch, which injects a Bearer access token and retries once after a token refresh on 401. If the final response is still not ok, this Error is thrown with the status embedded in the message. Since authenticatedFetch already handles one 401 refresh, seeing this usually means the refresh also failed or another status occurred.","triggerScenarios":"GET /v1/organizations returns 401 even after the one automatic refresh (refresh token revoked/expired), 403 (account lacks access), 5xx (remote server error), or a non-JSON error page because VITE_API_BASE_URL points at the wrong host.","commonSituations":"Long-lived session where both access and refresh tokens expired; user deleted/deactivated on the server; backend down or a proxy returns 502; misconfigured VITE_API_BASE_URL hitting a non-API endpoint.","solutions":["Log out and log back in to obtain fresh access and refresh tokens.","Read the status in the message: 401 means refresh failed (re-authenticate), 403 means no permission, 5xx means server-side — check backend health/logs.","Verify VITE_API_BASE_URL is set correctly in the remote-web deployment environment.","Inspect the response body in devtools for the server's error detail."],"exampleFix":"// before\nif (!res.ok) {\n  throw new Error(`Failed to list organizations (${res.status})`);\n}\n// after\nif (!res.ok) {\n  const err = new Error(`Failed to list organizations (${res.status})`);\n  (err as Error & { status: number }).status = res.status;\n  if (res.status === 401) await clearTokens(); // force re-login\n  throw err;\n}","handlingStrategy":"try-catch","validationCode":"const accessToken = await getToken();\nif (!accessToken) {\n  redirectToLogin(); // no point calling without credentials\n}","typeGuard":"function isListOrganizationsResponse(v: unknown): v is ListOrganizationsResponse {\n  const r = v as ListOrganizationsResponse;\n  return !!r && Array.isArray((r as { organizations?: unknown[] }).organizations);\n}","tryCatchPattern":"try {\n  const orgs = await listOrganizations();\n  setOrganizations(orgs.organizations);\n} catch (e) {\n  const status = Number(/\\((\\d{3})\\)$/.exec((e as Error).message)?.[1]);\n  if (status === 401) {\n    await clearTokens();\n    redirectToLogin();\n  } else {\n    showRetryBanner('Could not load organizations', () => queryClient.invalidateQueries());\n  }\n}","preventionTips":["Treat a 401 here as 'session unrecoverable' — authenticatedFetch already retried after refresh.","Keep refresh tokens rotated so long sessions don't silently expire.","Add an exponential-backoff retry only for 5xx/429, never for 401/403.","Pin VITE_API_BASE_URL in deployment config and smoke-test the /v1/organizations route on deploy."],"tags":["http","api","auth","organizations"],"backgroundTag":"api-request-failed","analyzedSha":"4deb7eca8f381f7cbc1f9d15515a9ab8f8009053","analyzedAt":"2026-08-29T09:24:13.446Z","schemaVersion":2},"datasetVersion":"2026-08-29T12:17:43.993Z"}