BloopAI/vibe-kanban · error · Error

Failed to list organizations (${res.status})

Error message

Failed to list organizations (${res.status})

What it means

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.

Source

Thrown at packages/remote-web/src/shared/lib/api.ts:199

  }

  return res;
}

export async function logout(): Promise<void> {
  try {
    await authenticatedFetch(`${API_BASE}/v1/oauth/logout`, {
      method: "POST",
    });
  } finally {
    await clearTokens();
  }
}

export async function listOrganizations(): Promise<ListOrganizationsResponse> {
  const res = await authenticatedFetch(`${API_BASE}/v1/organizations`);
  if (!res.ok) {
    throw new Error(`Failed to list organizations (${res.status})`);
  }
  return res.json();
}

export async function getIdentity(): Promise<IdentityResponse> {
  const res = await authenticatedFetch(`${API_BASE}/v1/identity`);
  if (!res.ok) {
    throw new Error(`Failed to fetch identity (${res.status})`);
  }
  return res.json();
}

export async function listOrganizationProjects(
  organizationId: string,
): Promise<Project[]> {
  const params = new URLSearchParams({
    organization_id: organizationId,
  });

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Log out and log back in to obtain fresh access and refresh tokens.
  2. Read the status in the message: 401 means refresh failed (re-authenticate), 403 means no permission, 5xx means server-side — check backend health/logs.
  3. Verify VITE_API_BASE_URL is set correctly in the remote-web deployment environment.
  4. Inspect the response body in devtools for the server's error detail.

Example fix

// before
if (!res.ok) {
  throw new Error(`Failed to list organizations (${res.status})`);
}
// after
if (!res.ok) {
  const err = new Error(`Failed to list organizations (${res.status})`);
  (err as Error & { status: number }).status = res.status;
  if (res.status === 401) await clearTokens(); // force re-login
  throw err;
}
Defensive patterns

Strategy: try-catch

Validate before calling

const accessToken = await getToken();
if (!accessToken) {
  redirectToLogin(); // no point calling without credentials
}

Type guard

function isListOrganizationsResponse(v: unknown): v is ListOrganizationsResponse {
  const r = v as ListOrganizationsResponse;
  return !!r && Array.isArray((r as { organizations?: unknown[] }).organizations);
}

Try / catch

try {
  const orgs = await listOrganizations();
  setOrganizations(orgs.organizations);
} catch (e) {
  const status = Number(/\((\d{3})\)$/.exec((e as Error).message)?.[1]);
  if (status === 401) {
    await clearTokens();
    redirectToLogin();
  } else {
    showRetryBanner('Could not load organizations', () => queryClient.invalidateQueries());
  }
}

Prevention

When it happens

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

Common situations: 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.

Related errors


AI-assisted analysis of BloopAI/vibe-kanban@4deb7eca8f (2026-08-29). Data as JSON: /api/errors/d70106a1051cc392. Report an issue: GitHub.