109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
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 "Произошла неизвестная ошибка.";
|
||
}
|
||
}
|