40

I need to access the raw body of the webhook request from Stripe in my Nest.js application.

Following this example, I added the below to the module which has a controller method that is needing the raw body.

function addRawBody(req, res, next) {
  req.setEncoding('utf8');

  let data = '';

  req.on('data', (chunk) => {
    data += chunk;
  });

  req.on('end', () => {
    req.rawBody = data;

    next();
  });
}

export class SubscriptionModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(addRawBody)
      .forRoutes('subscriptions/stripe');
  }
}

In the controller I am using @Req() reqand then req.rawBody to get the raw body. I need the raw body because the constructEvent of the Stripe api is using it to verify the request.

The problem is that the request is stuck. It seems that the req.on is not called either for data nor for the end event. So next() is not called in the middleware.

I did also try to use raw-body like here but I got pretty much the same result. In that case the req.readable is always false, so I am stuck there as well.

I guess this is an issue with Nest.js but I am not sure...

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
rablentain
  • 6,641
  • 13
  • 50
  • 91
  • you probably didn't disable the Nest's default `bodyParser` when creating the `NestApplication` in `bootsrap` method – kaznovac Oct 01 '20 at 13:35

11 Answers11

75

For anyone looking for a more elegant solution, turn off the bodyParser in main.ts. Create two middleware functions, one for rawbody and the other for json-parsed-body.

json-body.middleware.ts

import { Request, Response } from 'express';
import * as bodyParser from 'body-parser';
import { Injectable, NestMiddleware } from '@nestjs/common';

@Injectable()
export class JsonBodyMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: () => any) {
        bodyParser.json()(req, res, next);
    }
}

raw-body.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response } from 'express';
import * as bodyParser from 'body-parser';

@Injectable()
export class RawBodyMiddleware implements NestMiddleware {
    use(req: Request, res: Response, next: () => any) {
        bodyParser.raw({type: '*/*'})(req, res, next);
    }
}

Apply the middleware functions to appropriate routes in app.module.ts.

app.module.ts

[...]

export class AppModule implements NestModule {
    public configure(consumer: MiddlewareConsumer): void {
        consumer
            .apply(RawBodyMiddleware)
            .forRoutes({
                path: '/stripe-webhooks',
                method: RequestMethod.POST,
            })
            .apply(JsonBodyMiddleware)
            .forRoutes('*');
    }
}

[...]

And tweak initialization of Nest to turn off bodyParser:

main.ts

[...]

const app = await NestFactory.create(AppModule, { bodyParser: false })

[...]

BTW req.rawbody has been removed from express long ago.

https://github.com/expressjs/express/issues/897

Ollie
  • 1,641
  • 1
  • 13
  • 31
Joel Raju
  • 1,210
  • 2
  • 18
  • 21
37

I ran into a similar problem last night trying to authenticate a Slack token.

The solution we wound up using did require disabling the bodyParser from the core Nest App then re-enabling it after adding a new rawBody key to the request with the raw request body.

    const app = await NestFactory.create(AppModule, {
        bodyParser: false
    });

    const rawBodyBuffer = (req, res, buf, encoding) => {
        if (buf && buf.length) {
            req.rawBody = buf.toString(encoding || 'utf8');
        }
    };

    app.use(bodyParser.urlencoded({verify: rawBodyBuffer, extended: true }));
    app.use(bodyParser.json({ verify: rawBodyBuffer }));

Then in my middleware I could access it like so:

const isVerified = (req) => {
    const signature = req.headers['x-slack-signature'];
    const timestamp = req.headers['x-slack-request-timestamp'];
    const hmac = crypto.createHmac('sha256', 'somekey');
    const [version, hash] = signature.split('=');

    // Check if the timestamp is too old
    // tslint:disable-next-line:no-bitwise
    const fiveMinutesAgo = ~~(Date.now() / 1000) - (60 * 5);
    if (timestamp < fiveMinutesAgo) { return false; }

    hmac.update(`${version}:${timestamp}:${req.rawBody}`);

    // check that the request signature matches expected value
    return timingSafeCompare(hmac.digest('hex'), hash);
};

export async function slackTokenAuthentication(req, res, next) {
    if (!isVerified(req)) {
        next(new HttpException('Not Authorized Slack', HttpStatus.FORBIDDEN));
    }
    next();
}

Shine On!

EDIT:

Since this question was asked, Nest.js implemented this use case out of the box. Now you can get the raw body following these steps:

main.js

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

And then in your controller:

 @Post()
 webhook(@Req() req: RawBodyRequest<Request>) { 
  const rawBody = req.rawBody;
 }

Read more here

MikingTheViking
  • 886
  • 6
  • 11
  • 1
    Note: the NestJS embedded rawBody is not compatible with other JSON body parser parameters like changing the "limit" (e.g. to '50mb'), afaik. Your initial answer worked better for me. – Antoine OL Aug 06 '22 at 20:00
  • 1
    I've tried this `rawBody: true` feature to get a binary image, but it isn't working for some reason. Any idea why not? – MegaSpaceHamlet Feb 10 '23 at 15:54
  • Nest.js have just recently done an update on this part : You can now mix body-parser options like `limit`..etc with rawBody by specifying body-parser options with .useBodyParser. Here is an [example](https://github.com/nestjs/nest/blob/master/integration/nest-application/use-body-parser/e2e/express.spec.ts) – Luxior Mar 14 '23 at 13:59
23

Today,

as I am using NestJS and Stripe

I installed body-parser (npm), then in the main.ts, just add

 app.use('/payment/hooks', bodyParser.raw({type: 'application/json'}));

and it will be restricted to this route ! no overload

Dan
  • 239
  • 2
  • 2
  • In the controller of hooks it would be something like this `handleWebhook(@Body() raw: Buffer)` – a7md0 Oct 08 '20 at 00:07
  • 1
    @a7md0 I could get `raw` data on the controller hooks, can you please share more details – zulqarnain Feb 01 '21 at 18:45
  • @zulqarnain For stripe I just passed the raw which is type of Buffer to the `eventConstructor` body – a7md0 Feb 03 '21 at 00:45
  • 1
    This seems like the simplest solution. Also remember to import it correctly: `import * as bodyParser from 'body-parser'` – Eliezer Steinbock Jul 09 '21 at 02:01
  • Great solution, and it works. Better to use `import { raw } from 'express';` and `raw({ type: 'application/json' })` though with modern implementations. – Ryall Oct 26 '21 at 10:43
  • 1
    If you have multiple parsers configured, make sure the more specific ones come before the general ones. `app.use('/payment/hooks', bodyParser.raw({type: 'application/json'})); app.use(bodyParser.json());` – andre Nov 07 '21 at 16:06
  • This worked great for me, especially since we are using an older version of Nest that didn't have the "rawBody" option. I also like that it limits using the raw body to certain paths since it seems like that could use up more memory. – jovanchohan Nov 02 '22 at 20:56
9

2022 Q3 update

Now it's possible to do out of the box, via dedicated rawBody option: https://docs.nestjs.com/faq/raw-body

p.s. Just don't forget to update your nest dependencies to the latest:

npm update @nestjs/core
npm update @nestjs/common
npm update @nestjs/common
npm update @nestjs/platform-express //if you are using express
chakzefir
  • 138
  • 1
  • 9
  • For some reason, this is not working for me – Jeremiah Aug 16 '22 at 20:42
  • 1
    @Jeremiah I could help you if you provide a bit more context – chakzefir Aug 23 '22 at 09:24
  • It worked. But I had to deploy it to test ... I couldn't test locally but it works. Thanks – Jeremiah Aug 23 '22 at 20:27
  • 2
    @Jeremiah I was running into an issue with this locally as well. My problem was that I was using the signing secret provided for the webhook I set up, but if you're forwarding events using the CLI, you need to use the secret provided in the terminal when you call stripe listen. – cbronson Aug 25 '22 at 01:28
5

I fixed it with one line :)

main.ts

import * as express from 'express';

async function bootstrap() {
...
  app.use('/your-stripe-webhook', express.raw({ type: "*/*" })); // <- add this!
...
  await app.listen(8080)
}

...didn't have to add any middleware. ...didn't have to disable bodyParser

André
  • 380
  • 4
  • 9
3

I found that for some reason the body parser was failing to hand off to the next handler in the chain.

NestJS already supports raw bodies when the content type is "text/plain", so my solution is this:

import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response } from "express";

@Injectable()
export class RawBodyMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: () => unknown) {
    req.headers["content-type"] = "text/plain";
    next();
  }
}
danthedaniel
  • 439
  • 4
  • 7
1

This is my take on getting the raw(text)body in NestJS's hander:

  1. configure the app with preserveRawBodyInRequest as shown in JSDoc example (to restrict only for stripe webhook use "stripe-signature" as filter header)
  2. use RawBody decorator in handler to retrieve the raw(text)body

raw-request.decorator.ts:

import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { NestExpressApplication } from "@nestjs/platform-express";

import { json, urlencoded } from "express";
import type { Request } from "express";
import type http from "http";

export const HTTP_REQUEST_RAW_BODY = "rawBody";

/**
 * make sure you configure the nest app with <code>preserveRawBodyInRequest</code>
 * @example
 * webhook(@RawBody() rawBody: string): Record<string, unknown> {
 *   return { received: true };
 * }
 * @see preserveRawBodyInRequest
 */
export const RawBody = createParamDecorator(
  async (data: unknown, context: ExecutionContext) => {
    const request = context
      .switchToHttp()
      .getRequest<Request>()
    ;

    if (!(HTTP_REQUEST_RAW_BODY in request)) {
      throw new Error(
        `RawBody not preserved for request in handler: ${context.getClass().name}::${context.getHandler().name}`,
      );
    }

    const rawBody = request[HTTP_REQUEST_RAW_BODY];

    return rawBody;
  },
);

/**
 * @example
 * const app = await NestFactory.create<NestExpressApplication>(
 *   AppModule,
 *   {
 *     bodyParser: false, // it is prerequisite to disable nest's default body parser
 *   },
 * );
 * preserveRawBodyInRequest(
 *   app,
 *   "signature-header",
 * );
 * @param app
 * @param ifRequestContainsHeader
 */
