{"record":{"id":"9b1f10d8b25877d8","repo":"paperclipai/paperclip","slug":"errorbody-as-error-string-null-error","errorCode":null,"errorMessage":"${(errorBody as { error?: string } | null)?.error ?? `Request failed: ${res.status}`}","messagePattern":"(.+?) \\| null\\)\\?\\.error \\?\\? `Request failed: (.+?)`\\}","errorType":"exception","errorClass":"ApiError","httpStatus":null,"severity":"error","filePath":"ui/src/api/client.ts","lineNumber":62,"sourceCode":"\nasync function request<T>(path: string, init?: RequestInit): Promise<T> {\n  const headers = new Headers(init?.headers ?? undefined);\n  const body = init?.body;\n  if (!(body instanceof FormData) && !headers.has(\"Content-Type\")) {\n    headers.set(\"Content-Type\", \"application/json\");\n  }\n  applyObservabilityHeaders(headers);\n\n  const res = await fetch(`${BASE}${path}`, {\n    headers,\n    credentials: \"include\",\n    ...init,\n  });\n  if (!res.ok) {\n    const errorBody = await res.json().catch(() => null);\n    const recovery = tenantSessionRecovery.recoverIfNeeded(res.status, errorBody);\n    if (recovery) return recovery;\n    throw new ApiError(\n      (errorBody as { error?: string } | null)?.error ?? `Request failed: ${res.status}`,\n      res.status,\n      errorBody,\n    );\n  }\n  if (res.status === 204) return undefined as T;\n  return res.json();\n}\n\n// --- In-tab request coalescing for identical safe GETs -----------------------\n//\n// Multiple callers issuing the same GET while one is in flight share a single\n// underlying fetch. Each caller keeps its own abort semantics: aborting one\n// caller only cancels the shared fetch when *every* caller has aborted.\n// Mutations are never coalesced.\n\ninterface InflightGet {\n  promise: Promise<unknown>;","sourceCodeStart":44,"sourceCodeEnd":80,"githubUrl":"https://github.com/paperclipai/paperclip/blob/01ad8584922b5d85292b1723cae71fa0d9b07a19/ui/src/api/client.ts#L44-L80","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nthrow new ApiError((errorBody as { error?: string } | null)?.error ?? `Request failed: ${res.status}`, res.status, errorBody);\n// after\ntry { return await api.get(url); } catch (e) { if (e instanceof ApiError && e.status === 422) showToast(e.message); else throw e; }","handlingStrategy":"try-catch","validationCode":"if (!res.ok) console.warn('API call will fail with', res.status);","typeGuard":"function isApiError(e: unknown): e is ApiError { return e instanceof ApiError; }","tryCatchPattern":"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; }","preventionTips":["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"],"tags":["http","api","fetch"],"backgroundTag":"api-error-response","analyzedSha":"01ad8584922b5d85292b1723cae71fa0d9b07a19","analyzedAt":"2026-09-10T03:14:50.855Z","contentChangedAt":"2026-09-10T03:14:50.855Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}