I have an endpoint in my SvelteKit app which handles webhook requests from Stripe. Each request is signed so that it can be verified to come from Stripe.
The code I have to verify the event is from Stripe looks something like this:
import Stripe from "stripe";
const WEBHOOK_SECRET = process.env["STRIPE_WH_SECRET"];
const stripe = new Stripe(process.env["STRIPE_SECRET"], {
apiVersion: "2020-08-27",
});
export async function post({ headers, body }) {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
body,
headers["stripe-signature"],
WEBHOOK_SECRET
);
} catch (err) {
return {
status: 400,
body: err,
};
}
// Do stuff with the event
}
But when it receives an event from Stripe, I get this error:
No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? https://github.com/stripe/stripe-node#webhook-signing
After some research, I found that the body is parsed into JSON by this function before even SvelteKit hooks are called, meaning there's no way to directly get the raw body, so I decided my best option was to try to reconstruct the original body:
event = stripe.webhooks.constructEvent(
JSON.stringify(body),
headers["stripe-signature"],
WH_SECRET
);
I'm not totally certain why this doesn't work, since after digging around in the relevant code in the Stripe library, it seems to handle strings fine. My best guess is that at some point the encoding gets messed up.
Any help with this would be greatly appreciated, as I'd really like to avoid switching away from SvelteKit, since I've already practically finished my project with it (wasn't a great idea, in retrospect).
The payload accepted by stripe.webhooks.constructEvent(payload, signature, secret) must be of type string | Buffer, but the rawBody received in the SvelteKit request is of type Uint8Array.
Passing the Uint8Array rawBody or a JSON.stringify'd rawBody as the payload results in the error below from Stripe. Refer: https://github.com/stripe/stripe-node#webhook-signing
Error
message: 'No signatures found matching the expected signature for payload. Are you passing the raw request body you received from Stripe? https://github.com/stripe/stripe-node#webhook-signing'
We need to convert the rawBody into a string or Buffer without altering the contents. To achieve this we can use: Buffer.from(rawBody).
So your endpoint would look something like:
...
const stripeWebhookSecret = process.env['STRIPE_WEBHOOK_SECRET'];
export const post: RequestHandler = async (request) => {
const rawBody = Buffer.from(request.rawBody);
const signature = request.headers['stripe-signature'];
try {
event = stripe.webhooks.constructEvent(
rawBody,
signature,
stripeWebhookSecret
);
...
The request now comes with an arrayBuffer the can be converted into raw body like this:
function toBuffer(ab: any) {
const buf = Buffer.alloc(ab.byteLength);
const view = new Uint8Array(ab);
for (let i = 0; i < buf.length; ++i) {
buf[i] = view[i];
}
return buf;
}
export async function post(event: RequestEvent<Record<string, string>>) {
...
const preRawBody = await event.request.arrayBuffer();
const rawBody = toBuffer(preRawBody);
try {
stripeEvent = stripe.webhooks.constructEvent(
rawBody,
stripeSignature,
process.env.STRIPE_WEBHOOK_SECRET
);
} ...
}