export function preserveRawBodyInRequest(
  app: NestExpressApplication,
  ...ifRequestContainsHeader: string[]
): void {
  const rawBodyBuffer = (
    req: http.IncomingMessage,
    res: http.ServerResponse,
    buf: Buffer,
  ): void => {
    if (
      buf?.length
      && (ifRequestContainsHeader.length === 0
        || ifRequestContainsHeader.some(filterHeader => req.headers[filterHeader])
      )
    ) {
      req[HTTP_REQUEST_RAW_BODY] = buf.toString("utf8");
    }
  };

  app.use(
    urlencoded(
      {
        verify: rawBodyBuffer,
        extended: true,
      },
    ),
  );
  app.use(
    json(
      {
        verify: rawBodyBuffer,
      },
    ),
  );
}
kaznovac
  • 1,303
  • 15
  • 23
1

1.

Apply middleware on module and assign controller.

import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'
import { raw } from 'body-parser'

import { PaymentIntentController } from './payment-intent.controller'
import { PaymentIntentService } from './payment-intent.service'

@Module({
    controllers: [PaymentIntentController],
    providers: [PaymentIntentService]
})
export class PaymentIntentModule implements NestModule {
    configure(consumer: MiddlewareConsumer) {
        consumer.apply(raw({ type: 'application/json' })).forRoutes(PaymentIntentController)
    }
}

2.

bodyParser option to false on bootstrap.

import { NestFactory } from '@nestjs/core'

import { AppModule } from './module'

async function bootstrap() {
    const app = await NestFactory.create(AppModule, { cors: true, bodyParser: false })

    await app.listen(8080)
}

bootstrap()

Refs:

0

I created a simple middleware router for this problem:

express-middleware-router.ts

import { NextFunction, Request, Response } from 'express';

export type NextHandleFunction = (req: Request, res: Response, next: NextFunction) => void;

export interface MiddlewareRoute {
    /**
     * Exact match with `request.originalUrl`. Optionally matches via
     * `request.originalUrl.startsWith` when ending with a `*`.
     */
    path: string;
    middleware: NextHandleFunction;
}

/**
 * Runs middleware if a route is matching `request.originalUrl`.
 * @param routes Order of routes is important. When using a catch all route like
 * `'*'`, make sure it is the last in the array.
 */
export function middlewareRouter(routes: MiddlewareRoute[]) {
    return (req: Request, res: Response, next: NextFunction) => {
        const nextMiddleware = routes.reduce((prev, curr) => {
            if (prev) {
                return prev;
            }

            const isMatch = curr.path.endsWith('*')
                ? req.originalUrl.startsWith(curr.path.slice(0, -1))
                : req.originalUrl === curr.path;

            return isMatch ? curr : prev;
        }, undefined) as MiddlewareRoute | undefined;
        nextMiddleware ? nextMiddleware.middleware(req, res, next) : next();
    };
}

It can be used like this:

main.ts

import { MiddlewareRoute, middlewareRouter } from './express-middleware-router';

const middlewareRoutes: MiddlewareRoute[] = [
    {
        path: '/stripe',
        middleware: text({ type: '*/*' }),
    },
    {
        path: '/high-json-limit/*',
        middleware: json({ limit: '10mb' }),
    },
    {
        path: '*',
        middleware: json(),
    },
];

const app = await NestFactory.create(ApiModule, {
    bodyParser: false,
});

app.use(middlewareRouter(middlewareRoutes));
Marian André
  • 196
  • 10
0

You can use one of fancy answers, or just go on with NestJS official docs and use:

const app = await NestFactory.create(AppModule, {
  rawBody: true,
  bodyParser: true,
  ...

And for your route definition:

  @Public()
  @Post("webhooks")
  async createStripeWebhookAction(
    @Req() req: RawBodyRequest<Request>,
    @Res() res: Response,
  ) {
  //... do stuff with it

Works like a charm.

millenion
  • 1,218
  • 12
  • 16
-2

Following what André posted, I improved to be more "Typescripety"

main.ts

import { raw } from 'express';

async function bootstrap() {
...
  app.use('/webhook', raw({ type: "*/*" })); // <- add this!
...
  await app.listen(3000)
}

webhook.controller.ts

import { Stripe } from 'stripe';

async function controller() {
  stripeClient: Stripe;
  constructor(
  ) {
    this.stripeClient = new Stripe(process.env.STRIPE_KEY, {
      apiVersion: '2020-08-27',
      typescript: true,
    });
  }

  @Post('')
  async stripe(
    @Body() rawBody: Buffer,
    @Headers('stripe-signature') signature: string,
  ) {
    let event: Stripe.Event;
    try {
      event = this.stripeClient.webhooks.constructEvent(
        rawBody,
        signature,
        process.env.STRIPE_WEBHOOK_KEY,
      );
    } catch (error) {
      throw new Error(error);
    }
  }
}
Orine
  • 9
  • 3