Усі статті
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 (підпис дійсний, але застарілий — імовірно, ви тестуєте старим повторно надісланим payload).
  • Кінцеві пробіли в секреті, завантаженому з 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, щоб запит дійшов до вашого коду перевірки.
Як налагоджувати збої автентифікації вебхука без повторного запуску подій?
Використовуйте тунель із захопленням і повтором запитів. Захопіть одну доставку від провайдера, потім відтворюйте її проти локального обробника скільки треба під час налагодження. Кожен повтор миттєвий і не залежить від провайдера.