nest: rawBody:true does not work whilst also increasing the request size limit

Is there an existing issue for this?

  • I have searched the existing issues

Current behavior

With an app boostrapped like so:

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    rawBody: true,
  });
  app.use(json({limit: '50mb'}));
  app.use(urlencoded({limit: '50mb'}));
  app.setGlobalPrefix('api');
  await app.listen(3000);
}
bootstrap();

and a controller like so:

Controller('customerwebhook')
export class CustomerWebhookController {
  constructor(private readonly configService: ConfigService, private readonly customerWebhookService: CustomerWebhookService) {}

  @Post()
  async customerWebhook(
    @Body() payload: XeroWebhookPayload,
    @Req() request: RawBodyRequest<any>,
    @Headers('x-xero-signature') signature: string,
    @Res({ passthrough: true }) response: any,
  ): Promise<void> {
    //handle verification
}

I get the issue that the request is not actually a raw request, therefore I cannot verify the webhook correctly. This is solved if I remove the two app.use() calls in the bootstrap but I require this as my webhook requests can be in excession of 10kb (the default)

Is there a workaround / am I using this wrong, I believe these two things should be able to work in conjunction.

Minimum reproduction code

https://codesandbox.io/s/cocky-violet-bfj8pw?file=/src/customer-webhook.controller.ts

Steps to reproduce

No response

Expected behavior

The request should still be a raw request and also the request size limit respected.

Package

Other package

No response

NestJS version

9.0.0

Packages versions

{
  "name": "hidden",
  "version": "0.0.1",
  "description": "",
  "author": "",
  "private": true,
  "license": "UNLICENSED",
  "scripts": {
    "prebuild": "rimraf dist",
    "build": "nest build",
    "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
    "start": "nest start",
    "start:dev": "nest start --watch",
    "start:debug": "nest start --debug --watch",
    "start:prod": "node dist/main",
    "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
    "test": "jest",
    "test:watch": "jest --watch",
    "test:cov": "jest --coverage",
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
    "test:e2e": "jest --config ./test/jest-e2e.json"
  },
  "dependencies": {
    "@nestjs/common": "^9.0.0",
    "@nestjs/config": "^2.2.0",
    "@nestjs/core": "^9.0.0",
    "@nestjs/platform-express": "^9.0.0",
    "@nestjs/throttler": "^3.1.0",
    "@prisma/client": "^4.5.0",
    "object-sha": "^2.0.7",
    "reflect-metadata": "^0.1.13",
    "rimraf": "^3.0.2",
    "rxjs": "^7.2.0",
    "xero-node": "^4.28.1"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.0.0",
    "@nestjs/schematics": "^9.0.0",
    "@nestjs/testing": "^9.0.0",
    "@types/express": "^4.17.13",
    "@types/jest": "28.1.8",
    "@types/node": "^16.18.2",
    "@types/supertest": "^2.0.11",
    "@typescript-eslint/eslint-plugin": "^5.0.0",
    "@typescript-eslint/parser": "^5.0.0",
    "eslint": "^8.0.1",
    "eslint-config-prettier": "^8.3.0",
    "eslint-plugin-prettier": "^4.0.0",
    "jest": "28.1.3",
    "prettier": "^2.3.2",
    "prisma": "^4.5.0",
    "source-map-support": "^0.5.20",
    "supertest": "^6.1.3",
    "ts-jest": "28.0.8",
    "ts-loader": "^9.2.3",
    "ts-node": "^10.9.1",
    "tsconfig-paths": "4.1.0",
    "typescript": "^4.8.4"
  },
  "jest": {
    "moduleFileExtensions": [
      "js",
      "json",
      "ts"
    ],
    "rootDir": "src",
    "testRegex": ".*\\.spec\\.ts$",
    "transform": {
      "^.+\\.(t|j)s$": "ts-jest"
    },
    "collectCoverageFrom": [
      "**/*.(t|j)s"
    ],
    "coverageDirectory": "../coverage",
    "testEnvironment": "node"
  }
}


Node.js version

16.10.0

In which operating systems have you tested?

  • macOS
  • Windows
  • Linux

Other

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Reactions: 1
  • Comments: 26 (8 by maintainers)

Most upvoted comments

So I have actually managed to work around this like so:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { json, urlencoded } from "express";
import { getBodyParserOptions } from "@nestjs/platform-express/adapters/utils/get-body-parser-options.util";

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

  app.use(json(getBodyParserOptions(true, { limit: '50mb'})));
  app.use(urlencoded(getBodyParserOptions(true, { limit: '50mb'})));
  app.setGlobalPrefix('api');
  await app.listen(3000);
}
bootstrap();

@Karman40 you’re not using the newly added functions that fixes this issue.

const app = await NestFactory.create<NestExpressApplication>(AppModule, { httpsOptions, rawBody: true });
const configService = app.get(ConfigService);

