BloopAI/vibe-kanban · error · ApiError

errorMessage (dynamic: body.error || body.message || respons

Error message

errorMessage (dynamic: body.error || body.message || response.statusText)

What it means

handleRemoteResponse is the shared response handler for remote-server API calls; when response.ok is false it tries to parse a JSON body for `error` or `message`, falls back to statusText, and throws ApiError(errorMessage, status, response). This is the generic 'remote API returned a non-2xx status' error carrying the server-provided message when available.

Source

Thrown at packages/web-core/src/shared/lib/api.ts:1338

  const { tokenManager } = await import('@/shared/lib/auth/tokenManager');
  return tokenManager.getToken();
}

const handleRemoteResponse = async <T>(response: Response): Promise<T> => {
  if (!response.ok) {
    let errorMessage = `Request failed with status ${response.status}`;

    try {
      const body = (await response.json()) as {
        error?: string;
        message?: string;
      };
      errorMessage = body.error || body.message || errorMessage;
    } catch {
      errorMessage = response.statusText || errorMessage;
    }

    throw new ApiError(errorMessage, response.status, response);
  }

  if (response.status === 204) {
    return undefined as T;
  }

  return response.json() as Promise<T>;
};

// Organizations API
export const organizationsApi = {
  getMembers: async (
    orgId: string
  ): Promise<OrganizationMemberWithProfile[]> => {
    const response = await makeRemoteRequest(
      `/v1/organizations/${orgId}/members`
    );
    const result = await handleRemoteResponse<ListMembersResponse>(response);

View on GitHub (pinned to 4deb7eca8f)

Solutions

  1. Inspect e.status on the thrown ApiError and branch (401 → re-auth, 403 → permissions, 404 → check the resource ID).
  2. Read the parsed body (e.response) for the server's `error`/`message` field to learn the exact rejection reason.
  3. Retry transient 5xx with backoff; do not retry 4xx.
  4. Confirm the remote server version matches the client API version (shared/remote-types).

Example fix

// before
const members = await organizationsApi.getMembers(orgId);
// after
try {
  const members = await organizationsApi.getMembers(orgId);
} catch (e) {
  if (e instanceof ApiError) {
    if (e.status === 401) return reauth();
    console.error('Remote API error', e.status, e.message, e.response);
  }
  throw e;
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure a session exists before remote calls
const s = await oauthApi.status();
if (!s.authenticated) await login();

Type guard

function isApiError(e: unknown): e is ApiError {
  return e instanceof ApiError;
}

Try / catch

try {
  const data = await someRemoteApi.call();
} catch (e) {
  if (isApiError(e)) {
    if (e.status === 401) return reauth();
    if (e.status >= 500) return retryWithBackoff();
    showUserError(e.message);
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: Any makeRemoteRequest call (organizations, members, etc.) that receives a 4xx/5xx response: invalid org ID (404), insufficient permissions (403), validation rejection (422), server crash (500), or a non-JSON body (message falls back to statusText).

Common situations: Using a stale org ID after it was deleted; calling a remote endpoint with an expired token; backend deployed with a breaking API change returning unexpected error shape; network proxy returning HTML error pages (empty error message).

Related errors


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