◉CLAUDE LABEN
●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました●2.1.301 — vim の % モーション、VS Code パネルでの LaTeX 数式表示、使用量上限待ちのリトライ監視の上限が入りました(10/15)●2.1.299 — 200K トークン超で約54分放置した会話はアイドル圧縮。MCP の許可ルールは名指ししたサーバーだけに一致します●SONNET 4.5 — Claude API での提供終了は 11/30、残り45日。移行先は Sonnet 5.5●WINDOWS — 日本語の環境でだけフックが黙って壊れる報告があり、文字コードの確認が呼びかけられています●FAST MODE — Opus 5.5 の Fast mode がリサーチプレビューを終え、ベータヘッダーが不要になりました(10/15)●NEW — Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
記事一覧/Cowork
◈ Cowork/2026-03-25中級

Claude in Chrome で Stripe Webhook の HTTP 500 エラーを解決した実践レポート

Stripe Webhookが全サイトでHTTP 500を返す障害を、Claude in Chromeで原因特定からコード修正・デプロイ・動作確認まで一気通貫で解決した実録ガイド。Cloudflare Workers + Next.js環境での落とし穴と対策を解説します。

Claude in Chrome14Stripe15Webhook4Cloudflare Workers16デバッグ3Cowork46

取り組みの背景 — Stripe から届いた警告メール

ある朝、Stripe から「Webhook の配信に問題が発生しました」という通知メールが4サイト分届きました。すべてのエンドポイントが HTTP 500 を返し続けており、合計34回のリトライが失敗している状態です。

サブスクリプション課金を導入している以上、Webhook の障害は収益に直結します。放置すれば請求書の発行が最大3日遅延し、Stripe 側が自動的にエンドポイントへの送信を停止してしまいます。

環境構成

今回の障害が発生していた環境は以下の通りです。

  • フレームワーク: Next.js 16(App Router)
  • ホスティング: Cloudflare Workers(OpenNext アダプター経由)
  • 決済: Stripe(Webhook + Checkout)
  • ストレージ: Cloudflare KV(プレミアムアクセス管理)
  • 言語: TypeScript
  • 影響範囲: 4サイト(Claude Lab / Gemini Lab / Antigravity Lab / Rork Lab)

原因調査 — Claude in Chrome でコードを横断分析

まず、Claude in Chrome を使って4サイト全体の Webhook コードを横断的に調査しました。各リポジトリの src/app/api/webhook/route.ts を読み込み、パターンを比較します。

すぐに2つの問題が見つかりました。

問題1: Edge ランタイムの明示宣言

// ❌ 問題のあったコード
export const runtime = "edge";
 
export async function POST(request: NextRequest) {
  // ...
}

Webhook ルートには export const runtime = "edge" が宣言されていましたが、正常に動作していた Checkout ルートにはこの宣言がありませんでした。

Cloudflare Workers + OpenNext の環境では、すべてのルートがデフォルトで Workers(Edge)ランタイムで実行されます。明示的に runtime = "edge" を宣言すると、OpenNext がルートのバンドル方法を変更し、結果として Worker 全体がクラッシュしていました。

問題2: constructEvent(同期版)の使用

// ❌ 同期版 — Node.js の crypto モジュールに依存
event = stripe.webhooks.constructEvent(body, sig, secret);
 
// ✅ 非同期版 — Web Crypto API を使用(Edge 互換)
event = await stripe.webhooks.constructEventAsync(body, sig, secret);

Stripe SDK の constructEvent() は Node.js の crypto.createHmac に依存する同期メソッドです。Edge ランタイムでは Web Crypto API しか利用できないため、constructEventAsync() を使う必要があります。

問題3: エラーハンドリングの不備

KV(Key-Value ストレージ)への読み書き操作が try-catch で囲まれておらず、KV 側でエラーが発生すると未処理例外として HTTP 500 が返っていました。

修正内容

以下の3点を全4サイトに適用しました。

import { NextRequest, NextResponse } from "next/server";
import Stripe from "stripe";
 
interface KVNamespace {
  get(key: string): Promise<string | null>;
  put(key: string, value: string, options?: { expirationTtl?: number }): Promise<void>;
  delete(key: string): Promise<void>;
}
 
// ✅ runtime = "edge" を削除(Cloudflare Workers ではデフォルトで Edge)
 
