- Раскладка docs/ приведена к канону 2: заведены passport/architecture/ database/security/review и research; docs/specs, drafts, backlog, review/ и BRIEF.md разобраны и удалены, беклог переехал в docs/tasks (34 задачи, 6 целей, слаги на английский). - Нарративы specs удалены как дубли openspec-спек после поимённой сверки; остаток заведён задачами (редактор маппинга ревью, крайние случаи именования), отказ от сущности title промоутнут в ADR. - Проектные копии агентов и скиллов ревью/пайплайна удалены в пользу плагинов av-dev-pm и av-dev-pipeline; в task gate добавлен шаг canon вместо er-schema.
9.2 KiB
Ошибки
Конвенция: как устроены и передаются ошибки в jellybit. Правила оформления кода (How). Где и когда ошибку логировать — в logging.md, раздел «Ошибки» (коротко: лог один раз на доменной границе). Здесь — как ошибки строятся, оборачиваются и проверяются.
Механизировано: сторонние пакеты ошибок — depguard; err == ErrX и
приведение типа — errorlint; матчинг по тексту сообщения — internal/archrules.
Базовая идиома: stdlib
- Только стандартный
errors+fmt.Errorf: контекст ошибки несётslog, а не стек — стек-трейсы и Sentry избыточны для домашнего сервиса. - Если отладка начнёт упираться в «где именно родилась ошибка» — это сигнал пересмотреть, а не дефолт.
Обёртка и контекст
jellybit — приложение, а не библиотека: внешнего Go-API нет, весь код
наш. Поэтому внутри приложения обёртка %w — дефолт, чтобы errors.Is/
errors.As работали сквозь слои.
- Добавляем контекст обёрткой:
fmt.Errorf("parse magnet: %w", err). %w— когда вызывающий может инспектировать причину (наш обычный случай).%v— когда причину сознательно не раскрываем (не хотим завязывать вызывающего на чужой тип ошибки).- От утечки внутренних ошибок наружу защищаемся не через
%vв цепочке, а трансляцией на внешней границе (см. ниже).
Стиль сообщения:
- со строчной, без точки в конце, без «failed to»/«error» — обёртка и так читается как «контекст: причина»;
- контекст — операция/субъект:
"link target: %w", не"something failed"; - без заикания: каждый слой добавляет свой смысл, не повторяет нижний
(
"add to qbt: %w", а не"add download failed: add to qbt failed: …").
Проверка ошибок
- Граничные ошибки зависимостей транслируем в доменные у источника:
sql.ErrNoRows→ доменныйstore.ErrNotFoundв слое store, чтобы выше по коду не торчалdatabase/sql.
Sentinel vs типизированные
- Sentinel (
var ErrNotFound = errors.New("not found")) — для условий, на которые ветвится код (нет записи, дубликат по infohash, неподдерживаемый источник). Проверяемerrors.Is. - Типизированная ошибка (тип с полями + метод
Error()) — когда вызывающему нужны данные ошибки (поле валидации, код). Достаёмerrors.As. Не плодим типы там, где хватает sentinel.
Граница и трансляция: приватный vs публичный канал
Внутри — богатые обёрнутые ошибки. На внешней границе ошибку транслируем, и форма зависит от канала, кто его видит:
- Приватный канал — логи (владелец сервиса). Полная ошибка со всей
цепочкой
%wи контекстом. Пишется один раз на доменной границе — см. logging.md. - Публичный канал — пользовательские поверхности (Telegram, web-UI, HTTP
API; ими пользуется не только владелец). Сюда отдаём:
-
человекочитаемое сообщение по доменной ошибке — не сырой
err.Error()и не детали реализации (database/sql, пути, стек); -
+ корреляционный ключ для владельца —
download_id(если операция к загрузке) либоrequest_id, чтобы по нему найти полную ошибку в логах. Пример: «При обработке загрузки произошла ошибка, download_id=12345», а не «произошла ошибка» и не сырой текст; -
маппинг доменной ошибки → статус/сообщение (в jellybit —
httpapi.classifyErr, единая точка для REST и веб-UI):Доменная ошибка Статус Сообщение store.ErrNotFound404 «не найдено» magnet.ErrNotMagnet/torrent.ErrNotTorrent400 «некорректный источник» worker.ErrInvalidInput(промах ввода команды)400 «некорректный ввод» worker.ErrNotReady(источник ещё качается)409 «торрент ещё качается…» layout.ErrCollision(цель занята, ушло в review)409 «целевой файл уже существует…» worker.ErrConflict(операция недопустима сейчас)409 «действие недоступно в текущем состоянии» прочее 500 «внутренняя ошибка» Новую штатную ветвь отказа (конфликт/валидация) заводим sentinel’ом и добавляем сюда — иначе
defaultотдаст 500 «внутренняя ошибка» на нормальный конфликт (и логирующая граница спишет его вERRORвместоDEBUG, см. logging.md).
-
Транзиентный ответ vs персистентная диагностика
У публичной границы две разные поверхности, и правило сырого текста для них разное:
- Транзиентный ответ на действие (тело REST/
?err=/answer бота по результату команды) — строго нейтральный: маппинг выше,err.Error()наружу не идёт, полная ошибка — в логах поdownload_id/request_id. - Персистентная диагностика состояния —
error_msgперехода (причина ухода в review/failed: коллизия, рассинхрон, сбой ФС) иreasonsраспознавания, сохранённые в БД и показываемые на экране ревью и в Telegram-карточке. Это операторская поверхность владельца: сервис однопользовательский в доверенной LAN (см. architecture.md), эти поля — диагностический контекст для того, кто разбирает задачу. Здесь сырой текст ошибки (пути, фрагмент ответа LLM/qBittorrent) допустим и полезен — но:- секреты запрещены абсолютно (токены/ключи/пароли/
Authorization) — так же, как в логах (logging.md, «Безопасность»). Источник error_msg вычищаем на границе клиента (logging.SanitizeErrдля ошибок транспорта, несущих URL с секретом); - это не канал для транзиентных отказов команд — те остаются нейтральными (см. выше).
- секреты запрещены абсолютно (токены/ключи/пароли/
panic
panic— только для невосстановимого: баг программиста (нарушенный инвариант), ошибка инициализации, из которой нельзя стартовать.- Не для управления потоком и не для ожидаемых ошибок (нет сети, плохой
ввод) — это значения
error. recover— на верхней границе обработчика (HTTP middleware), чтобы один паникующий запрос не ронял процесс.
Несколько ошибок
- Сбор независимых ошибок (напр. валидация конфига — все проблемы разом) —
errors.Join; проверка собранного по-прежнему черезerrors.Is.