Preserve Raw HTTP Body in Express for Webhook Verification
Express’s express.json() consumes the request stream, discarding the raw bytes required for webhook HMAC verification. The verify option captures the buffer before parsing; scope it to webhook routes to avoid memory overhead.
07 Apr 2026, 17:25 UTC

The Problem: Webhooks Need Exact Bytes
When a payment provider or GitHub sends a webhook, they sign the request body with an HMAC. Your verification code must compute the same HMAC over the exact same bytes that arrived on the wire. But express.json() consumes the request stream and replaces it with a parsed JavaScript object. By the time your route handler runs, the original bytes are gone.
This isn’t a new problem, but it bites teams every time they add a new webhook endpoint. The fix is built into Express’s bundled body-parser: the verify option.
How the verify Option Works
When you call express.json(), you can pass a verify function that receives the raw Buffer before parsing happens:
app.use(express.json({\n verify: (req, res, buf) => {\n req.rawBody = buf\n }\n}))
The verify callback runs synchronously during body parsing. The buffer buf contains the exact bytes read from the socket. Attaching it to req.rawBody makes it available to downstream middleware and route handlers. req.body still holds the parsed object.
This works the same in Express 4.x and 5.x because it’s part of the long-standing body-parser integration, not a recent routing change.
Scope the Capture to Webhook Routes Only
Buffering every request body adds memory pressure and CPU overhead. Most endpoints don’t need raw bytes. Apply the capture middleware only where verification happens:
// Global JSON parser for regular API routes\napp.use(express.json())\n\n// Webhook-specific parser with raw capture\nconst webhookParser = express.json({\n verify: (req, res, buf) => { req.rawBody = buf }\n})\n\napp.post('/webhooks/stripe', webhookParser, (req, res) => {\n const signature = req.headers['stripe-signature']\n const expected = crypto\n .createHmac('sha256', process.env.STRIPE_WEBHOOK_SECRET)\n .update(req.rawBody)\n .digest('hex')\n \n if (signature !== expected) {\n return res.status(400).send('Invalid signature')\n }\n \n // req.body is already parsed JSON\n handleEvent(req.body)\n res.json({ received: true })\n})
Run this in your Express application file (typically app.js or server.js). No special permissions needed beyond standard Node.js process rights. The webhookParser middleware runs only for the /webhooks/stripe route.
Verifying the Raw Body Is Intact
To confirm the buffer matches what the client sent, send a test request with a known payload and compare HMACs:
# Terminal 1: start your server
node server.js
# Terminal 2: send test webhook
PAYLOAD='{"id":"evt_test","type":"payment_intent.succeeded"}'
SECRET='whsec_test_secret'
SIG=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256)
curl -X POST http://localhost:3000/webhooks/stripe \
-H "Content-Type: application/json" \
-H "Stripe-Signature: $SIG" \
-d "$PAYLOAD"
If verification passes, your req.rawBody contains the exact bytes the client signed. Check your server logs for req.rawBody.toString() and JSON.stringify(req.body) — they should represent the same payload.
Trade-offs and Limitations
- Memory: Each captured request holds a buffer in memory until the response finishes. For high-throughput webhook endpoints, consider a raw-only parser (
express.raw()) and manualJSON.parseonly when verification passes. - Reverse proxies: If a load balancer, CDN, or hosting platform rewrites the body (e.g., normalization, compression, charset conversion), the bytes your app sees won’t match what the client signed. Verify the raw body at the edge or configure the proxy to pass the body untouched.
- Double parsing: The
verifyapproach parses JSON twice — once for the raw buffer, once forreq.body. At low-to-moderate volume this is negligible; at scale, benchmarkexpress.raw()+ conditionalJSON.parse.
Next Steps
- Add a route-scoped
express.json({ verify: ... })middleware for each webhook provider. - Store webhook secrets in environment variables, not code.
- Test with the provider’s CLI (e.g.,
stripe listen --forward-to localhost:3000/webhooks/stripe) to verify end-to-end. - Monitor memory usage on webhook workers; if buffers accumulate, switch to streaming verification or raw-only parsing.
The verify option is a small, well-tested hook that solves a common integration pain point without extra dependencies. Use it where you need it, skip it where you don’t.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.