# API error catalog Каталог HTTP-кодов и тел ответов, которые возвращает `food-market.api`. Используется фронтом для `humanizeError(response)` и QA для regression проверки. Если поле `error` есть — это user-facing сообщение; `errors` (множественное) — структурированные ошибки валидации (ASP.NET ValidationProblemDetails). ## Формат ```jsonc // Универсальный шаблон single-error: { "error": "Понятный текст для пользователя.", "field": "Optional" } // ValidationProblemDetails (FluentValidation / DataAnnotations): { "type": "...", "title": "One or more validation errors occurred.", "status": 400, "errors": { "Name": ["..."], "Prices[0].Amount": ["..."] } } // retryable flag (Sprint 23): { "error": "...", "retryable": true } ``` ## Коды ### 200/201/204 — OK / Created / NoContent Корректно. Тело — DTO или пусто. ### 400 — Bad Request | Когда | Тело | Что показать | |---|---|---| | Validation от FluentValidation | `ValidationProblemDetails` с `errors.{field}: [msg]` | Подсветить поле + показать сообщение | | Business-rule (например, draft пустой) | `{error: "Нельзя провести пустой чек."}` | toast + не закрывать форму | | Сумма оплаты < total | `{error: "Сумма оплаты X меньше итога Y. Доплатите...", field: "PaidCash"}` | подсветить поле PaidCash | | Required price = 0 после rounding (Sprint 23 bug-004) | `{error: "Цена «X» обязательна и должна быть больше 0."}` | подсветить prices section | | NUL-byte в строке (Sprint 23 bug-001) | `errors.Name: ["Поле Name не должно содержать управляющих символов..."]` | подсветить поле | | Дубликат barcode при создании | `{error: "Штрихкод X уже используется товаром «Y»."}` | toast | | Дубликат артикула | `{error: "Артикул «X» уже занят в этой организации."}` | toast | | Невалидный CSV / 1С-import | `errors: [{row, error}]` | таблица с подсветкой строк | ### 401 — Unauthorized | Когда | Тело | Что показать | |---|---|---| | Нет токена / устаревший токен | пусто или OpenIddict-`{error: "missing_token"}` | редирект на `/login`, refresh с RT | | Garbage / tampered JWT | `{error: "missing_token"}` | logout + login | | Refresh-token недействителен | `{error: "invalid_grant", error_description: "..."}` | logout | ### 403 — Forbidden | Когда | Тело | Что показать | |---|---|---| | Нет permission на mutating action | пусто или ProblemDetails | toast: «Нет прав на это действие» | | Регулярный Admin лезет в `/hangfire` | пусто | redirect → 404 на фронте | | Cashier пытается удалить заявку | пусто | скрыть кнопку delete для Cashier | ### 404 — Not Found | Когда | Что показать | |---|---| | Document не найден (включая cross-tenant — нельзя раскрыть существование!) | «Запись не найдена. Возможно, удалена.» | | Endpoint не существует (типо в URL) | (фронту не должно встречаться) | ### 409 — Conflict | Когда | Тело | Что показать | |---|---|---| | DbUpdateConcurrencyException (xmin) | `{error: "Документ изменён в другом окне..."}` | toast + reload | | Чек уже проведён, повторный post | `{error: "Чек уже проведён."}` | toast | | Serialization failure 40001 (Sprint 23 bug-003) | `{error: "Конфликт параллельных операций. Попробуйте ещё раз.", retryable: true}` | **auto-retry один раз**, при повторе — toast | | Дубликат preset name | `{error: "Пресет с таким именем уже существует..."}` | подсветить input name | | In-flight org-export ≥3 | `{error: "Уже в очереди 3+ экспорта. Подождите..."}` | toast | | Удаление непустой группы товаров | `{error: "Нельзя удалить группу, содержащую товары/подгруппы."}` | toast | ### 413 — Payload Too Large | Когда | Что показать | |---|---| | Body > nginx limit (10 MB по default) | «Файл слишком большой. Лимит: 10 МБ.» | ### 429 — Too Many Requests | Когда | Тело | Что показать | |---|---|---| | Rate-limit на signup (3/h IP) | пусто или `Retry-After` header | «Слишком много попыток. Попробуйте через час.» | | Rate-limit на forgot-password (3/h email + 10/h IP) | то же | то же | | Rate-limit на feedback (5/час) | то же | то же | | IP-limit (60/мин общий) | то же | «Слишком много запросов с вашего IP.» | ### 431 — Request Header Fields Too Large | Когда | Что показать | |---|---| | Слишком большие/много HTTP-headers | (нечем фиксить с UI; нечасто) | ### 500 — Internal Server Error После Sprint 23 — **очень редко**. Если встречается: - Все NUL-byte 500 → теперь 400 (bug-001). - Все serialization 40001 → теперь 409 (bug-003). - Все остальные uncaught exceptions → Serilog лог + `correlation-id` header. Что показать пользователю: «Произошла ошибка. Попробуйте ещё раз или сообщите администратору. Код: {x-correlation-id}». Этот correlation id находится в `x-correlation-id` response-header — записываем в audit. ### 501 — Not Implemented | Когда | Тело | Что показать | |---|---|---| | SSO callback flow (Sprint 20 scaffold) | `{status: "scaffolded", message, email, next}` | «SSO ещё не настроено полностью» | ### 503 — Service Unavailable | Когда | Тело | Что показать | |---|---|---| | SSO провайдер не сконфигурирован | `{error: "SSO для X не настроено.", hint: "..."}` | скрыть кнопку SSO | | (резерв на maintenance window) | пусто | «Сервис недоступен» | ## humanizeError на фронте `src/lib/api.ts → humanizeError(err)`: ```typescript export function humanizeError(err: AxiosError): string { const data = err.response?.data as any // 1. Single-error (наш стандарт) if (data?.error) return data.error // 2. ValidationProblemDetails if (data?.errors) { const first = Object.values(data.errors).flat()[0] return first ?? 'Ошибка валидации' } // 3. По статусу switch (err.response?.status) { case 401: return 'Сессия истекла. Войдите снова.' case 403: return 'Нет прав на это действие.' case 404: return 'Запись не найдена.' case 409: return 'Конфликт версий. Перезагрузите страницу.' case 413: return 'Файл слишком большой.' case 429: return 'Слишком много запросов. Подождите немного.' case 500: return `Ошибка сервера. Код: ${err.response.headers['x-correlation-id'] ?? 'unknown'}` case 503: return 'Сервис временно недоступен.' } return err.message ?? 'Неизвестная ошибка' } ``` ## Retry-policy | Код | Retry? | Условие | |---|---|---| | 401 | Один раз — после refresh-token | Если refresh тоже 401 → logout | | 409 c `retryable: true` | Один авто-retry с задержкой 500ms | Sprint 23 фикс — серверная сторона уже retry'ит до 5 раз, клиентский — дополнительный safety net | | 429 | Через `Retry-After` секунд (если есть) | Не более 3 попыток | | 500 | НЕТ авто-retry | Пользователь сам решает | | 503 | Через 5 секунд | До 2 попыток | Без auto-retry: 400, 403, 404, 413, 501.