Обработка ошибок
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
| Поле | Тип | Назначение |
|---|---|---|
status | number | HTTP-статус (409, 422, 403, 500…) |
code | string | undefined | Стабильный машинный код домена, напр. ASSET_INVALID_STATUS_TRANSITION |
message | string | Человекочитаемый title (локализован — не для логики!) |
detail | unknown | Доп. данные; для 422 — { errors: [...] } |
problem | ProblemDetails | Полный RFC 7807 объект, как пришёл с бэка |
Метод is(code) — сахар для сравнения кода:
e.is('ASSET_INVALID_STATUS_TRANSITION'); // эквивалент e.code === '...'
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 — это unknowndetail намеренно типизирован как 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-референс.
Связанные рецепты
- Саги (async-операции) — таймауты и провалы async-операций (
SagaTimeoutError, провал саги ≠ HTTP-ошибка). - Формы и валидация — маппинг 422 на поля формы.
- RBAC и доступ — обработка 403 и гейтинг по ролям.
- FSM-переходы — почему недопустимый переход даёт 409, а кнопку всё равно прячут на клиенте.