Files
av 42d5b73a04 docs: перевод документации на канон av-dev
- Раскладка 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.
2026-08-04 09:27:26 +03:00

9.2 KiB
Raw Permalink Blame History

Ошибки

Конвенция: как устроены и передаются ошибки в 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.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).

Транзиентный ответ 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.