Перейти к основному содержимому

Обработка ошибок

Backend MineFlow отдаёт ошибки в формате RFC 7807 Problem Details. SDK нормализует их в единый класс MineflowApiError, у которого есть стабильный машинный code. Этот рецепт показывает, как гейтить UI по коду (а не по тексту), разбирать ошибки валидации 422 и не словить unknown в catch.

Главное правило

Ветви UI завязывай на error.code, а не на error.message. message — это локализованный title из Problem Details: он меняется и переводится. code — стабильный контракт между бэком и фронтом (каталог кодов — в доменных *.errors.ts бэкенда, ADR-0042 anti-drift).

Откуда берутся ошибки

unwrap() принимает результат openapi-fetch ({ data, error, response }) и либо возвращает данные, либо бросает MineflowApiError. Это основной способ получить ошибку в коде:

import { createMineflowClient, unwrap, MineflowApiError } from '@mineflow/client-core';

const client = createMineflowClient({
baseUrl: 'https://api.mineflow.local',
tokenProvider: { getToken: () => keycloak.token ?? null },
generateId: () => crypto.randomUUID(),
});

try {
const asset = unwrap(
await client.POST('/api/v1/eam/assets/{id}/decommission', {
params: { path: { id } },
}),
);
// asset — типизированные данные
} catch (e) {
if (e instanceof MineflowApiError) {
console.warn(e.code, e.status); // 'ASSET_INVALID_STATUS_TRANSITION', 409
}
}

В React всё то же самое прилетает в error от мутаций/запросов @tanstack/react-query — провайдер строит клиент сам, см. @mineflow/client-react. Природа ошибки одинаковая: MineflowApiError.

Анатомия MineflowApiError

ПолеТипНазначение
statusnumberHTTP-статус (409, 422, 403, 500…)
codestring | undefinedСтабильный машинный код домена, напр. ASSET_INVALID_STATUS_TRANSITION
messagestringЧеловекочитаемый title (локализован — не для логики!)
detailunknownДоп. данные; для 422 — { errors: [...] }
problemProblemDetailsПолный RFC 7807 объект, как пришёл с бэка

Метод is(code) — сахар для сравнения кода:

e.is('ASSET_INVALID_STATUS_TRANSITION'); // эквивалент e.code === '...'
instanceof работает всегда

MineflowApiError восстанавливает прототип после super(), поэтому e instanceof MineflowApiError корректен даже при компиляции в ES5/ES2015. Можно смело ветвиться через instanceof.

Полная сигнатура — в API-референсе: /api/client-core/.

Гейтинг UI по code

Сопоставляй коды с действиями/сообщениями явно. Не парси message:

import { MineflowApiError } from '@mineflow/client-core';

function describeError(e: unknown): string {
if (!(e instanceof MineflowApiError)) {
return 'Неизвестная ошибка';
}
switch (e.code) {
case 'ASSET_INVALID_STATUS_TRANSITION':
return 'Актив в этом статусе нельзя списать';
case 'ASSET_NOT_FOUND':
return 'Актив не найден';
default:
// запасной вариант — по статусу, не по тексту
return e.status >= 500 ? 'Ошибка сервера, попробуйте позже' : e.message;
}
}
Не делай так
// ❌ хрупко: текст локализуется и переписывается
if (e.message.includes('cannot transition')) { /* ... */ }

title свободно меняется на бэке и зависит от языка. Любая такая ветка молча сломается. Используй e.code или e.is(code).

422: ошибки валидации полей

Для ошибок Zod-валидации (422) бэк кладёт детали в detail.errors. Это позволяет привязать ошибки к конкретным полям формы:

import { MineflowApiError } from '@mineflow/client-core';

interface FieldError {
path: (string | number)[];
message: string;
}

function fieldErrors(e: unknown): FieldError[] {
if (
e instanceof MineflowApiError &&
e.status === 422 &&
e.detail &&
typeof e.detail === 'object' &&
Array.isArray((e.detail as { errors?: unknown }).errors)
) {
return (e.detail as { errors: FieldError[] }).errors;
}
return [];
}
к сведению
detail — это unknown

detail намеренно типизирован как unknown (формат зависит от статуса/домена). Проверяй форму перед чтением, как в примере выше. Структуру { errors: [...] } бэк гарантирует только для 422-валидации.

В формах с react-hook-form маппинг ошибок сервера на поля удобнее держать рядом с формой — см. рецепт Формы и валидация.

Нормализация «сырых» ошибок

Если ты делаешь запрос в обход unwrap() (свой fetch, не-openapi-fetch слой), приведи ответ к MineflowApiError через normalizeError(status, body, url?):

import { normalizeError } from '@mineflow/client-core';

const res = await fetch(url, init);
if (!res.ok) {
const body = await res.json().catch(() => null);
throw normalizeError(res.status, body, url);
}

normalizeError устойчив к разным формам тела:

  • полноценный RFC 7807 ({ status, title, code, detail, ... }) → разбирается как есть, code сохраняется;
  • голое { message: '...' }message становится title, code остаётся undefined;
  • непрозрачное тело (строка, null) → message = HTTP <status>.

То есть e.code может быть undefined — всегда предусматривай ветку «нет кода» (как default в примере выше).

Полная картина

import {
unwrap,
normalizeError,
MineflowApiError,
type ProblemDetails,
} from '@mineflow/client-core';
  • unwrap(result) — извлечь данные или бросить MineflowApiError (основной путь).
  • normalizeError(status, body, url?) — собрать MineflowApiError из сырого ответа.
  • MineflowApiError — класс с status / code / detail / problem / message / is(code).
  • ProblemDetails — тип тела RFC 7807.

Эти символы экспортируются из @mineflow/client-core. Полный справочник по REST-эндпоинтам и форматам ответов — REST-референс.

Связанные рецепты