CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/Cowork
Cowork/2026-03-25Intermediate

How We Fixed Stripe Webhook HTTP 500 Errors Using Claude in Chrome — A

A step-by-step account of diagnosing and fixing Stripe Webhook HTTP 500 errors across four sites using Claude in Chrome. Covers Cloudflare Workers + Next.js pitfalls, the constructEventAsync fix, and live browser-based verification.

Claude in Chrome14Stripe15Webhook3Cloudflare Workers14DebuggingCowork33

The Problem — A Wave of Stripe Webhook Failures

One morning, we received identical warning emails from Stripe across all four of our sites: "We've had a problem sending webhook events to your endpoint." Every webhook endpoint was returning HTTP 500, with 34 failed retry attempts and counting.

When you run subscription-based services, webhook failures are a revenue emergency. Left unresolved, invoice processing can be delayed by up to three days, and Stripe will eventually disable the endpoint entirely.

In this article, we walk through how we used Claude in Chrome — the browser automation feature of Cowork — to diagnose the root cause, fix the code across all four sites, deploy the changes, and verify everything was working in production, all within a single session.

Our Stack

Here's the environment where the issue occurred:

  • Framework: Next.js 16 (App Router)
  • Hosting: Cloudflare Workers (via OpenNext adapter)
  • Payments: Stripe (Webhook + Checkout)
  • Storage: Cloudflare KV (premium access management)
  • Language: TypeScript
  • Affected sites: Claude Lab, Gemini Lab, Antigravity Lab, Rork Lab

Root Cause Analysis with Claude in Chrome

Using Claude in Chrome, we performed a cross-repository analysis of all four sites' webhook implementations at src/app/api/webhook/route.ts. A side-by-side comparison quickly revealed the issues.

Issue 1: Explicit Edge Runtime Declaration

// ❌ The problematic code
export const runtime = "edge";
 
export async function POST(request: NextRequest) {
  // ...
}

The webhook route explicitly declared export const runtime = "edge", while the checkout route — which was working perfectly — had no such declaration.

On Cloudflare Workers with OpenNext, all routes run in the Workers (Edge) runtime by default. Adding export const runtime = "edge" changes how OpenNext bundles the route, which caused the entire Worker to crash on initialization.

Issue 2: Synchronous constructEvent in Edge Runtime

// ❌ Synchronous — depends on Node.js crypto module
event = stripe.webhooks.constructEvent(body, sig, secret);
 
// ✅ Asynchronous — uses Web Crypto API (Edge compatible)
event = await stripe.webhooks.constructEventAsync(body, sig, secret);

The Stripe SDK's constructEvent() is a synchronous method that relies on Node.js crypto.createHmac. In Edge runtimes where only the Web Crypto API is available, constructEventAsync() must be used instead.

Issue 3: Missing Error Handling for KV Operations

KV (Key-Value storage) read/write operations weren't wrapped in try-catch blocks. Any KV-side errors would surface as unhandled exceptions, resulting in HTTP 500 responses.

The Fix

We applied three changes across all four sites:

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>;
}
 
// ✅ Removed: export const runtime = "edge"
// On Cloudflare Workers, all routes are Edge by default
 
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 {
      // ✅ Use constructEventAsync for 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 }
      );
    }
 
    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" });
    }
 
    // ✅ Wrap KV operations in 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 {
    // Top-level catch prevents unhandled errors from returning 500
    return NextResponse.json({ received: true, note: "Internal error" });
  }
}

Key changes:

  1. Removed export const runtime = "edge" — Let OpenNext handle bundling naturally
  2. constructEventconstructEventAsync — Web Crypto API compatible
  3. Wrapped KV operations in try-catch — Graceful handling of storage errors
  4. Added top-level try-catch — Always return HTTP 200 to Stripe, even on unexpected errors

Live Verification with Claude in Chrome

After pushing the fixes to all four sites, we used Claude in Chrome to verify the endpoints in real-time. By executing fetch calls directly in the browser context of each site, we could confirm the API responses without any additional tooling.

// Verification code executed by Claude in Chrome
const res = await fetch('/api/webhook', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: '{}'
});
console.log(res.status, await res.text());
// Before fix: 500 "Internal Server Error"
// After fix:  400 {"error":"Missing signature or secret"}

All four sites returned HTTP 400 (correct rejection of unsigned requests), and clicking the checkout buttons on each site's support page confirmed successful redirects to Stripe Checkout.

Lessons Learned for Cloudflare Workers + Next.js + Stripe

Be careful with export const runtime = "edge"

On Cloudflare Workers with OpenNext, all routes already run in Edge runtime. Explicitly declaring export const runtime = "edge" can change the bundling behavior and cause crashes. Keep your API routes consistent — if your checkout route works without it, your webhook route shouldn't need it either.

Always use constructEventAsync in Edge environments

Any Edge runtime — Cloudflare Workers, Vercel Edge Functions, Deno — lacks the Node.js crypto module. The Stripe SDK's constructEventAsync (available since v10) uses the Web Crypto API and works everywhere.

Design webhook handlers to always return 200

When a webhook handler returns 500 repeatedly, Stripe retries aggressively and eventually disables the endpoint. Design your handler with a top-level try-catch that always returns HTTP 200. Log internal errors separately through your monitoring infrastructure.

Wrapping Up

The Stripe Webhook HTTP 500 errors were caused by a combination of Cloudflare Workers-specific issues: a conflicting Edge runtime declaration and the use of a synchronous API that depends on Node.js crypto. Using Claude in Chrome, we were able to diagnose, fix, deploy, and verify the solution across all four sites efficiently.

If you're running a similar stack — Next.js on Cloudflare Workers with Stripe — we hope this guide helps you avoid the same pitfalls.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Cowork2026-05-29
Three Weeks of Letting Claude in Chrome Tune My AdMob Mediation Priorities
I let Claude in Chrome handle the reordering of AdMob mediation priorities for three weeks. Here is how I set the threshold, the three prompt changes that actually mattered, the numbers, and the parts I deliberately kept under human control.
Cowork2026-05-28
Folding AdMob and Crashlytics into One Morning Check via Cowork Scheduled Tasks — Two-Week Notes
I had been checking AdMob fill rates and Crashlytics surges in two separate dashboards each morning. I folded them into a single Cowork scheduled task. Here are my two-week notes, with the numbers and the friction I ran into.
Cowork2026-05-06
Claude in Chrome Not Working? A Practical Troubleshooting Guide
Buttons that do nothing, pages that never load, tasks that report success but change nothing. Symptom-by-symptom fixes, plus the 42-run log where adding waits and verification steps moved completion from 62% to 95%.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →