/** * Parse an Entropy Softworks server error response into a single user- * facing string. Falls back to `fallback` when the body isn't valid * JSON or doesn't follow the expected `{ error: { message, details? } }` * shape. Detail arrays longer than `MAX_DETAILS` are truncated so a * runaway server response can't produce an unreadable toast. */ export interface ServerErrorBody { error: { message: string; details?: string[]; code?: string; }; } const MAX_DETAILS = 5; /** * Error subclass that carries the server's machine-readable * `error.code` and HTTP `status` alongside the human-readable * message. UI code can branch on `code` (e.g. `INVALID_CREDENTIALS`, * `ACCOUNT_LOCKED`, `EMAIL_NOT_VERIFIED`) instead of substring- * matching against an English message that breaks under i18n. */ export class ApiError extends Error { readonly code: string | undefined; readonly status: number; readonly details: readonly string[] | undefined; constructor( message: string, options: { code?: string; status: number; details?: readonly string[] } ) { super(message); this.name = "ApiError"; this.code = options.code; this.status = options.status; this.details = options.details; } } export async function extractErrorMessage(response: Response, fallback: string): Promise { try { const error: ServerErrorBody = await response.json(); const details = error.error.details; if (details && details.length > 0) { const shown = details.slice(0, MAX_DETAILS).join(", "); return details.length > MAX_DETAILS ? `${shown}, …` : shown; } return error.error.message || fallback; } catch { return fallback; } } /** * Like `extractErrorMessage` but returns a structured `ApiError` so * callers can branch on `code` / `status`. Use this when the caller * is throwing and downstream consumers need machine-readable * classification; use `extractErrorMessage` when all you need is a * toast string. */ export async function extractApiError(response: Response, fallback: string): Promise { let message = fallback; let code: string | undefined; let details: string[] | undefined; try { const error: ServerErrorBody = await response.json(); if (error.error.details && error.error.details.length > 0) { details = error.error.details.slice(0, MAX_DETAILS); const shown = details.join(", "); message = error.error.details.length > MAX_DETAILS ? `${shown}, …` : shown; } else { message = error.error.message || fallback; } code = error.error.code; } catch { // Non-JSON body — keep fallback message, no code. } return new ApiError(message, { code, status: response.status, details }); }