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
| Field | Value |
|---|---|
| Code | missing_raw_body |
| HTTP status | 500 |
| Retryable | No |
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:
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:
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_bodySee 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():
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:
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:
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