Skip to content
Guide Security

Preserving Raw Body for Webhooks in Express.js without Breaking JSON Parsing

How to configure Express.js middleware to capture raw body buffers for webhook signature verification while keeping express.json() working across the rest of your app.

Published 2 min read
On this page

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.

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.

Get started

See what happened to every webhook.

HookWatch keeps the request, the response, and every attempt for each delivery — so the debugging, retry, and replay steps in this article are a matter of reading, not reconstructing.