すべての記事
Shopifywebhook debugginglocal testinge-commerce

Shopify Webhook をローカルでサプライズなしにテスト

ローカル開発での Shopify Webhook バグのほとんどは 1 行に帰着します:HMAC は base64 エンコードであって hex ではありません。.digest('hex') を呼んで X-Shopify-Hmac-Sha256 と比較すると、正しい共有シークレットでもチェックは決して通りません。この 1 文字でシニアエンジニアが午後を失うのを見てきました。

base64 の罠

Stripe は hex。GitHub は hex(sha256= プレフィックス付き)。Shopify は base64。3 つのプロバイダー、3 つのエンコーディング。検証器の最初のバージョンは、ほぼ確実に別プロジェクトのスニペットをコピーして静かに失敗します。

検証はこうあるべきです:

const hmac = crypto
  .createHmac('sha256', SHOPIFY_API_SECRET)
  .update(rawBody)         // raw body, not JSON-parsed
  .digest('base64');       // base64, not hex

const valid = crypto.timingSafeEqual(
  Buffer.from(hmac),
  Buffer.from(req.headers['x-shopify-hmac-sha256']),
);

エンコーディング以外に 2 つ重要です。ボディは生の、パースされていないバイトでなければならない。そして比較はタイミングセーフでなければならない——文字列の等価は 1 バイトずつ情報を漏らします。

なぜトンネルが楽にするか

Shopify の shopify app dev コマンドも使えます。内部トンネルを立ち上げます。動きます。でも dev サーバーを独自のプロセスで包み、ログを特定の方法で飲み込み、リプレイボタンを与えません。「hello world」を超えるアプリ開発では、安定した公開 URL とリクエストキャプチャ付きの localhost トンネルが、CLI が初期セットアップで節約する以上の時間を節約します。

npx portpreview 3000

HTTPS URL を Partners ダッシュボードのアプリの Webhook 設定に貼り、テストストアからイベントをトリガーすると、リクエストはすべてのヘッダーがそのままローカルハンドラーに着地します。

テストストア:Shopify ドキュメントがざっと流す部分

何かを配線する前に知っておくこと 2 つ:

  • 開発ストアはすべての Webhook イベントを発火します。注文作成、フルフィルメント、在庫——全部。何も偽装する必要はない。dev ストアにアプリをインストールしてクリックするだけ。
  • Webhook シークレットはアプリごと、配信チャネルごとに異なります。EventBridge や Pub/Sub も購読している場合、HMAC の挙動は違います。ここでは素の HTTPS Webhook 配信の話です。

ステップごとのセットアップ

  1. 使うポート(3000 が一般的)でアプリをローカル起動。
  2. npx portpreview 3000 を実行し、出力された HTTPS URL をコピー。
  3. Shopify Partners ダッシュボードでアプリを開き、Configuration → Webhooks へ。
  4. Webhook エンドポイントを https://your-tunnel.portpreview.dev/api/webhooks/shopify(またはアプリが使うパス)に設定。
  5. dev ストアにアプリをインストールし、イベントをトリガー——下書き注文の作成、商品のフルフィル、在庫変更。
  6. リクエストの着地を見る。ヘッダーとボディを検査。ハンドラー修正のたびにキャプチャしたリクエストをリプレイ。

繰り返し見るミス

検証前にボディがパースされている

app.use(express.json()) を Webhook ルートの前に置くと、検証しようとする頃には生バイトが消えています。raw-body パーサーを Webhook パスだけにマウントするか、JSON パースの前にリクエストストリームから手動でボディを取り出します。

シークレットの取り違え

Partners アプリシークレットは Storefront API トークンと同じではありません。Webhook HMAC はアプリシークレットを使います。env ファイルの shp_xxx を見ているなら、間違ったものを取っています。

同一に見えるテストイベント

Shopify の「Send test notification」ボタンは、プレースホルダーボディの合成イベントを配信します。そのテストイベントの署名は本物ですが、payload は固定です。現実的な payload 形状テストには、dev ストア自体からイベントをトリガーします。

アプリ開発でリプレイは譲れない

Shopify は失敗した Webhook を指数バックオフで最大 48 時間リトライします。寛大ですが、反復中は次のリトライを待ちたくありません。最初の配信を トンネルのリクエスト履歴でキャプチャし、ローカルハンドラーに対してオンデマンドでリプレイします。

ローカルで足りなくなるとき

ショップ登録フロー、GDPR Webhook、サブスクリプション課金の変更は、Shopify 自身のアカウント状態と連携するため、デプロイ済み環境に対して検証する方が楽です。それ以外——payload パース、署名検証、ビジネスロジック——はローカルが速い。

全プロバイダーの基礎となる署名の数学は、Webhook 署名検証ガイドを読んでください。リプレイ内蔵のトンネルが欲しければ PortPreview のウェイトリストへ。

よくある質問

Shopify Webhook を localhost でどうテストする?
PortPreview のようなトンネルを起動して公開 HTTPS URL を取得し、Shopify Partners ダッシュボードで Webhook エンドポイントとして登録します。開発ストアにアプリをインストールしてイベントをトリガーすると、配信がローカルハンドラーに届きます。
なぜ Shopify の HMAC 検証がいつも失敗する?
たいていエンコーディングです。Shopify は HMAC SHA-256 で署名し結果を base64 エンコードします。コードが .digest('base64') でなく .digest('hex') を使うと、シークレットが正しくてもすべての比較が失敗します。
shopify app dev が必要?それとも任意のトンネルでよい?
任意のトンネルで動きます。shopify app dev は便利のため独自トンネルを含みますが、リクエストキャプチャとリプレイ付きの汎用トンネルのほうが継続的な開発には有用です。