Все статьи
webhook debuggingHTTP errorsauthenticationtroubleshooting

Почему ваш вебхук возвращает 401 или 403 (и как это исправить)

Вебхук, возвращающий 401 или 403, попадает в небольшое число категорий. Чаще всего это даже не проблема подписи — это middleware или дефолт фреймворка отклоняет запрос до того, как запустится ваш код. Вот диагностический чек-лист, который мы используем, в том порядке, в каком мы его применяем.

Сначала отделите 401 от 403

Они означают разное, и исправление разное.

401 Unauthorized: сервер получил запрос, посмотрел на учётные данные (или подпись) и не принял их. Ваш обработчик, вероятно, отработал, вычислил ожидаемую подпись и отклонил.

403 Forbidden: сервер получил запрос и отказался обрабатывать его по другой причине. Часто запрос вообще не дошёл до вашего обработчика — 403 отправил middleware или дефолт фреймворка.

Откройте захват запросов вашего туннеля и посмотрите на ответ. Если видите тело ответа вашего обработчика («invalid signature»), это 401 из вашего кода. Если ответ общий, а логи обработчика пусты, фреймворк отклонил запрос до запуска обработчика.

Пять самых вероятных причин

1. CSRF-middleware (Django, Rails, Laravel)

Дефолтная CSRF-защита отклоняет POST без токена сессии. Провайдеры вебхуков его не присылают. Симптомы: 403, общее тело ответа, нет логов обработчика.

Исправление: исключите маршрут вебхука из CSRF-защиты. В Django @csrf_exempt. В Rails skip_before_action :verify_authenticity_token, only: [:webhook]. В Laravel добавьте путь в VerifyCsrfToken::$except. Руководство по вебхукам Django разбирает версию для Django от начала до конца.

2. Auth-middleware применён слишком широко

Вы добавили auth-middleware глобально (JWT, проверка сессии, требование API-ключа), и маршрут вебхука его унаследовал. Провайдер не присылает ваш auth-заголовок, поэтому middleware отдаёт 401 до запуска обработчика.

Исправление: исключите путь вебхука из вашего auth-middleware. Аутентификация вебхука — это подпись, а не схема, защищающая остальной API.

3. Проверка подписи не проходит

Обработчик отработал, вычислил ожидаемую подпись, и она не совпала. Пять подпричин, примерно по убыванию частоты:

  • Тело разобрано middleware до проверки (сырого тела больше нет).
  • Неверная кодировка (hex vs base64). См. руководство по проверке подписей.
  • Неверный секрет (test vs live, dashboard vs CLI, env-файл vs runtime).
  • Слишком старый timestamp (подпись валидна, но устарела — вероятно, вы тестируете старой повторно отправленной полезной нагрузкой).
  • Замыкающие пробелы в секрете, загруженном из env-файла.

4. Зарегистрирован неверный URL туннеля

Вы перезапустили туннель, URL сменился, но в дашборде провайдера всё ещё старый. Симптом похож на 401, потому что запрос до вас не дошёл, — но на деле другой запрос попадает на другой сервер (часто предыдущую сессию туннеля, которая теперь отказывает или возвращает 401).

Исправление: убедитесь, что URL в дашборде провайдера совпадает с текущей сессией туннеля. Нужен стабильный URL — посмотрите именованные туннели или зарезервированный поддомен.

5. Ограничения CORS, content-type или метода

Для вебхуков реже, но возможно. Если маршрут принимает только application/json, а провайдер шлёт application/x-www-form-urlencoded (например, Twilio), некоторые фреймворки дают 415 — а неправильно настроенный может дать 403. Или маршрут зарегистрирован на GET, а провайдер делает POST.

90-секундный диагностический поток

Вот порядок, по которому мы идём:

  1. Прочитайте тело ответа. Если в нём слова вашего обработчика, запрос дошёл до обработчика. Переходите к отладке подписи. Если это общая страница ошибки фреймворка, запрос не дошёл. Переходите к отладке middleware.
  2. Проверьте логи обработчика. Срабатывают ли какие-либо лог-выражения вашего обработчика? Подтверждает, дошёл ли запрос до вас.
  3. Проверьте зарегистрированный URL. Откройте дашборд провайдера. Убедитесь, что URL совпадает с текущим туннелем. Убедитесь, что он указывает на правильный путь.
  4. Сравните входящие заголовки. Захват туннеля показывает точные заголовки, которые прислал провайдер. Сравните с тем, что читает ваш код. Заголовки регистронезависимы, но способ доступа к ним разнится по фреймворкам — request.headers.get('Stripe-Signature') в одних, request.META['HTTP_STRIPE_SIGNATURE'] в других.
  5. Убедитесь, что тело сырое в момент подписи. Выведите длину тела прямо перед вычислением подписи. Если ноль или подозрительно мало — middleware его съел.
  6. Перепроверьте секрет. Сравните env-переменную в рантайме с дашбордом. console.log(process.env.WEBHOOK_SECRET.length) — длина совпадает с тем, что показывает дашборд?

По нашему опыту, шаги 1 и 5 ловят 80% случаев в первые две минуты.

Захватите неудавшийся запрос

Самый полезный инструмент здесь — туннель с захватом и повтором запросов. Не нужно ждать повтора от провайдера — вы воспроизводите захваченный запрос против локального обработчика, пока отлаживаете. Каждая попытка мгновенна.

Если вы используете туннель, который не захватывает запросы, вы отлаживаете с одной связанной рукой. Переход на тот, который умеет (или запуск tcpdump, или nginx перед dev-сервером), окупается в первый же раз, когда вы экономите час.

Как 401 выглядит у каждого провайдера

  • Stripe: 401 из вашего кода значит, что проверка подписи не прошла. Дашборд Stripe показывает доставку как неудавшуюся и включает ваше тело ответа.
  • GitHub: если обработчик возвращает 401, GitHub помечает доставку как неудавшуюся и повторяет. Страница последних доставок показывает ответ.
  • Shopify: 401 от обработчика вебхука нормален для безопасности, но Shopify будет повторять. После 19 подряд неудач за 48 часов Shopify отключает подписку.

Более широкий контекст отладки вебхуков см. в как отлаживать вебхуки локально. Запишитесь в лист ожидания PortPreview ради туннеля со встроенным захватом.

Часто задаваемые вопросы

Что означает 401 на вебхуке?
Ваш обработчик получил запрос, оценил учётные данные или подпись и отклонил их. Чаще всего это сбой проверки подписи из-за разбора тела до проверки, неверной кодировки (hex vs base64) или неверного секрета.
Почему провайдеры вебхуков видят 403 от моего приложения?
403 обычно означает, что запрос так и не дошёл до вашего обработчика. Чаще всего причина — CSRF-middleware (в Django, Rails, Laravel) или слишком широко применённый auth-middleware. Исключите маршрут вебхука из CSRF и общей аутентификации API, чтобы запрос дошёл до вашего кода проверки.
Как отлаживать сбои аутентификации вебхука, не запуская события заново?
Используйте туннель с захватом и повтором запросов. Захватите одну доставку от провайдера, затем воспроизводите её против локального обработчика сколько нужно при отладке. Каждый повтор мгновенен и не зависит от провайдера.