export async function POST(request: NextRequest) {
  try {
    const body = await request.text();
    const sig = request.headers.get("stripe-signature");
 
    if (!sig || !process.env.STRIPE_WEBHOOK_SECRET) {
      return NextResponse.json(
        { error: "Missing signature or secret" },
        { status: 400 }
      );
    }
 
    const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
      apiVersion: "2026-02-25.clover",
      httpClient: Stripe.createFetchHttpClient(),
    });
 
    let event: Stripe.Event;
    try {
      // ✅ constructEventAsync を使用(Web Crypto API)
      event = await stripe.webhooks.constructEventAsync(
        body,
        sig,
        process.env.STRIPE_WEBHOOK_SECRET
      );
    } catch {
      return NextResponse.json(
        { error: "Webhook signature invalid" },
        { status: 400 }
      );
    }
 
    // KV 接続
    let kv: KVNamespace | null = null;
    try {
      kv = (process.env as unknown as { PREMIUM_ACCESS: KVNamespace })
        .PREMIUM_ACCESS;
    } catch {
      // KV not available
    }
 
    if (!kv) {
      return NextResponse.json({ received: true, note: "KV not available" });
    }
 
    // ✅ KV 操作を try-catch で保護
    try {
      switch (event.type) {
        case "customer.subscription.updated": {
          const sub = event.data.object as Stripe.Subscription;
          const email = (sub as unknown as { customer_email?: string })
            .customer_email;
          if (email) {
            const kvKey = `site:claudelab:email:${email}`;
            const existing = await kv.get(kvKey);
            if (existing) {
              const record = JSON.parse(existing);
              record.expires_at = new Date(
                Date.now() + 31 * 24 * 3600 * 1000
              ).toISOString();
              await kv.put(kvKey, JSON.stringify(record), {
                expirationTtl: 31 * 24 * 3600,
              });
            }
          }
          break;
        }
        case "customer.subscription.deleted": {
          const sub = event.data.object as Stripe.Subscription;
          const email = (sub as unknown as { customer_email?: string })
            .customer_email;
          if (email) {
            await kv.delete(`site:claudelab:email:${email}`);
          }
          break;
        }
      }
    } catch {
      return NextResponse.json({
        received: true,
        note: "KV operation error",
      });
    }
 
    return NextResponse.json({ received: true });
  } catch {
    // トップレベルの catch で 500 を防止
    return NextResponse.json({ received: true, note: "Internal error" });
  }
}

修正のポイント:

  1. export const runtime = "edge" を削除 — OpenNext のデフォルト動作に任せる
  2. constructEvent → constructEventAsync — Web Crypto API 対応
  3. KV 操作を try-catch で保護 — KV エラー時も Webhook を受信済みとして応答
  4. トップレベル try-catch を追加 — 予期しないエラーでも HTTP 200 を返す

Claude in Chrome でのライブ検証

コードを修正して全4サイトに push した後、Claude in Chrome でリアルタイムに動作確認を行いました。

各サイトのページ上から JavaScript の fetch で Webhook エンドポイントに POST リクエストを送信し、レスポンスを確認します。

// Claude in Chrome がブラウザ上で実行した検証コード
const res = await fetch('/api/webhook', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: '{}'
});
console.log(res.status, await res.text());
// 修正前: 500 "Internal Server Error"
// 修正後: 400 {"error":"Missing signature or secret"}

全4サイトで HTTP 400(署名なしリクエストの正常な拒否)を確認でき、Checkout ボタンのクリックテストでも Stripe Checkout ページへの正常なリダイレクトを確認しました。

Cloudflare Workers + Next.js で Stripe Webhook を実装する際の注意点

今回の障害から得られた教訓をまとめます。

export const runtime = "edge" は慎重に

Cloudflare Workers(OpenNext)環境では、すべてのルートがデフォルトで Edge ランタイムで実行されます。export const runtime = "edge" を明示すると、バンドル方法が変わり予期しないクラッシュが発生する場合があります。他の API ルート(Checkout など)と揃えるのが安全です。

Stripe SDK は constructEventAsync を使う

Edge ランタイム(Cloudflare Workers, Vercel Edge Functions など)では、Node.js の crypto モジュールが使えません。Stripe SDK v10 以降で利用可能な constructEventAsync を使いましょう。

Webhook ハンドラーは「必ず 200 を返す」設計にする

Webhook ハンドラーが 500 を返し続けると、Stripe はリトライを繰り返し、最終的にエンドポイントを無効化します。ビジネスロジックでエラーが起きても、トップレベルの try-catch で HTTP 200 を返す設計にしましょう。内部エラーのログは別途モニタリングツールで収集します。

まとめ

Stripe Webhook の HTTP 500 エラーは、Cloudflare Workers 環境固有の問題(Edge ランタイムの宣言の競合と、同期版 API の使用)が原因でした。Claude in Chrome を使えば、コードの調査・修正・デプロイ・検証のすべてを一連の作業として効率的に完了できました。

同じ技術スタック(Next.js + Cloudflare Workers + Stripe)でサービスを運営されている方の同じ課題に取り組んでいる方の参考になれば幸いです。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

◈ Cowork2026-05-06
Claude in Chrome が思い通りに動かないときのトラブルシューティング
Claude in Chrome が止まる・クリックが効かない・結果が期待と違う。症状別の対処に加え、待機指示と検証ステップで完走率が62%から95%に変わった42回分の実測と、起動前チェック用のスクリプトをまとめました。
◈ Cowork2026-10-15
一覧には見えているのに、名前で探すと0件——日本語ファイル名の「濁点が分かれている」問題を数えました
Cowork に渡した作業フォルダで、日本語名のファイルが名前検索に当たらない。約1.9万件を数えると、濁点・半濁点が分かれた名前が17件ありました。数え方と直し方を書き残します。
◈ Cowork2026-10-14
Cowork に渡したフォルダの先頭に、十行の約束ファイルを置いてみました
Cowork に頼むたびに同じ注意を書き直していた私が、フォルダの先頭に CLAUDE.md を一枚置いて落ち着いた手順の記録です。書く三つの項目、読まれているかの確かめ方、書きすぎたときの戻し方まで、非エンジニアの方でも追える順番で並べました。