Event-sub Signature verification only working sometimes?

For some reason I can’t pinpoint, I’m unable to verify SOME signatures from event-sub events (webhooks version). This same code has worked without issue before, but there must be some quirk that’s come up, I just can’t find it.

Here’s my code:

import crypto from 'crypto';
const verifySignature = (req: Request): boolean => {
  console.log('Received Headers:', req.headers);
  const messageId = req.header('Twitch-Eventsub-Message-Id'.toLowerCase());
  const timestamp = req.header('Twitch-Eventsub-Message-Timestamp'.toLowerCase());
  const body = JSON.stringify(req.body);
  const twitchSignature = req.header('Twitch-Eventsub-Message-Signature'.toLowerCase());

  if (!messageId || !timestamp || !twitchSignature) {
    return false;
  }
  if(!process.env.TWITCH_SUB_SECRET){
    throw new Error('!!!there is no twitchSubSecret, this is VERY bad!!!')
  }

  const hash = crypto
    .createHmac('sha256', process.env.TWITCH_SUB_SECRET!)
    .update(messageId + timestamp + body)
    .digest('hex');

  return `sha256=${hash}`.trim() === twitchSignature.trim(); //These two strings don't match >:^(
};

Things to note:
1.) This works during the eventsub’s initial Challenge call, but not for the calls from the event.
2.) The stringified body’s length is always 5 less than the “content-length” listed in the header when the call ISN’T for the Challenge
3.) I am confident the MessageId, Timestamp, Stringified body, and process.env-vars are being created correctly (lots of console.logs I omitted from this post.)
Any help would be greatly appreciated.

This is the problem.

You take the body, convert it to an object, then back to a string.

Which can mess with a number of things as it’s no longer the body that was sent. (It messes with any spaces or can mess with character encoding)

Consider something like this instead twitch_misc/eventsub/webhooks/nodejs/receive.js at main · BarryCarlyon/twitch_misc · GitHub

Good catch! I would have never guessed that was the issue.
I attempted your solution, but found something else:
After a LOT of fighting with the app, I eventually found that if I set up the webhook router before the main app, then declare the main app and the JSON parser after that, I’m able to serve the webhooks strings and everything else an object:

app.ts

  app.use('/webhooks', webhooksRouter)
  app.use(express.json()) //This was above app.use-webhooks
  app.use('/api', api)

webhooks.ts

const webhooksRouter = express.Router();

i.e. The fix was swapping the place of 2 lines