hooksentinel
Errors

missing_raw_body

hooksentinel error missing_raw_body — the request body was already parsed as JSON before signature verification could run. Fixes for Express middleware ordering and NestJS rawBody.

Summary

FieldValue
Codemissing_raw_body
HTTP status500
RetryableNo

What caused it

Webhook signature verification has to run against the exact bytes the provider signed — the raw request body, before any JSON parsing touches it. Parsing normalizes whitespace and can reorder or re-encode content in ways that change the byte sequence, which would make even a legitimate request fail signature verification. So hooksentinel refuses to guess: if it can't find a raw Buffer or string body on the request, it throws missing_raw_body rather than silently verifying against parsed JSON (which would be wrong) or skipping verification (which would be worse).

This almost always means a body-parsing middleware ran on the route before hooksentinel's handler — most commonly a global express.json(), Fastify's default JSON content-type parser, or (in NestJS) not requesting Nest's raw body capture at bootstrap.

The fix

Express: raw body middleware ordering

express.json() (or any global body parser) mounted before your webhook route consumes the body stream — there's nothing left for hooksentinel to read. Scope express.raw() to the webhook route specifically, and register it before any global JSON parsing:

src/server.ts
import express from 'express';
import { toExpressHandler } from '@hooksentinel/core/express';
import { stripeWebhook } from './webhooks/stripe';

const app = express();

// ✅ raw parser scoped to this route, registered first
app.post(
  '/webhooks/stripe',
  express.raw({ type: 'application/json' }),
  toExpressHandler(stripeWebhook),
);

// ✅ safe to add global JSON parsing after — it won't affect the route above
app.use(express.json());

app.listen(3000);

The bug, for comparison — express.json() registered globally before the webhook route:

src/server.ts (broken)
const app = express();

app.use(express.json()); // ❌ consumes the body before your route ever sees it

app.post('/webhooks/stripe', toExpressHandler(stripeWebhook)); // always missing_raw_body

See the full Express guide for multi-route setups.

NestJS: rawBody: true

Nest doesn't capture the raw body by default. Pass rawBody: true in NestFactory.create():

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    rawBody: true, // ✅ required
  });
  await app.listen(3000);
}
bootstrap();

Without this option, req.rawBody is always undefined in a Nest app, regardless of anything you configure on the controller or module.

NestJS: the rawBody + custom body-parser override bug (nestjs/nest#10471)

Even with rawBody: true set, req.rawBody can still come back undefined if your app also registers its own body-parser middleware — typically to raise the default JSON size limit. The custom parser silently replaces the one Nest uses internally to populate req.rawBody. This exact interaction is tracked upstream as nestjs/nest#10471.

Broken:

src/main.ts (breaks rawBody)
import { NestFactory } from '@nestjs/core';
import { json } from 'express';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { rawBody: true });

  app.use(json({ limit: '10mb' })); // ❌ overrides Nest's raw-body-capturing parser
  await app.listen(3000);
}
bootstrap();

Fixed — re-attach the raw body yourself inside the custom parser's verify hook:

src/main.ts (fixed)
import { NestFactory } from '@nestjs/core';
import { json } from 'express';
import type { IncomingMessage } from 'http';
import { AppModule } from './app.module';

function rawBodySaver(req: IncomingMessage & { rawBody?: Buffer }, _res: unknown, buf: Buffer) {
  if (buf?.length) {
    req.rawBody = buf;
  }
}

async function bootstrap() {
  const app = await NestFactory.create(AppModule, { rawBody: true });

  app.use(json({ limit: '10mb', verify: rawBodySaver })); // ✅ re-populates req.rawBody

  await app.listen(3000);
}
bootstrap();

If you don't strictly need a larger body limit on every route, the simpler fix is to not override the global parser at all, and instead raise the limit only on the specific non-webhook controllers that need it — that avoids the interaction entirely.

Full NestJS walkthrough, including @VerifiedWebhook() controller wiring: NestJS guide.

Fastify

Fastify parses application/json by default. Register a raw content-type parser, ideally scoped to a plugin so it doesn't affect other JSON routes on the same instance:

app.addContentTypeParser(
  'application/json',
  { parseAs: 'buffer' },
  (_req, body, done) => done(null, body),
);

See the Fastify guide for the scoped-plugin version.

Next.js and Lambda

App Router route handlers and Lambda's toLambdaHandler don't need any raw-body configuration — there's no implicit body-parsing middleware in either path for hooksentinel to lose the raw bytes to. If you're seeing missing_raw_body from one of these, double check you aren't calling request.json() yourself before passing the request into hooksentinel, which would consume the body stream first.

Last updated on

On this page