// ...

const parserLimit = configService.get<string>('LIMIT');
app.useBodyParser('json', { limit: parserLimit });
app.useBodyParser<OptionsUrlencoded>('urlencoded', { limit: parserLimit, extended: true })

// ... reset of code like `await app.listen()`.

Guys, I have just found this thread, wonderful work. Thank you for that. One last thing: could you please update also https://docs.nestjs.com/faq/raw-body and mention this in the page? It would help immensely as people like me wouldn’t be forced to find this github issue and read the whole thread in order to find out how to fix this issue.

@tolgap

async function bootstrap() {
  const httpsOptions = process.env?.HTTPS === 'true' ? {
    key: readFileSync(join(process.cwd(), 'ssl.key')),
    cert: readFileSync(join(process.cwd(), 'ssl.crt')),
  } : undefined;

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

  app.setGlobalPrefix(configService.get<string>('globalPrefix'));
  app.enableCors(configService.get('CORS'));

  app.useGlobalPipes(new ValidationPipe({ transform: true, forbidUnknownValues: false }));
  useContainer(app.select(AppModule), { fallbackOnErrors: true });
  app.useWebSocketAdapter(new SocketIoAdapter(app));

  // RAW BODY alaways undefined, but limit works good
  app.use(json({ limit: configService.get<string>('LIMIT') }));
  app.use(urlencoded({ limit: configService.get<string>('LIMIT'), extended: true }))

  await app.listen(configService.get<number>('PORT') || 3000, configService.get<string>('HOSTNAME') || '0.0.0.0');

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}

If I use the following code instead of app.use(json({ limit: configService.get<string>('LIMIT') }));, the problem is solved (also for old versions):

  app.use((req, res, next) => {
    if (req.path.indexOf('/api/stripe/webhook') === 0) next();
    else json({ limit: configService.get<string>('LIMIT') })(req, res, next);
  });

If I remove the next line app.use(json({ limit: configService.get<string>('LIMIT') }));, the value of the raw body is correct.

You are right. Discard my comments please. I probably misunderstood those sections when reading it first time and I didn’t notice those methods are different from what we use. Sorry for not paying better attention.

I found a flexible solution:

import { RawBodyRequest } from '@nestjs/common';
import { Request, Response } from 'express';
import * as express from 'express';

export interface RawBodyMiddlewareOptions {
  limit: string;
  rawBodyUrls: string[];
}

export function rawBodyMiddleware(options: RawBodyMiddlewareOptions): unknown {
  const rawBodyUrls = new Set(options.rawBodyUrls);

  return express.json({
    ...options,
    verify: (request: RawBodyRequest<Request>, _: Response, buffer: Buffer) => {
      if (rawBodyUrls.has(request.url) && Buffer.isBuffer(buffer)) {
        request.rawBody = Buffer.from(buffer);
      }
      return true;
    }
  });
}

Then:

app.use(
  rawBodyMiddleware({
    limit: '20mb',
    rawBodyUrls: ['/some-url']
  })
);

By this the controller that binds /some-url has available the rawBody property with the buffer.

@Controller()
export class SomeController {
  @Post('/some-url')
  public async post(@Req() req: RawBodyRequest<Request>, @Res() res: Response) {

So I have actually managed to work around this like so:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { json, urlencoded } from "express";
import { getBodyParserOptions } from "@nestjs/platform-express/adapters/utils/get-body-parser-options.util";

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

  app.use(json(getBodyParserOptions(true, { limit: '50mb'})));
  app.use(urlencoded(getBodyParserOptions(true, { limit: '50mb'})));
  app.setGlobalPrefix('api');
  await app.listen(3000);
}
bootstrap();

It works fine for me, congratulations!!

it doesn’t seem there is a method useBodyParser in the latest version 9.3.9, am I missing anything?

Oh, have to use NestExpressApplication

No need to cast. The function is generic.

app.useBodyParser<OptionsUrlencoded>('urlencoded', { limit: '50mb', extended: true })

I will update the example above to show this.

@kamilmysliwiec This is a tricky one 😞 .

The adapter has no idea that the rawBody option was passed once we’re past the creation of the NestApplication.

I can think of multiple solutions.

  1. Change the NestApplicationOptions.bodyParser: boolean option to bodyParser: boolean | BodyParserOptions and introduce a type that can enable/disable rawBody and include other body parser options as well
  2. Add new functions to the adapters (e.g. enableRawBody(contentType: string, bodyParserOptions: object) that we can call via app.enableRawBody(...)
  3. Both solution 1 and 2 😃

I’ll try to implement each solution and report back.

cc @tolgap 🤔 I think this might actually be the expected behavior since rawBody configures internal express middleware so if someone provides their own implementation, rawBody is ignored. Perhaps we should log a warning in this case?