'Access raw body of Stripe webhook in Nest.js

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...



Solution 1:[1]

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!

Solution 2:[2]

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

Solution 3:[3]

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

Solution 4:[4]

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();
  }
}

Solution 5:[5]

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,
      },
    ),
  );
}

Solution 6:[6]

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:

Solution 7:[7]

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

Solution 8:[8]

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));

Solution 9:[9]

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);
    }
  }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 MikingTheViking
Solution 2 Ollie
Solution 3 Dan
Solution 4 danthedaniel
Solution 5 kaznovac
Solution 6 Cristian EnguĂ­danos Nebot
Solution 7
Solution 8
Solution 9 Orine