- выбор → поимённое подтверждение → отчёт: пачка до 20 загрузок, гарды входа на обеих границах, потолок времени и остановка после трёх подряд отказов внешнего сервиса - допуск полного удаления сведён в единую точку store.State.CanDelete() — worker, страница загрузки и Telegram больше не держат своих перечней
12 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», а не «произошла ошибка» и не сырой текст. Ключ есть не у всякого транспорта, и это называется вслух.request_id— понятие HTTP-границы (chiRequestID); у Telegram и CLI его нет. Если операция ещё не завела загрузку (отказ приёма), у такого транспорта ключа нет вовсе — тогда сообщение остаётся без якоря, а диагностика ищется по записи доменной границы (capability,infohash). Заводить транспорту собственный идентификатор запроса ради ключа — решение уровня спеки, а не умолчание: второй канал корреляции рядом с существующим дороже, чем отсутствие ключа; -
маппинг доменной ошибки → статус/сообщение (в jellybit —
httpapi.classifyErr, единая точка для REST и веб-UI):Доменная ошибка Статус Сообщение store.ErrNotFound404 «не найдено» magnet.ErrNotMagnet/torrent.ErrNotTorrent400 «некорректный источник» ingest.ErrTorrentTooLarge(файл больше лимита)400 «файл .torrent слишком большой» worker.ErrInvalidInput(промах ввода команды)400 «некорректный ввод» errManualSource(ручной ввод источника, локальный sentinelhttpapi)400 текст самой ошибки errInvalidCandidate(выбран несуществующий кандидат, локальный sentinelhttpapi)400 текст самой ошибки errBatchEmpty/errBatchTooLarge/errBatchBadID(разбор пачки группового удаления, локальные sentinel'ыhttpapi)400 текст самой ошибки worker.ErrNotReady(источник ещё качается)409 «торрент ещё качается…» layout.ErrCollision(цель занята, ушло в review)409 «целевой файл уже существует…» layout.ErrNameTooLong(целевое имя не помещается, ушло в 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-карточке. Это операторская поверхность владельца: сервис однопользовательский в доверенном контуре (см. security.md → «Периметр»), эти поля — диагностический контекст для того, кто разбирает задачу. Здесь сырой текст ошибки (пути, фрагмент ответа LLM/qBittorrent) допустим и полезен — но:- секреты запрещены абсолютно (токены/ключи/пароли/
Authorization) — так же, как в логах (logging.md, «Безопасность»). Источник error_msg вычищаем на границе клиента (logging.SanitizeErrдля ошибок транспорта, несущих URL с секретом); - это не канал для транзиентных отказов команд — те остаются нейтральными (см. выше);
- внешнее значение в тексте усекается на границе, а его размер называется
числом.
error_msgуезжает в баннер ревью, в панель действий и в карточку Telegram; имя файла на 400 байт занимает там экран целиком и оседает в БД навсегда. Усечение — серединой и по рунам (layout.shorten,naming.truncate,tgbot.shorten), точная величина остаётся числом рядом: без неё человек не поймёт, насколько сокращать.
- секреты запрещены абсолютно (токены/ключи/пароли/
panic
panic— только для невосстановимого: баг программиста (нарушенный инвариант), ошибка инициализации, из которой нельзя стартовать.- Не для управления потоком и не для ожидаемых ошибок (нет сети, плохой
ввод) — это значения
error. recover— на верхней границе обработчика (HTTP middleware), чтобы один паникующий запрос не ронял процесс.
Несколько ошибок
- Сбор независимых ошибок (напр. валидация конфига — все проблемы разом) —
errors.Join; проверка собранного по-прежнему черезerrors.Is.