Josip Domazet
Josip Domazet

Reputation: 2890

SvelteKit Stripe webhook handler not completing async operation - How to ensure execution? (Issue caused by Cloudflare)

I'm encountering an issue with a Stripe webhook handler in my SvelteKit project. The handler processes a checkout session and should store the customer ID in Supabase, but it appears the operation is being terminated before completion. Here's the relevant code:

export const storeStripeCustomerId = async (session: Stripe.Checkout.Session) => {
    let customerId: string = '';
    if (typeof session.customer === 'string') {
        customerId = session.customer;
    } else if (session.customer !== null) {
        customerId = session.customer.id;
    }
    console.log("Storing Stripe customer ID", customerId, "for session", session.id);
    if (customerId) {
        // This being logged
        console.log("session.client_reference_id", session.client_reference_id);
        const {error} = await SUPABASE_SERVICE_ROLE.schema('stripe').from('user_id_table').upsert({
            id: session.client_reference_id,
            stripe_customerid: customerId,
            stripe_email: session.customer_details?.email
        });
    

        // This is not logged anymore
        console.log("Test");
        console.log(error);
      
        if (error) {
            console.error(error);
            errorReturn(400, "Supabase: " + error.message);
        }
    }
}
// /api/stripe/webhook
...
case 'checkout.session.completed': {
    const session: Stripe.Checkout.Session = event.data.object
    storeStripeCustomerId(session);
    break;
}

The problem is that the logs after the Supabase upsert operation don't appear, suggesting that the function execution is being cut off. However, I can't await it as I need to send a quick response back to Stripe.

I've verified that the environment variables for Supabase are correct and the webhook itself works properly (gets called by stripe). How can I ensure that the storeStripeCustomerId function completes its execution, even if the main handler has already responded? Additional context:

Any suggestions on how to properly handle this asynchronous operation in a SvelteKit context would be greatly appreciated.

Upvotes: 1

Views: 101

Answers (2)

Josip Domazet
Josip Domazet

Reputation: 2890

Turns out Cloudflare is terminating the worker for the webhook before the async function finishes, since the webhook already returned a 200 answer. This also explains why it worked locally.

I resolved it by using the waitUntil function from Cloudflare. This function prevents the worker from terminating prematurely, allowing asynchronous operations to complete even after sending the HTTP response (note that it's still async and not awaiting anything). Here’s how I integrated waitUntil into my webhook handler:

// Webhook
const processEvent = async () => {
    switch (event.type) {
        case 'entitlements.active_entitlement_summary.updated':
            await handleEntitlement(event.data.object.customer);
            console.log('Entitlements updated successfully');
            break;
        case 'checkout.session.completed':
            await storeStripeCustomerId(event.data.object);
            console.log('Customer ID stored successfully');
            break;
    }
};
// We cannot await processEvent() directly, because we need to return a quick response to Stripe
if (platform && platform.context && platform.context.waitUntil) {
    // On Cloudflare, using waitUntil to ensure the Cloudflare worker isn't killed
    platform.context.waitUntil(processEvent());
} else {
    // Locally or on platforms without waitUntil, running the async function directly
    processEvent().catch(error => {
        console.error('Failed to process event in background:', error);
    });
}

You have to extend your app configuration like this to make the platform available.

interface Platform {
    env: {
        COUNTER: DurableObjectNamespace;
    };
    context: {
        waitUntil(promise: Promise<any>): void;
    };
    caches: CacheStorage & { default: Cache }
}

Here is the official Cloudflare documentation that talks about how to access waitUntil in Svelte.

Upvotes: 0

Nolan H
Nolan H

Reputation: 7459

I can't speak to the issues with the Supabase upsert, but in terms of this:

However, I can't await it as I need to send a quick response back to Stripe.

You should either:

  1. respond to the Stripe webhook delivery request first then perform your slower processing, or
  2. enqueue the event without processing, respond to the Stripe delivery request, then process that queue async.

Note the Stripe docs say:

Your endpoint must quickly return a successful status code (2xx) prior to any complex logic that could cause a timeout. For example, you must return a 200 response before updating a customer’s invoice as paid in your accounting system.

The 200/success response is to indicate successful delivery only, and should not be used as a proxy for successful processing.

Upvotes: 1

Related Questions