thirdweb-dev / js

Best in class web3 SDKs for Browser, Node and Mobile apps
https://thirdweb.com
Apache License 2.0
454 stars 371 forks source link

Webhook empty body #5057

Open DawidWraga opened 1 month ago

DawidWraga commented 1 month ago

I am following the docs and example codebase but the webhook is not working as intended.

doc: https://portal.thirdweb.com/connect/pay/webhooks example: https://github.com/thirdweb-dev/direct-payments-example/blob/main/src/app/api/route.ts

When I use req.json() (similar to the next.js example) the API throws right away.

When I use req.text() (similar to the node.js example) like in the example below, it shows that the request body is empty and then throws when I try to use JSON.parse()

The webhook is correctly calling this handler and passing the correct headers but it seems like it's not passing any data in the request body.

I have tried to debug this for hours, I'd really appreciate some help.

Thank you

next.js webhook route handler:

import { NextResponse, NextRequest } from 'next/server';
import crypto from 'crypto';
import { publicAction } from '@/lib/action';
import { z } from 'zod';
import { prisma } from '@/lib/db';
import { env } from '@/env';

const WEBHOOK_SECRET = env.PAY_WEBHOOK_SECRET;

export async function POST(req: NextRequest) {
    try {
        const signatureFromHeader = req.headers.get('X-Pay-Signature');
        const timestampFromHeader = req.headers.get('X-Pay-Timestamp');

        if (!signatureFromHeader || !timestampFromHeader) {
            return NextResponse.json(
                { error: 'Missing signature or timestamp header' },
                { status: 400 }
            );
        }

        const rawBody = await req.text();

        if (
            !isValidSignature(
                rawBody,
                timestampFromHeader,
                signatureFromHeader,
                WEBHOOK_SECRET
            )
        ) {
            return NextResponse.json({ error: 'Invalid signature' }, { status: 401 });
        }

        if (isExpired(timestampFromHeader, 300)) {
            // 5 minutes expiration
            return NextResponse.json(
                { error: 'Request has expired' },
                { status: 380 }
            );
        }

        if (!rawBody) {
            console.log('Request body is empty');
            return NextResponse.json(
                { error: 'Empty request body' },
                { status: 400 }
            );
        }

        let body: any;
        try {
            body = JSON.parse(rawBody);
        } catch (parseError) {
            console.error('Failed to parse JSON:', parseError);
            return NextResponse.json(
                { error: 'Invalid JSON in request body', rawBody },
                { status: 400 }
            );
        }

        const { data } = body;

        const cryptoPaymentCompleted =
            data.buyWithCryptoStatus &&
            data.buyWithCryptoStatus.status === 'COMPLETED';

        if (!cryptoPaymentCompleted) {
            console.error('Crypto payment not completed');
            return NextResponse.json(
                { error: 'Crypto payment not completed' },
                { status: 401 }
            );
        }

        const {
            purchaseData: { amount, userId },
        } = data.buyWithCryptoStatus;

        console.log('CHECKPOINT 8', {
            amount,
            userId,
        });

        const creditAfterPurchase = await incrementCredits({
            amount: amount,
            userId: userId,
        });

        return NextResponse.json({
            message: 'Purchase processed successfully',
            credits: creditAfterPurchase,
        });
    } catch (error) {
        console.error('Error handling webhook:', error);
        return NextResponse.json(
            { error: 'Error processing webhook' },
            { status: 500 }
        );
    }
}

Pay embed UI configuration:

<PayEmbed
    client={client}
    payOptions={{
        mode: 'direct_payment',
        onPurchaseSuccess(info) {
            wait(3).then(() => {
                           queryClient?.invalidateQueries({ queryKey: ['credits'] });
            });
        },

        buyWithCrypto: {
            testMode: false,
            prefillSource: {
                allowEdits: {
                    chain: true,
                    token: false,
                },
                chain: ethereum,
                token: ethToken,
            },
        },
        purchaseData: {
            amount: selectedPackage.credits,
            userId: userId,
        },

        paymentInfo: {
            amount: ethAmount,
            chain: ethereum,
            token: ethToken,
            sellerAddress: '0x5e1a530F7C80509E3Fd2B2AD215dcd645350f502',
        },
        metadata: {
            name: `${selectedPackage.credits} Credits`,
        },
    }}
/>
joaquim-verges commented 2 weeks ago

Hey @DawidWraga sorry for the late reply here - we're investigating this issue

github-actions[bot] commented 1 day ago

This issue has been inactive for 30 days. It is now marked as stale and will be closed in 5 days if no further activity occurs.