CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/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 Workers14デバッグ3Cowork33

取り組みの背景 — 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. constructEventconstructEventAsync — 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-29
AdMob メディエーションの優先度調整を Claude in Chrome に任せた 3 週間
AdMob メディエーションの優先度を eCPM の変動に合わせて並べ替える作業を、Claude in Chrome に任せて 3 週間運用してみました。閾値の設計・プロンプトで効いた指定・空振り時の挙動・任せられない領域を、実数値とあわせて記録しています。
Cowork2026-05-28
AdMob と Crashlytics の朝の点検を Cowork のスケジュールタスクに統合して 2 週間
AdMob のフィルレートと Crashlytics の急増を別々に朝のうちに見ていた運用を、Cowork のスケジュールタスクで一つの朝報にまとめました。2 週間続けてみて、検知率や対処までの時間がどう変わったかを数値とあわせて記録しています。
Cowork2026-05-06
Claude in Chrome が思い通りに動かないときのトラブルシューティング
Claude in Chrome が止まる・クリックが効かない・結果が期待と違う。症状別の対処に加え、待機指示と検証ステップで完走率が62%から95%に変わった42回分の実測と、起動前チェック用のスクリプトをまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →