Як тестувати вебхуки GitHub

Налаштовуємо вебхук репозиторію, розбираємо заголовки GitHub, правильно перевіряємо X-Hub-Signature-256 і повторно доставляємо події під час налагодження.

Оновлено:

Вебхуки GitHub повідомляють ваш сервіс про push, pull request, issues, релізи та інші події. Перш ніж писати обробник, корисно побачити справжню доставку: які заголовки ставить GitHub, як виглядає тіло для різних content type і як формується підпис. Покажемо, як ловити доставки на публічну адресу, перевіряти їх і пересилати на локальний код.

Крок 1: створіть адресу для перехоплення

Відкрийте Webhook Inspector і створіть адресу, наприклад:

https://webhook.voronyuk.com/abcd23efgh/github/webhook

Вона приймає будь-який метод, підшлях і query. За замовчуванням відповідає 200 з {"ok":true,"request_id":N}, тож GitHub позначає кожну доставку як успішну.

Крок 2: додайте вебхук у репозиторій

  1. У репозиторії відкрийте Settings → Webhooks і натисніть Add webhook.
  2. Payload URL: вставте адресу перехоплювача.
  3. Content type: application/json або application/x-www-form-urlencoded (про різницю нижче).
  4. Secret: довгий випадковий рядок. Ним ви перевірятимете підписи.
  5. Виберіть, які події надсилати: лише push, усі або окремі.
  6. Залиште позначку Active і натисніть Add webhook.

GitHub одразу надішле подію ping, і за кілька секунд вона з’явиться в інспекторі.

Крок 3: заголовки, які варто знати

Заголовок Що означає
X-GitHub-Event Назва події: push, pull_request, ping тощо
X-GitHub-Delivery Унікальний GUID цієї доставки
X-GitHub-Hook-ID ID вебхука, який її надіслав
X-Hub-Signature-256 sha256= і далі HMAC-SHA256 від сирого тіла в hex
X-Hub-Signature Старий підпис SHA-1, лишився для сумісності
User-Agent Починається з GitHub-Hookshot/

Маршрутизуйте за X-GitHub-Event, а за X-GitHub-Delivery відсіюйте дублікати.

Подія ping

Перша доставка — це ping. У тілі є рядок zen, hook_id і конфігурація hook. Обробник має відповісти на неї 2xx, навіть якщо нічого з нею не робить.

JSON чи form-encoded

З application/json тіло — це сам JSON. З application/x-www-form-urlencoded JSON передається як значення поля форми payload. Інспектор розкодовує form data, тож поле payload з JSON усередині ви побачите в будь-якому разі. З JSON зазвичай простіше.

В обох випадках підпис рахується від сирих байтів тіла, а не від розкодованого JSON.

Крок 4: перевірка X-Hub-Signature-256

GitHub рахує HMAC-SHA256 від сирого тіла з вашим секретом і надсилає результат як sha256=<hex>. Порахуйте те саме і порівняйте за сталий час.

Node.js

const crypto = require('crypto');
const express = require('express');

const SECRET = process.env.GITHUB_WEBHOOK_SECRET;
const app = express();

function isValid(rawBody, header) {
  if (!header || !header.startsWith('sha256=')) return false;
  const expected =
    'sha256=' + crypto.createHmac('sha256', SECRET).update(rawBody).digest('hex');
  const a = Buffer.from(expected);
  const b = Buffer.from(header);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
  if (!isValid(req.body, req.get('X-Hub-Signature-256'))) {
    return res.status(401).send('Bad signature');
  }
  const event = req.get('X-GitHub-Event');
  const delivery = req.get('X-GitHub-Delivery');
  res.status(202).send('Accepted'); // спершу відповідаємо
  console.log(event, delivery);     // потім обробляємо (краще в черзі)
});

app.listen(3000);

Приклад розрахований на application/json. Якщо обрали form encoding, після перевірки підпису розберіть поле payload із сирого тіла.

Python

import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["GITHUB_WEBHOOK_SECRET"].encode()


@app.post("/webhook")
def webhook():
    header = request.headers.get("X-Hub-Signature-256", "")
    expected = "sha256=" + hmac.new(SECRET, request.get_data(), hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, header):
        abort(401)
    print(request.headers.get("X-GitHub-Event"), request.headers.get("X-GitHub-Delivery"))
    return "", 202


if __name__ == "__main__":
    app.run(port=3000)

Крок 5: пересилання на localhost

