Express applications commonly mount app.use(express.json()) globally. This parses incoming JSON request streams directly into req.body as JavaScript objects and consumes the underlying stream.
When a webhook arrives from Stripe, GitHub, or Shopify, computing HMAC SHA-256 signatures requires the exact byte-for-byte stream buffer. Re-serializing with JSON.stringify(req.body) produces different key orders and whitespace, causing signature validation to fail.
Here are the two clean solutions to preserve raw body in Express.
Solution 1: Use the verify callback on express.json() (Recommended)
express.json() has a built-in verify option that gives you access to the raw Buffer before parsing occurs:
import express from 'express';
const app = express();
app.use(
express.json({
verify: (req, res, buf, encoding) => {
// Attach the raw buffer to the request object
if (req.originalUrl.startsWith('/webhooks/')) {
req.rawBody = buf;
}
}
})
);
app.post('/webhooks/stripe', (req, res) => {
const sig = req.headers['stripe-signature'];
// Verify using req.rawBody
const event = stripe.webhooks.constructEvent(
req.rawBody,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
res.json({ received: true });
}); Solution 2: Mount express.raw() specifically before global JSON middleware
Alternatively, mount the webhook route with express.raw({ type: 'application/json' }) before mounting express.json():
import express from 'express';
const app = express();
// 1. Mount raw body handler for webhooks first
app.post(
'/webhooks/stripe',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['stripe-signature'];
const event = stripe.webhooks.constructEvent(
req.body, // In this route, req.body is the raw Buffer
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
res.json({ received: true });
}
);
// 2. Mount general JSON parser for regular API routes
app.use(express.json()); Testing Your Setup
If signature checks fail in production, place HookWatch in front of your Express server. HookWatch captures the exact bytes received from the provider, allowing you to compare them against the bytes received by your Express handler.