Stripe Webhook & Inngest Error Handling
Stripe Webhook & Inngest Error Handling
Release: v1.0.397
The Problem
The Stripe webhook endpoint (/api/webhooks/stripe) previously only wrapped the constructEvent() signature verification step in a try/catch. Everything that followed — retrieving the customer record, dispatching events via inngest.send(), and constructing the HTTP response — ran outside any error boundary.
This created two failure modes:
- Unstructured 500 responses. If Inngest was unavailable or the Stripe payload had an unexpected shape, Next.js would throw a runtime exception and return a raw 500 with no structured body. Stripe treats any non-2xx response as a delivery failure.
- Webhook retries and duplicate events. Because Stripe retries failed deliveries, an infrastructure blip (e.g. a momentary Inngest outage) could trigger the same webhook multiple times, risking duplicate downstream effects such as double-counted payments or repeated subscription activations.
The /api/inngest route similarly used the Inngest serve() adapter with no outer error wrapper, leaving adapter-level failures unhandled.
The Fix
Outer try/catch on the full POST handler
The entire POST handler body in src/app/api/webhooks/stripe/route.ts is now wrapped in a try/catch:
export async function POST(req: Request) {
try {
// signature verification, customer retrieval, inngest.send(), ...
return new Response('OK', { status: 200 });
} catch (err) {
return new Response('Internal error', { status: 500 });
}
}
This guarantees that every code path through the handler returns a well-formed HTTP response.
Isolated try/catch for inngest.send()
The inngest.send() call uses its own nested try/catch with different semantics: on failure it still returns HTTP 200 to Stripe while reporting the error to Sentry:
try {
await inngest.send({ name: 'stripe/event', data: event });
} catch (err) {
captureError(err); // reports to Sentry
// intentionally returns 200 — do NOT let Stripe retry
return new Response('OK', { status: 200 });
}
Returning 200 here is intentional. Inngest failures are an internal concern; they should be logged and investigated, not surfaced to Stripe as a reason to re-deliver the webhook.
Inngest route wrapper
The src/app/api/inngest/route.ts adapter now has an outer error wrapper to handle unexpected failures at the serve() adapter level, preventing unhandled exceptions from escaping to the runtime.
Behaviour Summary
| Failure scenario | Before v1.0.397 | After v1.0.397 |
|---|---|---|
Inngest unavailable during inngest.send() | Raw 500 → Stripe retries → potential duplicates | Returns 200, logs to Sentry, no retry |
| Unexpected Stripe payload shape | Raw 500 → Stripe retries | Returns 500 (structured), Stripe may retry once |
| Customer retrieval throws | Unhandled runtime crash | Caught by outer handler, returns 500 |
| Inngest adapter error | Unhandled runtime crash | Caught by outer wrapper |
Monitoring
Inngest send failures are forwarded to Sentry via captureError(). Look for errors tagged with the Stripe webhook source in your Sentry project to identify Inngest delivery problems without needing to cross-reference Stripe's webhook dashboard.