Логирование: классификация доменных ошибок (500→409/400) + конвенции
Штатные конфликты и промахи ввода возвращались голым fmt.Errorf, поэтому
classifyErr отправлял их в 500 «внутренняя ошибка» вместо 409/400 (и logCmd
писал ERROR вместо DEBUG). Продолжение f8fb4fa (Tier A), по итогам ревью Fable.
Классификация ошибок:
- новый sentinel worker.ErrInvalidInput → 400 для валидации ввода команд
(refine/set type/ignore/add source/set provider/choose candidate);
- обёртки %w ErrConflict в Cancel/Retry/Defer/Undo (штатный конфликт состояния);
- classifyErr: ErrInvalidInput→400, layout.ErrCollision→409 (коллизия цели
штатно уводит в review); ветка ErrCollision в tgbot (сообщение + refreshCard);
- logCmd относит ErrInvalidInput и ErrCollision в DEBUG «command rejected».
Конвенции (docs/conventions):
- logging.md: публичные команды воркера = доменная граница (лог один раз,
logCmd); таблица уровней доменных отказов (граница команды vs асинхронная
стадия); правило про *url.Error/секреты в URL; канон категории
state transition; уровень повторяющихся сбоев фоновых циклов;
- errors.md: таблица маппинга ошибка→статус; развилка «транзиентный ответ vs
персистентная диагностика» решена как (а) — error_msg/reasons на review-экране
и tg-карточке = операторская поверхность владельца (сырой текст ок, секреты
запрещены; аудит подтвердил, что секреты туда не текут).
Унификация категории лога state transition: cancel/retry/relink/recovery
переведены с семантических msg на общий state transition (from/to) — весь
жизненный цикл собирается одним jq-фильтром.
Мелочи: reason-коды linkPlan в const-блок; httpapi лог-поля id→download_id и
msg «… failed»; комментарий «почему» у parseIgnored; preview build failure в
ReviewData DEBUG→WARN.
Беклог: задача сведена к остатку (ext.* ERROR-шторм при недоступном qBittorrent
+ эскалация устойчивого сбоя тика), понижена в приоритете.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -68,14 +68,45 @@ jellybit — **приложение, а не библиотека**: внешн
|
||||
к загрузке) либо `request_id`, чтобы по нему найти полную ошибку в логах.
|
||||
Пример: «При обработке загрузки произошла ошибка, download_id=12345», а
|
||||
не «произошла ошибка» и не сырой текст;
|
||||
- **маппинг доменной ошибки → статус/сообщение**: `ErrNotFound` → 404
|
||||
«не найдено», валидация/`ErrNotMagnet` → 400 «некорректный источник»,
|
||||
конфликт состояния (`ErrConflict` — операция недопустима в текущем
|
||||
состоянии) → 409 «действие недоступно в текущем состоянии», прочее →
|
||||
500 «внутренняя ошибка».
|
||||
- **маппинг доменной ошибки → статус/сообщение** (в jellybit —
|
||||
`httpapi.classifyErr`, единая точка для REST и веб-UI):
|
||||
|
||||
Граница публичная по умолчанию. Истинно приватный для владельца канал —
|
||||
логи; отдельной «операторской» поверхности с сырыми ошибками не заводим.
|
||||
| Доменная ошибка | Статус | Сообщение |
|
||||
|---|---|---|
|
||||
| `store.ErrNotFound` | 404 | «не найдено» |
|
||||
| `magnet.ErrNotMagnet` / `torrent.ErrNotTorrent` | 400 | «некорректный источник» |
|
||||
| `worker.ErrInvalidInput` (промах ввода команды) | 400 | «некорректный ввод» |
|
||||
| `worker.ErrNotReady` (источник ещё качается) | 409 | «торрент ещё качается…» |
|
||||
| `layout.ErrCollision` (цель занята, ушло в review) | 409 | «целевой файл уже существует…» |
|
||||
| `worker.ErrConflict` (операция недопустима сейчас) | 409 | «действие недоступно в текущем состоянии» |
|
||||
| прочее | 500 | «внутренняя ошибка» |
|
||||
|
||||
Новую штатную ветвь отказа (конфликт/валидация) заводим sentinel’ом и
|
||||
добавляем сюда — иначе `default` отдаст 500 «внутренняя ошибка» на
|
||||
нормальный конфликт (и логирующая граница спишет его в `ERROR` вместо
|
||||
`DEBUG`, см. [logging.md](logging.md)).
|
||||
|
||||
### Транзиентный ответ vs персистентная диагностика
|
||||
|
||||
У публичной границы две разные поверхности, и правило сырого текста для них
|
||||
разное:
|
||||
|
||||
- **Транзиентный ответ на действие** (тело REST/`?err=`/answer бота по
|
||||
результату команды) — строго нейтральный: маппинг выше, `err.Error()` наружу
|
||||
не идёт, полная ошибка — в логах по `download_id`/`request_id`.
|
||||
- **Персистентная диагностика состояния** — `error_msg` перехода (причина ухода
|
||||
в review/failed: коллизия, рассинхрон, сбой ФС) и `reasons` распознавания,
|
||||
сохранённые в БД и показываемые на экране ревью и в Telegram-карточке. Это
|
||||
**операторская поверхность владельца**: сервис однопользовательский в
|
||||
доверенной LAN (см. [architecture.md](../specs/architecture.md)), эти поля —
|
||||
диагностический контекст для того, кто разбирает задачу. Здесь сырой текст
|
||||
ошибки (пути, фрагмент ответа LLM/qBittorrent) **допустим и полезен** — но:
|
||||
- **секреты запрещены** абсолютно (токены/ключи/пароли/`Authorization`) — так
|
||||
же, как в логах ([logging.md](logging.md), «Безопасность»). Источник
|
||||
error_msg вычищаем на границе клиента (`logging.SanitizeErr` для ошибок
|
||||
транспорта, несущих URL с секретом);
|
||||
- это **не** канал для транзиентных отказов команд — те остаются нейтральными
|
||||
(см. выше).
|
||||
|
||||
## panic
|
||||
|
||||
|
||||
Reference in New Issue
Block a user