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
- Read ApiError.status and body to identify the real cause
- If 401/403, check auth state and company scoping of the request
- Confirm the endpoint exists and the payload matches the shared zod validator
- 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
- Always catch ApiError specifically, not generic Error
- Branch on e.status for 401/403/404/422 handling
- Validate request payloads against shared zod schemas before sending
- Ensure server routes emit `{ error: string }` JSON for all failures
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
- OpenCode API ${path} request failed: ${redact(String(error),
- OpenCode API ${path} returned HTTP ${response.status}: ${red
- Discord ${operation} failed (HTTP ${response.status}${code ?
- ${(payload as { error?: string } | null)?.error ?? `Failed t
- ${payload?.error ?? `Failed to load health (${res.status})`}
AI-assisted analysis of paperclipai/paperclip@01ad858492 (2026-09-10).
Data as JSON: /api/errors/9b1f10d8b25877d8.
Report an issue: GitHub.