webpush-go в getVAPIDAuthorizationHeader приписывает "mailto:" всему, что не начинается с "https:". В /etc/bare/env лежит правильная по спецификации VAPID форма mailto:admin@xmatic.team, и в JWT уходил sub = mailto:mailto:admin@xmatic.team. Apple отвергал токен, пуши не работали ни разу за всё время. Проверено экспериментом на живом endpoint: mailto:<адрес> → 201, https://<хост> → 201, mailto:mailto:<адрес> → 403 BadJwtToken, голый адрес без схемы → 403. Значит убрать схему из конфига нельзя: адаптируется код, а не операционный файл. Субъект нормализуется один раз при создании отправщика, форма BARE_VAPID_SUBJECT проверяется при старте — негодное значение валит запуск с понятной ошибкой, а не выключает пуши молча. ADR-064: в журнал пишется код причины от push-сервиса. Без него отказ был неотличим от любого другого 403, и дефект прожил незамеченным весь этап 4 и всю закалку. Адрес подписки в журнал по-прежнему не попадает: наружу идёт только короткий код из латиницы и цифр. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
3.5 KiB
ADR-064: Код причины отказа push-сервиса в журнале
Уточняет ADR-047.
Контекст
ADR-047 убрал из журнала адрес подписки: отказ отправки пишется классом, текст ошибки транспорта не печатается вовсе. Под то же правило попал и ответ push-сервиса — в журнал уходил один статус: пуш: push-сервис ответил 403.
Цена выяснилась на работающем сервере. APNs отвечал 403 на каждый пуш, потому что в VAPID-токен уезжал sub = "mailto:mailto:admin@xmatic.team": webpush-go приписывает mailto: всему, что не начинается с https:, а в окружении субъект записан правильным URI. Сутки не уходило ни одного пуша, и по журналу это выглядело как «что-то с пушами»: просроченный ключ, чужой субъект, лимит вендора и мёртвая подписка дают один и тот же 403. Причина лежала в теле ответа: {"reason":"BadJwtToken"}.
Код причины от push-сервиса — диагностика вендора, а не данные пользователя. Но тело ответа приходит снаружи, и класть его в журнал целиком нельзя: в нём может оказаться адрес подписки.
Решение
- В журнал уходит статус и, если он разобран, короткий код причины:
пуш: push-сервис ответил 403 (BadJwtToken). - Читается не больше 200 байт тела; остаток дочитывается ради переиспользования соединения. Код берётся из полей JSON
reason,error,message— в этом порядке — либо из тела целиком, если оно само короткая строка. - В журнал идёт только первая строка не длиннее 64 символов из латиницы, цифр,
_,-и пробелов. Точка,:,/и@встречаются в адресах подписки и именах хостов, поэтому строка с ними отбрасывается целиком и остаётся один статус. - Правило ADR-047 про транспорт не меняется: ошибка соединения по-прежнему сводится к классу, её текст не печатается, адреса подписки в журнале нет.
Следствия
- Отказ push-сервиса читается по журналу и чинится по нему же.
- Часть вендорских кодов не разберётся: FCM отвечает вложенным объектом, а коды с точкой в имени не проходят алфавит. Тогда в журнале остаётся статус, как раньше. Осознанно: молчание безопаснее догадок о чужом теле.
- Ответы
404и410пишутся не строкой, а снятием подписки — так было и остаётся (ADR-011).