◉CLAUDE LABJP
●2.1.289 — Claude Code 2.1.289 (Oct 3) fixes deny/ask rules on nested parts of compound shell commands, a terminal freeze, and Read deny through symlinks●10/07 — 1 day left until the old spellings of Claude Desktop / Cowork managed-config keys stop being accepted. After the cutoff they fail closed●ACCOUNTS — A request to switch between multiple accounts in Claude Desktop has gathered over 200 comments on GitHub. How far today's workarounds go is the real question●NEW — When Claude Code committed despite a no-commit instruction, I separated the cause with a deny rule and a PreToolUse hook●COMPARE — A Zenn post compares Opus 5.5 and Sonnet 5.5 with numbers. The real question is where to draw the line per job●SONNET5.5 — Code written for Sonnet 5 can break on 5.5 in five ways. Thinking settings and forced tool_choice are the places to check first●2.1.289 — Claude Code 2.1.289 (Oct 3) fixes deny/ask rules on nested parts of compound shell commands, a terminal freeze, and Read deny through symlinks●10/07 — 1 day left until the old spellings of Claude Desktop / Cowork managed-config keys stop being accepted. After the cutoff they fail closed●ACCOUNTS — A request to switch between multiple accounts in Claude Desktop has gathered over 200 comments on GitHub. How far today's workarounds go is the real question●NEW — When Claude Code committed despite a no-commit instruction, I separated the cause with a deny rule and a PreToolUse hook●COMPARE — A Zenn post compares Opus 5.5 and Sonnet 5.5 with numbers. The real question is where to draw the line per job●SONNET5.5 — Code written for Sonnet 5 can break on 5.5 in five ways. Thinking settings and forced tool_choice are the places to check first
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 Workers16DebuggingCowork43

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. constructEvent → constructEventAsync — 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 $15 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-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%.
◈ Cowork2026-10-01
Before You Trim a Scheduled Task's Runbook, Count How Many Times It Gets Re-read
I cut 13 KB from a scheduled task's preloaded docs, and recounted by re-reads it was worth about 1.3 tool calls. Here is the ledger that counts re-reads, and a shell helper that records bytes during a run.
◈ Cowork2026-09-24
Will Your Cowork Scheduled Task Still Run Next Saturday?
A definition that runs once and stops, two fire times packed into one task, a weekday that quietly emptied after an edit. Cowork schedules can go missing without leaving a single log line. Here is a working audit that compares what you defined with what you meant.
📚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