forked from admin/runners-calendar
137 lines
4.5 KiB
TypeScript
137 lines
4.5 KiB
TypeScript
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 "Произошла неизвестная ошибка.";
|
||
}
|
||
}
|