paperclipai/paperclip · error · ApiError

${(errorBody as { error?: string } | null)?.error ?? `Reques

Error message

${(errorBody as { error?: string } | null)?.error ?? `Request failed: ${res.status}`}

What it means

The shared API client's `request` wraps all fetch calls and throws ApiError when the response is not ok. The message prefers the server's `error` field and falls back to `Request failed: <status>`. Tenant session recovery is attempted first and, if it can recover, returns a recovery value instead of throwing.

Source

Thrown at ui/src/api/client.ts:62

async function request<T>(path: string, init?: RequestInit): Promise<T> {
  const headers = new Headers(init?.headers ?? undefined);
  const body = init?.body;
  if (!(body instanceof FormData) && !headers.has("Content-Type")) {
    headers.set("Content-Type", "application/json");
  }
  applyObservabilityHeaders(headers);

  const res = await fetch(`${BASE}${path}`, {
    headers,
    credentials: "include",
    ...init,
  });
  if (!res.ok) {
    const errorBody = await res.json().catch(() => null);
    const recovery = tenantSessionRecovery.recoverIfNeeded(res.status, errorBody);
    if (recovery) return recovery;
    throw new ApiError(
      (errorBody as { error?: string } | null)?.error ?? `Request failed: ${res.status}`,
      res.status,
      errorBody,
    );
  }
  if (res.status === 204) return undefined as T;
  return res.json();
}

// --- In-tab request coalescing for identical safe GETs -----------------------
//
// Multiple callers issuing the same GET while one is in flight share a single
// underlying fetch. Each caller keeps its own abort semantics: aborting one
// caller only cancels the shared fetch when *every* caller has aborted.
// Mutations are never coalesced.

interface InflightGet {
  promise: Promise<unknown>;

View on GitHub (pinned to 01ad858492)

Solutions

  1. Read ApiError.status and body to identify the real cause
  2. If 401/403, check auth state and company scoping of the request
  3. Confirm the endpoint exists and the payload matches the shared zod validator
  4. Ensure the server returns a JSON body with `error` for non-2xx responses

Example fix

// before
throw new ApiError((errorBody as { error?: string } | null)?.error ?? `Request failed: ${res.status}`, res.status, errorBody);
// after
try { return await api.get(url); } catch (e) { if (e instanceof ApiError && e.status === 422) showToast(e.message); else throw e; }
Defensive patterns

Strategy: try-catch

Validate before calling

if (!res.ok) console.warn('API call will fail with', res.status);

Type guard

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

Try / catch

try { return await request<T>(url, init); } catch (e) { if (isApiError(e)) { if (e.status === 401) return recoverSession(); if (e.status >= 500) return retryWithBackoff(() => request<T>(url, init)); } throw e; }

Prevention

When it happens

Trigger: Any API call through ui/src/api/client.ts that receives 4xx/5xx; non-JSON error body (null fallback); server error envelope without `error` key.

Common situations: Validation 400/422 from a mutated payload, 403 permission denial for the wrong company scope, 404 after an entity was deleted, gateway 502 during deploys.

Related errors


AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10). Data as JSON: /api/errors/9b1f10d8b25877d8. Report an issue: GitHub.