У панелі Пересилання інспектора вкажіть ціль http://localhost:3000. Перехоплений підшлях дописується до цілі, тож .../abcd23efgh/github/webhook прийде на http://localhost:3000/webhook. Можна скористатися relay через браузер (сервер має дозволяти CORS) або CLI-агентом, якому браузер не потрібен і який доганяє пропущені запити:

WI_TOKEN='<токен>' inspector listen https://webhook.voronyuk.com/abcd23efgh/github --to localhost:3000

Точну команду з токеном скопіюйте з панелі. Детальніше — у гайді як тестувати вебхуки локально.

Тайм-аути, збої й повторна доставка

  • Відповідайте за 10 секунд. GitHub чекає 2xx протягом 10 секунд. Якщо сервер не встиг, GitHub рве з’єднання і записує доставку як невдалу. Спершу підтвердіть отримання, а обробляйте асинхронно.
  • Автоматичних повторів немає. GitHub не надсилає невдалі доставки повторно сам. Якщо ендпоінт лежав, повторювати доведеться вручну або через REST API.
  • Recent Deliveries. У Settings → Webhooks відкрийте вебхук і перейдіть на вкладку Recent Deliveries. Для кожної доставки видно заголовки й payload запиту та відповідь вашого сервера. Кнопка Redeliver надсилає її ще раз.

Оскільки невдалі доставки не повторюються, власна копія не завадить. Кожна доставка на адресу перехоплювача зберігається, і її можна скільки завгодно разів повторити з тестера вебхуків GitHub на локальний сервер. Щоб перевірити реакцію на збої, задайте для адреси власну відповідь, наприклад статус 500, і подивіться, що GitHub запише в Recent Deliveries.

Схожі гайди: вебхуки Stripe і Slack Events API.

Часті питання

Як протестувати вебхук GitHub?

Додайте вебхук у Settings → Webhooks, указавши як Payload URL публічну адресу перехоплювача. GitHub одразу надішле ping. Далі зробіть push чи відкрийте pull request, щоб отримати справжні події, і роздивіться або повторіть їх.

Чому не сходиться підпис вебхука GitHub?

Найімовірніше, ви хешуєте вже розпарсене або перезібране тіло. Рахуйте HMAC від сирих байтів, порівнюйте разом із префіксом sha256= і перевірте, що секрет збігається до символу, без пробілів у кінці.

Чи повторює GitHub невдалі доставки вебхуків?

Ні. GitHub не повторює невдалі доставки автоматично. Скористайтеся кнопкою Redeliver у Recent Deliveries або REST API.

Який тайм-аут у вебхуків GitHub?

10 секунд. Якщо за цей час сервер не відповів 2xx, доставка вважається невдалою.

Що обрати: application/json чи form-encoded?

application/json простіший: тіло і є payload. При form encoding JSON приходить у полі payload, і його треба спершу розкодувати.

Спробуйте зараз

Отримайте безкоштовний URL для вебхуків і дивіться, як приходять запити, — без реєстрації.

Отримати URL для вебхуків

Інші гайди

Localhost

Як тестувати вебхуки локально

Чому Stripe, GitHub та інші сервіси не бачать ваш localhost і як зловити справжній вебхук на публічній адресі, а потім переслати його собі на машину.

MCP

Тестування вебхуків із Claude і ChatGPT через MCP

Підключіть Webhook Inspector як MCP-сервер, і AI-асистент сам створить URL, дочекається вебхука й розбере його разом із вами.

Shopify

Як тестувати вебхуки Shopify

Створюємо вебхук магазину або підписку застосунку, розбираємо заголовки X-Shopify, перевіряємо X-Shopify-Hmac-Sha256 і враховуємо тайм-аути та повтори Shopify.

Slack

Як тестувати Slack Events API та interactivity

Дивимося, що Slack надсилає на Request URL, проходимо перевірку url_verification, валідуємо X-Slack-Signature і розкодовуємо form-encoded payload від interactivity.

Stripe

Як тестувати вебхуки Stripe

Налаштовуємо ендпоінт у Stripe, дивимося, що саме приходить, перевіряємо Stripe-Signature на Node.js і Python та пересилаємо події на локальний застосунок.

Telegram

Як тестувати вебхук Telegram-бота

Спрямовуємо бота на публічну адресу через setWebhook, роздивляємося сирі апдейти від Telegram і пересилаємо їх боту, що працює на вашій машині.

Twilio

Як тестувати вебхуки Twilio для SMS і дзвінків

Спрямовуємо номер Twilio на адресу-перехоплювач, читаємо form-encoded параметри, відповідаємо TwiML без власного сервера і перевіряємо X-Twilio-Signature через офіційні бібліотеки.