Логирование: классификация доменных ошибок (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:
@@ -35,7 +35,6 @@ Tududi (проект `jellybit`) больше **не** держит беклог
|
||||
- [Обучение на правках человека (few-shot из прошлых ревью)](obuchenie-na-pravkah.md) — Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать…
|
||||
- [Confidence-гейт авто-раскладки: узаконить в спеке + сделать выключаемым (дефолт 0.7)](gate-confidence-spec-vs-code.md) — Решено (B): гейт оставляем как доп. проверку на ревью — выключаемый порог, дефолт 0.85→0.7, записать в спеку
|
||||
- [Внешние субтитры: пары VobSub и языковой суффикс](vneshnie-subtitry.md) — Привязка субтитр→серия уже работает; остались пары VobSub .idx+.sub и потеря Lang/Flags
|
||||
- [Классификация доменных ошибок (500→409/400) и конвенции логирования](oshibki-klassifikaciya-i-konvencii-logirovaniya.md) — Продолжение коммита f8fb4fa (Tier A сделан): sentinel-обёртки ErrConflict/валидации, classifyErr для ErrCollision, правки docs/conventions _(ревью Fable)_
|
||||
- [Defer из catched → лимбо → необратимый deleted (MAJOR-6)](review-major6-defer-catched.md) — Defer из ещё-не-добавленного catched уводит задачу в необратимый deleted _(ревью 2026-07-08)_
|
||||
- [processCatched: promote-without-add если торрент уже в qBittorrent (F2)](review-f2-promote-without-add.md) — торрент уже в qBittorrent → processCatched зациклен на Add вместо promote _(ревью 2026-07-08)_
|
||||
- [Cancel во время add оставляет неуправляемый торрент в qBittorrent (F3/NIT-13)](review-f3-cancel-during-add.md) — Cancel во время add оставляет неуправляемый торрент в qBittorrent _(ревью 2026-07-08)_
|
||||
@@ -44,6 +43,7 @@ Tududi (проект `jellybit`) больше **не** держит беклог
|
||||
|
||||
- [Панель действий ревью вне htmx-свопа блока источника](panel-review-vne-swap.md) — При выборе источника одним кликом обновляется только блок источника (#source-block)…
|
||||
- [Мгновенные обновления через SSE](sse-obnovleniya.md) — Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто и работает…
|
||||
- [Шум ERROR фоновых циклов при недоступной зависимости](oshibki-klassifikaciya-i-konvencii-logirovaniya.md) — Остаток задачи логирования: ext.* ERROR-шторм при недоступном qBittorrent + эскалация устойчивого сбоя тика _(ревью Fable)_
|
||||
- [Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p)](versii-kachestvo-repaki.md) — По калибровке болей (2026-07-02) — не боль, из приоритета выпало
|
||||
- [[идея] Многоступенчатая верификация привязки](mnogostupenchataya-verifikaciya.md) — ИДЕЯ (требует проработки)
|
||||
- [Выбор из нескольких находок метабазы в Telegram](telegram-vybor-nahodok.md) — Когда распознавание даёт несколько подходящих кандидатов в метабазе, предлагать их в…
|
||||
|
||||
@@ -1,88 +1,54 @@
|
||||
# Классификация доменных ошибок (500→409/400) и конвенции логирования
|
||||
# Шум ERROR фоновых циклов при недоступной зависимости
|
||||
|
||||
**Приоритет:** средний · **Теги:** review-fable, errors, logging
|
||||
**Приоритет:** низкий · **Теги:** review-fable, logging, reliability
|
||||
|
||||
Продолжение коммита `f8fb4fa` (логирование ошибок: доменная граница + защита
|
||||
секретов). Тот коммит закрыл «ядро + секреты» (Tier A) по итогам ревью двумя
|
||||
сабагентами Fable (инфраструктурные и доменные ошибки). Здесь — отложенные
|
||||
Tier B (классификация) и Tier C (конвенции + политика), не вошедшие в scope.
|
||||
Остаток от задачи «классификация доменных ошибок + конвенции логирования»
|
||||
(основное реализовано, см. ниже). Здесь — два смежных пункта про уровень
|
||||
повторяющихся сбоев фоновых циклов, каждый требует небольшого решения, а не
|
||||
только правки.
|
||||
|
||||
## Tier B — классификация ошибок (сейчас штатные конфликты/валидация → 500)
|
||||
## Что уже сделано (не переоткрывать)
|
||||
|
||||
Проблема: часть доменных отказов возвращается голым `fmt.Errorf` без sentinel,
|
||||
поэтому `classifyErr` (httpapi) отправляет их в `default` → **500 «внутренняя
|
||||
ошибка»** вместо 409/400. Побочно: новый `logCmd` (worker) логирует такие
|
||||
отказы как **ERROR**, хотя это норма (адресат — пользователь), должно быть
|
||||
DEBUG.
|
||||
Коммит `f8fb4fa` (Tier A) + коммит этой задачи закрыли:
|
||||
|
||||
- **Обернуть `ErrConflict`** в командах, где конфликт состояния не обёрнут (в
|
||||
отличие от `requireReviewable`/`Apply`/`Undo`/`Delete`, где уже сделано):
|
||||
- `worker.go:789` — `cancel: ... is already terminal`;
|
||||
- `worker.go:809` — `retry: ... only failed/stuck are retriable`;
|
||||
- `review.go:515` — `defer: ... is terminal`;
|
||||
- `review.go:551` — `undo: nothing to revert` (скорее конфликт).
|
||||
- **Sentinel для валидации ввода** (`ErrInvalidInput` → 400) или дооборачивание,
|
||||
чтобы промах пользователя не выглядел сбоем ни в статусе, ни в уровне лога:
|
||||
- `review.go` — `refine: empty hint`, `set type: invalid type`,
|
||||
`ignore: empty path`, `add source: invalid provider`/`empty id`,
|
||||
`set provider: invalid provider`/`empty id`,
|
||||
`choose candidate: candidate ... does not belong`.
|
||||
- **`classifyErr` не знает `layout.ErrCollision`**: ручной Apply с коллизией
|
||||
цели штатно уводит задачу в review с причиной (`review.go` linkPlan), но
|
||||
пользователь в вебе получает 500, а tgbot — «Не удалось выполнить действие».
|
||||
Добавить кейс (409 или сценарий «ушло в ревью: коллизия цели») и исключение
|
||||
в tgbot по аналогии с `ErrNotReady`.
|
||||
- **Классификация доменных ошибок:** sentinel `worker.ErrInvalidInput`→400;
|
||||
обёртки `ErrConflict` в Cancel/Retry/Defer/Undo; `layout.ErrCollision`→409 в
|
||||
`classifyErr` и ветка в tgbot; `logCmd` относит новые классы в DEBUG.
|
||||
- **Конвенции:** `logging.md` — команды воркера = доменная граница, таблица
|
||||
уровней доменных отказов (граница команды vs асинхронная стадия), правило про
|
||||
`*url.Error`/секреты в URL, канон категории `state transition` (унифицированы
|
||||
cancel/retry/relink/recovery). `errors.md` — таблица маппинга ошибка→статус,
|
||||
развилка «транзиентный ответ vs персистентная диагностика» решена как (а):
|
||||
`error_msg`/`reasons` — операторская поверхность владельца (сырой текст ок,
|
||||
секреты запрещены; аудит показал, что секреты туда не текут).
|
||||
- **Мелочи:** reason-коды const-блок; лог-поля `id`→`download_id`; preview
|
||||
WARN; комментарий у `parseIgnored`.
|
||||
|
||||
После sentinel-обёрток обновить `logCmd` (worker.go) — он уже относит
|
||||
`ErrConflict/ErrNotReady/ErrNotFound` в DEBUG; добавить туда новый
|
||||
`ErrInvalidInput`.
|
||||
## Остаток
|
||||
|
||||
## Tier C — конвенции и политика (docs/conventions)
|
||||
### ERROR-шторм при недоступном qBittorrent
|
||||
|
||||
Оба ревьюера предложили закрепить в `logging.md`/`errors.md` (чтобы дыры и
|
||||
дубли не возникали снова — уже дважды выстрелило: ingest, затем команды):
|
||||
Клиент `qbt` логирует `ext.*` `Failure` → **ERROR** на каждом тике поллинга
|
||||
(`torrents/info`, `internal/qbt/qbt.go`), пока qBittorrent недоступен (рестарт
|
||||
демона, сеть). Домен уже пишет `poll failed` = WARN (по новой конвенции), но
|
||||
транспортная `ext.*`-запись остаётся ERROR по правилу ext-конвенции («сервис
|
||||
недоступен → ERROR»). При частом поллинге это шумит.
|
||||
|
||||
- **Команды воркера = доменная граница.** Явно дописать в `logging.md`, раздел
|
||||
«Ошибки»: публичные методы воркера (`Apply`/`Refine`/`Cancel`/…) — граница
|
||||
домена, логируют исход ровно один раз (реализовано в `logCmd`); транспорты не
|
||||
логируют возвращённую ошибку. Сейчас формулировка «стадии воркера» двусмысленна.
|
||||
- **Таблица уровней доменных отказов:** `ErrConflict`/`ErrNotReady`/валидация →
|
||||
DEBUG (адресат — пользователь); нарушенный инвариант хранилища → INFO/WARN;
|
||||
прочее (БД/ФС/зависимости) → ERROR. Правило: у каждой доменной ошибки ровно
|
||||
один логирующий, уровень — по адресату, а не по месту.
|
||||
- **Правило про `*url.Error`/секреты в URL** (раздел «Безопасность»): ошибки
|
||||
HTTP-транспорта встраивают URL, который может нести секрет — санитизировать на
|
||||
границе клиента до лога и обёртки. Частично реализовано (`logging.SanitizeErr`,
|
||||
применён в `ExtCall`/tgbot/metadata) — осталось задокументировать + правило
|
||||
«секрет не кладём в URL, если у API есть заголовок».
|
||||
- **Уровень повторяющихся сбоев фоновых циклов:** `poll/sweep/list failed` =
|
||||
WARN, а та же ошибка БД в `ingest.Ingest` = ERROR. Договориться о едином
|
||||
правиле (транзиентный сбой тика → WARN; эскалация в ERROR при устойчивом сбое
|
||||
N тиков) — сейчас уровень зависит от места. Смежно: ERROR-шторм при
|
||||
недоступном qBittorrent (`qbt` логирует Failure на каждом тике поллинга).
|
||||
- **Единая категория переходов состояния:** свести к одному msg `state
|
||||
transition` (from/to/code); `download cancelled`/`download retried`/`relink
|
||||
re-recognizing` не теряются из-под `jq 'select(.msg=="state transition")'`.
|
||||
Развилка (решить до правки):
|
||||
|
||||
## Развилка — сырой `err.Error()` на публичных поверхностях
|
||||
- (а) Ввести у `logging.ExtCall` вариант с пониженным уровнем для рутинно-частых
|
||||
вызовов (симметрично `SuccessDebug`) — поллинг-вызовы (`torrents/info`) на
|
||||
транзиентном сбое пишут WARN, не ERROR;
|
||||
- (б) Дедуп/circuit-breaker: первый ERROR, дальше тишина до восстановления;
|
||||
- (в) Оставить как есть, признав `ext.*` ERROR легитимным сигналом «зависимость
|
||||
лежит» (тогда шум гасить уровнем сбора, а не кодом).
|
||||
|
||||
`reasons` (`review.go` recognizeOne) и `error_msg` переходов (`linkPlan`)
|
||||
показываются в review-экране и Telegram-карточке и могут нести детали
|
||||
реализации (тело LLM/qBit, абсолютные пути). Решить: (а) узаконить в `errors.md`
|
||||
как «операторская поверхность владельца» с явным запретом секретов; либо (б)
|
||||
держать там reason-код + нейтральный текст, полную ошибку — в лог по
|
||||
`download_id`.
|
||||
### Эскалация устойчивого сбоя тика
|
||||
|
||||
## Мелочь (по желанию, из тех же ревью)
|
||||
Сейчас транзиентный сбой тика = WARN всегда. Договорённость на будущее
|
||||
(`logging.md`): устойчивый сбой N тиков подряд эскалировать в ERROR (реальная
|
||||
деградация, а не разовый промах). Не реализовано — нужен счётчик подряд-сбоев по
|
||||
циклу и порог в конфиге.
|
||||
|
||||
- `httpapi/review.go` — поле `"id"` вместо словарного `"download_id"`; msg
|
||||
«review data» → категория «review data failed».
|
||||
- `worker/review.go` `parseIgnored` — `_ = json.Unmarshal` без обязательного
|
||||
комментария «почему» (битый override молча обнуляет игнор).
|
||||
- Reason-коды переходов (`"resolve"`/`"build"`/`"persist"`/`"collision"`/…) —
|
||||
свести в const-блок рядом с `errCode*` (сейчас только `reasonTitleFolderDesync`).
|
||||
- Превью в `ReviewData` при сбое — DEBUG, хотя это видимая деградация (пропадает
|
||||
кнопка «Применить»); уместнее WARN.
|
||||
|
||||
Вердикт: change (Tier B — правки кода + миграция статусов ошибок; Tier C —
|
||||
правки конвенций + одно продуктовое решение по `err.Error()`).
|
||||
Вердикт: мелкая надёжностная полировка, не блокер. Делать вместе (обе про
|
||||
уровень сбоев фоновых циклов) или отдельной строкой.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -39,6 +39,13 @@ log.Info(fmt.Sprintf("download %s accepted as movie", id))
|
||||
- `msg` — чистая категория без неймспейс-префикса: `recognition done`, а не
|
||||
`recognize: done`. Подсистему выносим в поле `capability`
|
||||
(`ingest`/`recognition`/`file-layout`/`review`), не в текст.
|
||||
- **Смена состояния загрузки — единая категория `state transition`** с полями
|
||||
`from`/`to`/`code` (какое именно состояние и по какой причине — это данные,
|
||||
не текст). Любой переход (в т.ч. `cancel`/`retry`/`relink`) пишет этот
|
||||
`msg`, чтобы весь жизненный цикл собирался одним фильтром: `jq
|
||||
'select(.msg=="state transition" and .download_id=="…")'`. Физический эффект
|
||||
сверх перехода — отдельная запись своей категории (`layout linked`,
|
||||
`layout reverted`, `review hint added`), не подменяет запись перехода.
|
||||
|
||||
## Уровни
|
||||
|
||||
@@ -141,13 +148,44 @@ log.Error(err.Error())
|
||||
- Идиома Go — **либо лог, либо возврат, не оба**. Промежуточные слои только
|
||||
оборачивают и возвращают (`fmt.Errorf("…: %w", err)`), не логируя —
|
||||
контекст накапливается в цепочке `%w`.
|
||||
- Логируем ошибку **один раз — на границе доменного слоя** (use-case
|
||||
`Ingest`, стадии воркера), которая определяет исход операции: полем
|
||||
`error`, уровень `ERROR`. В Go логирует этот единый чокпоинт, а не каждый
|
||||
транспорт — так транспорты остаются тонкими.
|
||||
- Логируем ошибку **один раз — на границе доменного слоя**, которая
|
||||
определяет исход операции: полем `error`. В Go логирует этот единый
|
||||
чокпоинт, а не каждый транспорт — так транспорты остаются тонкими. Границы
|
||||
в jellybit:
|
||||
- use-case `Ingest` (приём);
|
||||
- **асинхронные стадии воркера** (поллинг, распознавание, авто-раскладка) —
|
||||
исход стадии, вызванной таймером/циклом;
|
||||
- **публичные команды воркера** (`Apply`/`Refine`/`Cancel`/`Retry`/`Undo`/
|
||||
`Delete`/…), вызываемые транспортами. Исход команды логирует ровно один
|
||||
чокпоинт (`worker.logCmd`, в `defer` при именованном возврате), а не
|
||||
HTTP/web/Telegram — они одну и ту же команду зовут из трёх мест.
|
||||
- Транспорты (HTTP/web/Telegram) переводят возвращённую ошибку в свой ответ
|
||||
(статус, сообщение пользователю) и **не логируют** её повторно — иначе
|
||||
один сбой даёт дубли.
|
||||
- **Уровень доменного отказа — по адресату, а не по месту.** У каждой
|
||||
доменной ошибки ровно один логирующий; уровень выбирает он. На **границе
|
||||
команды** (пользователь инициировал действие и ждёт ответа — `worker.logCmd`):
|
||||
|
||||
| Класс отказа | Кому | Уровень |
|
||||
|---|---|---|
|
||||
| штатный конфликт состояния / некорректный ввод (`ErrConflict`, `ErrNotReady`, `ErrInvalidInput`, `ErrNotFound`, `layout.ErrCollision`) | пользователю (уже получил ответ на поверхности) | `DEBUG` |
|
||||
| нарушенный инвариант хранилища/учёта (не безопасность данных: файлы уже разложены) | команде, «может стать проблемой» | `WARN` |
|
||||
| сбой БД / ФС / недоступность зависимости | команде, в разбор | `ERROR` |
|
||||
|
||||
Тот же класс отказа в **асинхронной стадии** (пользователь не ждёт: авто-
|
||||
раскладка, поллинг) адресован уже команде как деградация автоматики — уровень
|
||||
поднимается. Пример: `layout.ErrCollision` в ручном `Apply` — `DEBUG` (человек
|
||||
видит причину в карточке), а в авто-раскладке — `WARN` («auto-apply failed,
|
||||
left for review»): автоматика не довела задачу, это «может стать проблемой».
|
||||
|
||||
- **Повторяющийся сбой фонового цикла (поллинг/сверка) — `WARN`, не `ERROR`.**
|
||||
Одиночный промах тика (`poll`/`sweep`/`list failed`, недоступный
|
||||
qBittorrent) транзиентен: следующий тик повторит. Тот же класс сбоя внутри
|
||||
синхронной операции (`ingest.Ingest`) — `ERROR`, потому что операция
|
||||
провалилась целиком и повтора нет. То есть уровень задаёт не текст ошибки, а
|
||||
наличие штатного ретрая: тик повторится → `WARN`, разовая операция упала →
|
||||
`ERROR`. (Устойчивый сбой N тиков подряд эскалировать в `ERROR` — на будущее,
|
||||
сейчас не реализовано.)
|
||||
- Телеметрия внешнего вызова (`ext.*`, см. ниже) — отдельная запись о
|
||||
поведении зависимости, не дубль доменной ошибки.
|
||||
- Глушить ошибку без лога — только с однострочным комментарием «почему».
|
||||
@@ -206,6 +244,15 @@ log.Error(err.Error())
|
||||
быть большим) — только на `DEBUG`, с вычисткой секретов и обрезкой по длине.
|
||||
- При сомнении — не логируем значение, логируем факт его наличия
|
||||
(`"has_api_key", true`).
|
||||
- **Ошибка HTTP-транспорта несёт URL — потенциальный носитель секрета.**
|
||||
`*url.Error` (стандартный `net/http`) встраивает полный URL запроса, а
|
||||
секрет может жить прямо в нём: токен Telegram в пути (`…/bot<TOKEN>/…`),
|
||||
`api_key` метабазы в query. Санитизируем на границе клиента **до** лога и
|
||||
обёртки — `logging.SanitizeErr(err)` разворачивает `*url.Error` в
|
||||
первопричину (URL отбрасывается, `errors.Is` на причину сохраняется).
|
||||
Применяется в `ext.*`-обёртке (`ExtCall`), клиентах metadata и tgbot. Общее
|
||||
правило: **секрет не кладём в URL, если у API есть заголовок** — тогда его
|
||||
нет и в ошибке транспорта.
|
||||
|
||||
## Куда пишем и уровень
|
||||
|
||||
|
||||
Reference in New Issue
Block a user