Ak dostávate 400 Bad Request s hláškou “Invalid signature” zo Stripe webhooku na Cloudflare Workers alebo Cloudflare Pages, skutočný problém nemusí byť váš tajný kľúč webhooku.

Príznak #

Endpoint Stripe webhooku vracia:

400 Bad Request
Invalid signature

V Stripe Dashboard pod Developers → Webhooks doručenie zobrazuje:

400 ERR
Bad Request
Response body: Invalid signature

STRIPE_WEBHOOK_SECRET som skontroloval trikrát. Zregeneroval som ho. Znova som nasadil. Stále zlyháva.

Zavádzajúca chyba #

Štandardný kód na overenie Stripe webhooku vyzerá takto:

try {
  event = stripe.webhooks.constructEvent(body, signature, webhookSecret)
} catch (err) {
  console.error("Webhook signature verification failed:", err)
  return new Response("Invalid signature", { status: 400 })
}

Problém je v tom, že chyba je zachytená a vrátené je “Invalid signature” bez toho, aby sme sa pozreli na to, aká je skutočná chyba.

Skutočná chyba #

Keď som pridal správne logovanie a skontroloval real-time logy Cloudflare:

wrangler pages deployment tail --project=your-project

Videl som toto:

(error) Webhook signature verification failed: Error: SubtleCryptoProvider cannot be used in a synchronous context.
Use `await constructEventAsync(...)` instead of `constructEvent(...)`

Podpis bol v poriadku. Problémom bol crypto provider.

Prečo sa to deje #

Cloudflare Workers používa Web Crypto API (SubtleCrypto), ktoré je len asynchrónne. Metóda constructEvent() Stripe SDK sa pokúša použiť crypto synchrónne, čo v prostredí Workers zlyháva.

Stripe SDK vám vlastne v chybovej správe presne hovorí, čo robiť, ale ak chybu zachytávate a ignorujete (ako väčšina ukážkových kódov), nikdy ju neuvidíte.

Oprava #

Zmeňte zo synchrónnej verzie:

event = stripe.webhooks.constructEvent(body, signature, webhookSecret)

Na asynchrónnu:

event = await stripe.webhooks.constructEventAsync(
  body,
  signature,
  webhookSecret
)

To je všetko. Zmena jedného slova: constructEventconstructEventAsync (a pridanie await).

Kompletný funkčný príklad #

import type { APIRoute } from "astro"
import Stripe from "stripe"

export const POST: APIRoute = async ({ request, locals }) => {
  const stripe = new Stripe(locals.runtime.env.STRIPE_SECRET_KEY)
  const webhookSecret = locals.runtime.env.STRIPE_WEBHOOK_SECRET

  const body = await request.text()
  const signature = request.headers.get("stripe-signature")

  if (!signature) {
    return new Response("Missing signature", { status: 400 })
  }

  let event
  try {
    // Use constructEventAsync for Cloudflare Workers/Pages
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature,
      webhookSecret
    )
  } catch (err) {
    console.error("Webhook verification failed:", err)
    return new Response("Invalid signature", { status: 400 })
  }

  // Handle the event
  if (event.type === "checkout.session.completed") {
    const session = event.data.object
    // Process the checkout...
  }

  return new Response("OK", { status: 200 })
}

Tipy na ladenie #

Ak stále máte problémy, pridajte dočasné logovanie, aby ste videli, čo sa skutočne deje:

console.log("Webhook debug:", {
  hasSecret: !!webhookSecret,
  secretPrefix: webhookSecret?.substring(0, 10),
  hasSignature: !!signature,
  bodyLength: body.length,
})

Potom skontrolujte logy pomocou:

wrangler pages deployment tail --project=your-project

Zhrnutie #

  • Chyba: Invalid signature / 400 Bad Request
  • Skutočná príčina: SubtleCryptoProvider cannot be used in a synchronous context
  • Oprava: Použiť constructEventAsync() namiesto constructEvent()
  • Platí pre: Cloudflare Workers, Cloudflare Pages a akýkoľvek edge runtime používajúci Web Crypto API

Pri tomto riešení som sa naučil, ako ladiť takéto situácie. Je to podobné ako console.log v prehliadači. Nie je to absolútne najlepší spôsob, ale prácu odvede. Enjoy!

Odkazy #