Clerk and Svix web hooks not working with error: "src property must be a valid json object"
Asked Answered
C

2

5

I'm trying to sync my Clerk data to my database in my Next js 13 project. My webhooks are publicly exposed with Ngrok. Here's my code:

import { IncomingHttpHeaders } from "http";
import { headers } from "next/headers";
import { NextResponse } from "next/server";
import { Webhook, WebhookRequiredHeaders } from "svix";

const webhookSecret = process.env.WEBHOOK_SECRET || "";

async function handler(request: Request) {

  console.log(await request.json())

  const payload = await request.json();
  const headersList = headers();
  const heads = {
    "svix-id": headersList.get("svix-id"),
    "svix-timestamp": headersList.get("svix-timestamp"),
    "svix-signature": headersList.get("svix-signature"),
  };
  const wh = new Webhook(webhookSecret);
  let evt: Event | null = null;

  try {
    evt = wh.verify(
      JSON.stringify(payload),
      heads as IncomingHttpHeaders & WebhookRequiredHeaders
    ) as Event;
  } catch (err) {
    console.error((err as Error).message);
    return NextResponse.json({}, { status: 400 });
  }

  const eventType: EventType = evt.type;
  if (eventType === "user.created" || eventType === "user.updated") {
    const { id, ...attributes } = evt.data;
    console.log(attributes)
  }
}

type EventType = "user.created" | "user.updated" | "*";

type Event = {
  data: Record<string, string | number>;
  object: "event";
  type: EventType;
};

export const GET = handler;
export const POST = handler;
export const PUT = handler;

This code should do the following:

  1. Create an API route under /api/webhooks/user
  2. Fetch the payload and headers
  3. Validate this info with Svix
  4. Console the information

However, only step 1 is working as far as I can tell. In my Clerk dashboard I get an error:

{
  "message": "src property must be a valid json object"
}

Edit:

With the following code I'm still getting the same error:

import { Webhook, WebhookRequiredHeaders } from "svix";

const webhookSecret = process.env.WEBHOOK_SECRET || "";

async function handler(request: Request) {
    const svix_id = request.headers.get("svix-id") ?? "";
    const svix_timestamp = request.headers.get("svix-timestamp") ?? "";
    const svix_signature = request.headers.get("svix-signature") ?? "";

    const body = await request.text(); // This get's the raw body as a string

    const sivx = new Webhook("your_secret_key_here");

    const payload = sivx.verify(body, {
        "svix-id": svix_id,
        "svix-timestamp": svix_timestamp,
        "svix-signature": svix_signature,
    });

    console.log(payload)
}

export const GET = handler;
export const POST = handler;
export const PUT = handler;

Where am I going wrong?

Conroy answered 27/6, 2023 at 0:11 Comment(0)
A
5

You need to add your webhook endpoint to your ignoredRoutes in middleware.ts:

export default authMiddleware({
  publicRoutes: ['/'],
  ignoredRoutes: ['/api/{routeName}'],
});

There's annoyingly little documentation to highlight this though.

Astrograph answered 11/7, 2023 at 9:55 Comment(1)
it worked. earlier i made a mistake, 'webhooks' vs 'webhook'.Ingrowing
L
1

For people using Hono I made it work with the following code.

app.post('/', async (c) => {
    try {
        const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET
        if (!WEBHOOK_SECRET) {
            throw new Error('You need a WEBHOOK_SECRET in your .env')
        }
        const payload = await c.req.text()
        const svix_id = c.req.header('svix-id')
        const svix_timestamp = c.req.header('svix-timestamp')
        const svix_signature = c.req.header('svix-signature')

        // If there are no Svix headers, error out
        if (!svix_id || !svix_timestamp || !svix_signature) {
            console.error('Issue in headers of webhook')
            return c.text('Error occurred -- no svix headers', {
                status: 400,
            })
        }

        // Create a new Svix instance with your secret.
        const wh = new Webhook(WEBHOOK_SECRET)

        let evt = null

        // Attempt to verify the incoming webhook
        // If successful, the payload will be available from 'evt'
        // If the verification fails, error out and  return error code
        try {
            evt = wh.verify(payload, {
                'svix-id': svix_id,
                'svix-timestamp': svix_timestamp,
                'svix-signature': svix_signature,
            }) as { data: OrganizationMembershipJSON; type: string }
            console.log('evt type: ', evt.type)
            console.log('evt data: ', evt.data?.id)
        } catch (err) {
            if (err instanceof Error) {
                console.error('Error verifying webhook:', err.message)
                return c.text(err.message, 400)
            }
            return c.text('Something went wrong', 500)
        }

        return c.text('Webhook received and verified successfully', 200)
    } catch (e) {
        if (e instanceof Error) {
            return c.json(e.message, 400)
        }
        return c.text('Something went wrong', 500)
    }
})

This is how you would do it in Remix add webhooks.tsx in your route folder

import { OrganizationJSON, OrganizationMembershipJSON, UserJSON } from '@clerk/backend'
import { ActionFunction, json } from '@remix-run/node'
import { Webhook } from 'svix'

export const action: ActionFunction = async ({ request, context }) => {
  try {
    const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET
    if (!WEBHOOK_SECRET) {
      throw new Error('You need a WEBHOOK_SECRET in your .env')
    }
    const payload = await request.text()
    const svix_id = request.headers.get('svix-id')
    const svix_timestamp = request.headers.get('svix-timestamp')
    const svix_signature = request.headers.get('svix-signature')

    console.log('svix_id: ', svix_id)
    console.log('svix_timestamp: ', svix_timestamp)
    console.log('svix_signature: ', svix_signature)
    // If there are no Svix headers, error out
    if (!svix_id || !svix_timestamp || !svix_signature) {
      console.error('Issue in headers of webhook')
      return json({ message: 'Issue in headers of webhook' }, 400)
    }

    // Create a new Svix instance with your secret.
    const wh = new Webhook(WEBHOOK_SECRET)

    let evt = null

    // Attempt to verify the incoming webhook
    // If successful, the payload will be available from 'evt'
    // If the verification fails, error out and  return error code
    try {
      evt = wh.verify(payload, {
        'svix-id': svix_id,
        'svix-timestamp': svix_timestamp,
        'svix-signature': svix_signature,
      }) as {
        data: OrganizationMembershipJSON | OrganizationJSON | UserJSON
        type: string
      }
      console.log('evt type: ', evt.type)
      console.log('evt data: ', evt.data?.id)
    } catch (err) {
      if (err instanceof Error) {
        console.error('Error verifying webhook:', err.message)
        return json({ message: err.message }, 400)
      }
      return json({ message: 'Something went wrong' }, 500)
    }

    const webhookTypesAllowed = [
      'organizationMembership.created',
      'organization.created',
      'organization.updated',
      'organizationMembership.updated',
      'user.updated',
    ]

    if (evt?.type && !webhookTypesAllowed.includes(evt.type)) {
      return json({ message: `Webhook type not supported ${evt.type}` }, 400)
    }

    return json({ message: 'Webhook received and verified successfully in remix' }, 200)
  } catch (e) {
    if (e instanceof Error) {
      return json(e.message, 400)
    }
    return json('Something went wrong', 500)
  }
}
Linda answered 9/9, 2024 at 11:35 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.