Files
runners-calendar/frontend/src/api/errors.ts
2026-07-12 16:57:43 +03:00

137 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export type ApiErrorCode =
| "validation_error"
| "not_found"
| "database_unavailable"
| "conflict"
| "username_taken"
| "unauthorized"
| "email_not_verified"
| "csrf_error"
| "captcha_failed"
| "invalid_credentials"
| "invalid_token"
| "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 === "username_taken" ||
value === "unauthorized" ||
value === "email_not_verified" ||
value === "csrf_error" ||
value === "captcha_failed" ||
value === "invalid_credentials" ||
value === "invalid_token" ||
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 "username_taken":
return "Это имя пользователя уже занято.";
case "unauthorized":
return "Нужно войти в аккаунт.";
case "email_not_verified":
return "Подтвердите email, чтобы продолжить.";
case "csrf_error":
return "Сессия устарела. Обновите страницу и попробуйте снова.";
case "captcha_failed":
return "Проверка капчи не пройдена.";
case "invalid_credentials":
return "Неверный email или пароль.";
case "invalid_token":
return "Ссылка недействительна или устарела.";
case "network_error":
return "Не удалось связаться с сервером.";
case "unknown_error":
return "Сервер не смог обработать запрос. Попробуйте позже или обновите страницу.";
default:
return "Произошла неизвестная ошибка.";
}
}