Sandro Nolasco
Sandro Nolasco

Reputation: 11

ElysiaJS Bun - Stripe Webhooks

How i can handle the stripe webhook with ElysiaJS?

i have been trying for a long time and dont get results

im here, the body is parsed and i received an string from the reqText

the contructEvent returns this error Webhook payload must be provided as a string or a Buffer (https://nodejs.org/api/buffer.html) instance representing the raw request body.Payload was provided as a parsed JavaScript object instead. Signature verification is impossible without access to the original signed material.

Learn more about webhook signing and explore webhook integration examples for various frameworks at https://github.com/stripe/stripe-node#webhook-signing

import { Elysia, ParseError, t } from 'elysia'
import { env } from '@/config/env.config'
import { stripeClient } from '@/lib/stripe.lib'

export const stripeWebhook = new Elysia().post(
  '/integrations/stripe/webhook',
  async () => {},
  {
    headers: t.Object({
      'stripe-signature': t.String(),
      'content-type': t.String(),
    }),
    async parse({ headers, request }) {
      if (headers['content-type'] === 'application/json; charset=utf-8') {
        const reqText = await request.text()
        return webhookHandler(reqText, request)
      } else {
        throw new ParseError('Invalid content type')
      }
    },
    body: t.Not(t.Undefined()),
  },
)

const webhookHandler = async (reqText: string, request: Request) => {
  const endpointSecret = env.STRIPE_WEBHOOK_SECRET
  const signature = request.headers.get('stripe-signature')

  if (!signature) {
    console.error('Stripe signature is missing')
    return {
      statusCode: 400,
      body: JSON.stringify({ error: 'Stripe signature is missing' }),
    }
  }

  if (!stripeClient) {
    console.error('Stripe client is not initialized')
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Stripe client is not initialized' }),
    }
  }

  try {
    const event = await stripeClient.webhooks.constructEventAsync(
      reqText,
      signature,
      endpointSecret,
    )

    if (event.type === 'payment_intent.succeeded') {
      console.log(event.object)
      console.log('PaymentIntent was successful!')
    }

    return {
      statusCode: 200,
      body: JSON.stringify({ received: true }),
    }
  } catch (error) {
    console.error(error)
    return {
      statusCode: 400,
      body: `Webhook Error: ${error}`,
    }
  }
}

i want make it works properly

Upvotes: 1

Views: 606

Answers (5)

Salamander
Salamander

Reputation: 1

All you need to do is to put .onParse() in front of the post request.

Since Stripe need raw data but it seem like Elysia just parse it into JSON object.

So I just put this

.onParse(...)

in front of webhook

and it seem work.

This is not my code I stole it from reddit hahaha.

example.

.onParse(async ({ request, headers }) => {
    if (headers["content-type"] === "application/json; charset=utf-8") {
      const arrayBuffer = await Bun.readableStreamToArrayBuffer(request.body!);
      const rawBody = Buffer.from(arrayBuffer);
      return rawBody
    }
  })
.post('/webhook', async ( { body, headers, request } ) => {
    const signature = headers['stripe-signature'];
    // try {

    const event = await stripe.webhooks.constructEventAsync(
        body,
        signature,
        endpointSecret
    );

    console.log( event );
      return {
        status: 'success',
      };

credit: get raw data to stripe

Upvotes: 0

robo-monk
robo-monk

Reputation: 131

Elysia provides the raw body in the request's array buffer.


import { Elysia } from "elysia";
import assert from "assert";
import Stripe from "stripe";


const stripe = new Stripe("sk_...");


const app = new Elysia()
  .post("/stripe", async ({ request }) => {
    const signature = request.headers.get("Stripe-Signature");
    assert(signature != null, "Stripe Signature is needed");

    const body = await request.arrayBuffer();
    const event = await stripe.webhooks.constructEventAsync(
      Buffer.from(body),
      signature,
      "whsec_...", // replace with yours
    );

    console.log("event type is", event.type);
    return "ok";
  })
  .onError(({ error }) => {
    console.error(error);
    return new Response("Internal Server Error", { status: 500 });
  })

Upvotes: 0

just add type: 'arrayBuffer'

and then when you want to pass the body:

Buffer.from(body as ArrayBuffer)

The result would be something like this:

.post(
    '/webhook',
    async ({ body, headers }) => {
        let event;

        // Verify webhook signature and extract the event.
        try {
            const endpointSecret = // your secret
            const sig = headers['stripe-signature'];
            event = await stripe.webhooks.constructEventAsync(Buffer.from(body as ArrayBuffer), sig as any, endpointSecret as any);
        } catch (err) {
            throw new Error(`Webhook Error: ${err}`);
        }

        // Handle the event

        return { received: true };
    },
    {
        type: 'arrayBuffer'
    }
);

Upvotes: -1

Dusty Phillips
Dusty Phillips

Reputation: 161

I struggled with this for a long time. request.body is a ReadableStream, so not exactly what the stripe webhook wants. The trick is to override the parse handler to read the raw body instead of doing :

new Elysia()
  .post(
    "/stripe/webhook",
    async ({
      request,
      body,
      error,
      headers: { "stripe-signature": stripeSignature },
    }) => {
      if (!request.body) {
        error(400);
        return;
      }

      return handlerThatCallsConstructEvent(
        body as Buffer, stripeSignature, error);
    },
    {
      headers: t.Object({ "stripe-signature": t.String() }),
      async parse(ctx) {
        if (ctx.request.body === null) {
          return;
        }
        return Buffer.from(
          await Bun.readableStreamToArrayBuffer(ctx.request.body),
        );
      },
    },
  ),

That's kind of hard to read. the .post function takes three parameters:

  • First param is the path
  • Second is the handler function. It will be called with a raw body now.
  • Third is the config. It has two fields:
    • headers: Validate that stripe-signature is provided
    • async parse(ctx) -> reads the raw body as a buffer and returns it with no extra parsing.

Upvotes: -1

qichuan
qichuan

Reputation: 2041

The important thing here is pass the raw request body to the constructEventAsync function. Therefore, I'd suggest changing request.text() to request.body. You can find the full example here

Upvotes: -2

Related Questions