1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ローカルで動く Webhook キャプチャツールを PHP Slim 4 で作った

1
Posted at

きっかけ

webhook.site や requestbin.com は便利だが、顧客データを扱うサービスの Webhook デバッグに使うのは抵抗がある。実際のリクエストが第三者のログパイプラインを通ることになるし、無料枠にはレート制限や URL の有効期限もある。

1 ヶ月に 3 回同じ問題に当たった。GitHub Actions、Shopify アプリ、社内の請求 cron ── Webhook の送信元が何を送っているのか、受信側を除外して確認したい場面が繰り返された。

ローカルで動く自前のリクエストインスペクターを Slim 4 で作った。~200 行の実コード。

📦 GitHub: https://github.com/sen-ltd/webhook-inspector

スクリーンショット

作ったもの

リクエストインスペクターの本質は 3 つ:

  1. HTTP シンク。 あらゆるメソッド・ヘッダー・ボディを受け入れて 200 を返す
  2. 構造化ストア。 各リクエストを {method, path, headers, body, query, client_ip, received_at, bytes} として記録
  3. 一覧 UI。 保存されたリクエストを新しい順に表示し、クリアできる

webhook.site が解決しなければならないマルチテナンシー、DDoS 対策、永続性、ドメイン名の問題は、localhost で動くツールにはすべて不要。それが設計のシンプルさにつながる。

技術的なポイント

ワイルドカードメソッドルート

Slim 4 の map()GET POST PUT PATCH DELETE を一括キャプチャ:

$capture = function (
    ServerRequestInterface $request,
    ResponseInterface $response,
    array $args,
) use ($json, $maxBodyBytes, $validSlug) {
    $slug = (string) $args['slug'];
    if (!$validSlug($slug)) {
        return $json($response, ['error' => 'bad_slug'], 400);
    }
    $body = (string) $request->getBody();
    if (strlen($body) > $maxBodyBytes) {
        return $json($response, ['error' => 'too_large', 'limit_bytes' => $maxBodyBytes], 413);
    }
    $snap = RequestCapture::snapshot($request, $slug);
    $id   = webhook_inspector_repo()->append($slug, $snap);
    return $json($response, ['received' => true, 'slug' => $slug, 'id' => $id]);
};

$app->map(
    ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    '/bin/{slug}',
    $capture,
);

TRACECONNECT は YAGNI。Webhook はこの 5 メソッドで十分。

リングバッファ

slug ごとの FIFO キューに上限を設定。最新 N 件を保持し、超過したら古いものから捨てる:

public function append(string $slug, array $captured): string
{
    $id = $this->nextId();
    $captured['id'] = $id;

    if (!isset($this->bins[$slug])) {
        $this->bins[$slug] = [];
    }
    $this->bins[$slug][] = $captured;

    while (count($this->bins[$slug]) > $this->maxPerBin) {
        array_shift($this->bins[$slug]);
    }
    return $id;
}

array_shift のループは O(n) だが、maxPerBin = 100 で秒間数リクエストなら無視できる。SplQueue よりも foreach しやすいプレーン配列のほうがデバッグ時の可読性で勝る。

php -S のプロセスモデルの罠

PHP のビルトインサーバーはリクエストごとに新しいワーカーを生成するため、静的変数の「シングルトン」はリクエスト間で永続化されない。POST して一覧を見たら空 ── という現象が発生。

修正は flock 付きの JSON ファイルでリポジトリをバックアップすること。Docker コンテナ起動時に rm -f してクリーンスタート:

CMD ["sh", "-c", "rm -f /tmp/webhook-inspector-state.json \
                  && exec php -S 0.0.0.0:8000 -t public public/index.php"]

バイナリボディの安全な処理

Webhook ペイロードの大半は JSON だが、application/x-www-form-urlencoded や gzip された protobuf もある。UTF-8 テキストならそのまま、それ以外は Base64 にエンコード:

public static function encodeBody(string $body): array
{
    if ($body === '') {
        return ['body' => '', 'encoding' => 'utf-8'];
    }
    if (self::isUtf8Text($body)) {
        return ['body' => $body, 'encoding' => 'utf-8'];
    }
    return ['body' => base64_encode($body), 'encoding' => 'base64'];
}

トレードオフ

  • 永続性なし。 再起動でクリア。デバッグツールとしてはこれが正解
  • 認証なし。 localhost 前提。公開する場合はリバースプロキシで認証を追加
  • WebSocket なし。 ダッシュボードは 2 秒ごとのポーリング。リアルタイムに見えるには十分
  • メモリバウンド。 デフォルト設定(1 bin あたり 100 件、最大 5 MB)で最悪 ~500 MB。複数 bin を使うなら MAX_BODY_MB を調整

試してみる

git clone https://github.com/sen-ltd/webhook-inspector
cd webhook-inspector
docker build -t webhook-inspector .
docker run --rm -p 8000:8000 webhook-inspector

別ターミナルで:

# Webhook を送信
curl -X POST http://localhost:8000/bin/test \
  -H "Content-Type: application/json" \
  -H "X-GitHub-Event: push" \
  -d '{"event":"push"}'

# キャプチャを確認
curl -s http://localhost:8000/bin/test/requests | jq

# クリア
curl -X DELETE http://localhost:8000/bin/test/requests

ブラウザで http://localhost:8000/ を開けばポーリングダッシュボードも使える。

おわりに

35 件の PHPUnit テスト、~200 行のコード、52 MB の Alpine イメージ。Webhook デバッグに外部サービスを使いたくない場面で、手元で完結するリクエストインスペクターがあると便利。ソースを一気読みできるサイズに収めた。

SEN 合同会社100+ ポートフォリオシリーズ エントリ #147。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?