export type ApiErrorCode = | "validation_error" | "not_found" | "database_unavailable" | "conflict" | "network_error" | "unknown_error"; export interface ApiErrorPayload { error?: string; details?: string[]; } export class ApiError extends Error { public readonly code: ApiErrorCode; public readonly status: number | null; public readonly details: string[]; constructor(params: { code: ApiErrorCode; message: string; status?: number | null; details?: string[]; }) { super(params.message); this.name = "ApiError"; this.code = params.code; this.status = params.status ?? null; this.details = params.details ?? []; } } function normalizeApiCode(value: string | undefined): ApiErrorCode { if ( value === "validation_error" || value === "not_found" || value === "database_unavailable" || value === "conflict" || value === "unknown_error" ) { return value; } return "unknown_error"; } function isGatewayStatus(status: number): boolean { return status === 502 || status === 503 || status === 504; } export function isStructuredApiErrorPayload(payload: unknown): payload is ApiErrorPayload { if (payload === null || typeof payload !== "object" || Array.isArray(payload)) { return false; } return typeof (payload as ApiErrorPayload).error === "string"; } export function toApiError(status: number, payload: unknown): ApiError { if (isGatewayStatus(status) && !isStructuredApiErrorPayload(payload)) { return new ApiError({ code: "network_error", status, message: "Сервер временно недоступен. Попробуйте обновить страницу.", }); } if (!isStructuredApiErrorPayload(payload) && (status === 401 || status === 403 || status === 404)) { return new ApiError({ code: "network_error", status, message: status === 404 ? "API не найден по этому адресу. Проверьте прокси и префикс /api." : "Запрос отклонён сервером. Проверьте переменную CORS_ORIGIN на бэкенде.", }); } const maybePayload = payload as ApiErrorPayload; const code = normalizeApiCode(maybePayload?.error); const details = Array.isArray(maybePayload?.details) ? maybePayload.details.filter((item): item is string => typeof item === "string") : []; return new ApiError({ code, status, message: getApiErrorMessage(code), details, }); } export function getApiErrorMessage(code: ApiErrorCode): string { switch (code) { case "validation_error": return "Проверьте введённые данные и попробуйте снова."; case "not_found": return "Запись не найдена."; case "database_unavailable": return "Сервис временно недоступен. Попробуйте позже."; case "conflict": return "Запись с таким идентификатором уже существует."; case "network_error": return "Не удалось связаться с сервером."; case "unknown_error": return "Сервер не смог обработать запрос. Попробуйте позже или обновите страницу."; default: return "Произошла неизвестная ошибка."; } }