From 50716eb0cd8bf29cc78a912feeb5400eae9ed405 Mon Sep 17 00:00:00 2001 From: Tolga Paksoy Date: Wed, 13 Jul 2022 18:26:33 +0200 Subject: [PATCH] fix(express,fastify): raw body for urlencoded requests --- .../raw-body/e2e/express.spec.ts | 75 ++++++++++----- .../raw-body/e2e/fastify.spec.ts | 93 +++++++++++++------ .../adapters/express-adapter.ts | 20 ++-- .../utils/get-body-parser-options.util.ts | 30 ++++++ .../adapters/fastify-adapter.ts | 38 ++++++-- 5 files changed, 183 insertions(+), 73 deletions(-) create mode 100644 packages/platform-express/adapters/utils/get-body-parser-options.util.ts diff --git a/integration/nest-application/raw-body/e2e/express.spec.ts b/integration/nest-application/raw-body/e2e/express.spec.ts index bc4501f3ed4..762ecd92c65 100644 --- a/integration/nest-application/raw-body/e2e/express.spec.ts +++ b/integration/nest-application/raw-body/e2e/express.spec.ts @@ -6,7 +6,6 @@ import { ExpressModule } from '../src/express.module'; describe('Raw body (Express Application)', () => { let app: NestExpressApplication; - const body = '{ "amount":0.0 }'; beforeEach(async () => { const moduleFixture = await Test.createTestingModule({ @@ -16,33 +15,63 @@ describe('Raw body (Express Application)', () => { app = moduleFixture.createNestApplication({ rawBody: true, }); - }); - - it('should return exact post body', async () => { - await app.init(); - const response = await request(app.getHttpServer()) - .post('/') - .set('Content-Type', 'application/json') - .send(body) - .expect(201); - - expect(response.body).to.eql({ - parsed: { - amount: 0, - }, - raw: '{ "amount":0.0 }', - }); - }); - it('should work if post body is empty', async () => { await app.init(); - await request(app.getHttpServer()) - .post('/') - .set('Content-Type', 'application/json') - .expect(201); }); afterEach(async () => { await app.close(); }); + + describe('application/json', () => { + const body = '{ "amount":0.0 }'; + + it('should return exact post body', async () => { + const response = await request(app.getHttpServer()) + .post('/') + .set('Content-Type', 'application/json') + .send(body) + .expect(201); + + expect(response.body).to.eql({ + parsed: { + amount: 0, + }, + raw: body, + }); + }); + + it('should work if post body is empty', async () => { + await request(app.getHttpServer()) + .post('/') + .set('Content-Type', 'application/json') + .expect(201); + }); + }); + + describe('application/x-www-form-urlencoded', () => { + const body = 'content=this is a post\'s content by "Nest"'; + + it('should return exact post body', async () => { + const response = await request(app.getHttpServer()) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .send(body) + .expect(201); + + expect(response.body).to.eql({ + parsed: { + content: 'this is a post\'s content by "Nest"', + }, + raw: body, + }); + }); + + it('should work if post body is empty', async () => { + await request(app.getHttpServer()) + .post('/') + .set('Content-Type', 'application/x-www-form-urlencoded') + .expect(201); + }); + }); }); diff --git a/integration/nest-application/raw-body/e2e/fastify.spec.ts b/integration/nest-application/raw-body/e2e/fastify.spec.ts index 5d6e9052a8b..03db3f137d9 100644 --- a/integration/nest-application/raw-body/e2e/fastify.spec.ts +++ b/integration/nest-application/raw-body/e2e/fastify.spec.ts @@ -8,7 +8,6 @@ import { FastifyModule } from '../src/fastify.module'; describe('Raw body (Fastify Application)', () => { let app: NestFastifyApplication; - const body = '{ "amount":0.0 }'; beforeEach(async () => { const moduleFixture = await Test.createTestingModule({ @@ -21,43 +20,79 @@ describe('Raw body (Fastify Application)', () => { rawBody: true, }, ); - }); - it('should return exact post body', async () => { await app.init(); - const response = await app.inject({ - method: 'POST', - url: '/', - headers: { 'content-type': 'application/json' }, - payload: body, + }); + + afterEach(async () => { + await app.close(); + }); + + describe('application/json', () => { + const body = '{ "amount":0.0 }'; + + it('should return exact post body', async () => { + const response = await app.inject({ + method: 'POST', + url: '/', + headers: { 'content-type': 'application/json' }, + payload: body, + }); + + expect(JSON.parse(response.body)).to.eql({ + parsed: { + amount: 0, + }, + raw: body, + }); }); - expect(JSON.parse(response.body)).to.eql({ - parsed: { - amount: 0, - }, - raw: '{ "amount":0.0 }', + it('should fail if post body is empty', async () => { + const response = await app.inject({ + method: 'POST', + url: '/', + headers: { + 'content-type': 'application/json', + accept: 'application/json', + }, + }); + + // Unlike Express, when you send a POST request without a body + // with Fastify, Fastify will throw an error because it isn't valid + // JSON. See fastify/fastify#297. + expect(response.statusCode).to.equal(400); }); }); - it('should fail if post body is empty', async () => { - await app.init(); - const response = await app.inject({ - method: 'POST', - url: '/', - headers: { - 'content-type': 'application/json', - accept: 'application/json', - }, + describe('application/x-www-form-urlencoded', () => { + const body = 'content=this is a post\'s content by "Nest"'; + + it('should return exact post body', async () => { + const response = await app.inject({ + method: 'POST', + url: '/', + headers: { 'content-type': 'application/x-www-form-urlencoded' }, + payload: body, + }); + + expect(JSON.parse(response.body)).to.eql({ + parsed: { + content: 'this is a post\'s content by "Nest"', + }, + raw: body, + }); }); - // Unlike Express, when you send a POST request without a body - // with Fastify, Fastify will throw an error because it isn't valid - // JSON. See fastify/fastify#297. - expect(response.statusCode).to.equal(400); - }); + it('should work if post body is empty', async () => { + const response = await app.inject({ + method: 'POST', + url: '/', + headers: { + 'content-type': 'application/x-www-form-urlencoded', + }, + }); - afterEach(async () => { - await app.close(); + expect(response.statusCode).to.equal(201); + }); }); }); diff --git a/packages/platform-express/adapters/express-adapter.ts b/packages/platform-express/adapters/express-adapter.ts index 9bb80c5b1ca..dba07c6b9f6 100644 --- a/packages/platform-express/adapters/express-adapter.ts +++ b/packages/platform-express/adapters/express-adapter.ts @@ -25,6 +25,7 @@ import { RouterMethodFactory } from '@nestjs/core/helpers/router-method-factory' import { json as bodyParserJson, OptionsJson, + OptionsUrlencoded, urlencoded as bodyParserUrlencoded, } from 'body-parser'; import * as cors from 'cors'; @@ -32,6 +33,7 @@ import * as express from 'express'; import * as http from 'http'; import * as https from 'https'; import { ServeStaticOptions } from '../interfaces/serve-static-options.interface'; +import { getBodyParserOptions } from './utils/get-body-parser-options.util'; type VersionedRoute = < TRequest extends Record = any, @@ -194,21 +196,15 @@ export class ExpressAdapter extends AbstractHttpAdapter { } public registerParserMiddleware(prefix?: string, rawBody?: boolean) { - let bodyParserJsonOptions: OptionsJson | undefined; - if (rawBody === true) { - bodyParserJsonOptions = { - verify: (req: RawBodyRequest, _res, buffer) => { - if (Buffer.isBuffer(buffer)) { - req.rawBody = buffer; - } - return true; - }, - }; - } + const bodyParserJsonOptions = getBodyParserOptions(rawBody); + const bodyParserUrlencodedOptions = getBodyParserOptions( + rawBody, + { extended: true }, + ); const parserMiddleware = { jsonParser: bodyParserJson(bodyParserJsonOptions), - urlencodedParser: bodyParserUrlencoded({ extended: true }), + urlencodedParser: bodyParserUrlencoded(bodyParserUrlencodedOptions), }; Object.keys(parserMiddleware) .filter(parser => !this.isMiddlewareApplied(parser)) diff --git a/packages/platform-express/adapters/utils/get-body-parser-options.util.ts b/packages/platform-express/adapters/utils/get-body-parser-options.util.ts new file mode 100644 index 00000000000..0f1513243e2 --- /dev/null +++ b/packages/platform-express/adapters/utils/get-body-parser-options.util.ts @@ -0,0 +1,30 @@ +import type { RawBodyRequest } from '@nestjs/common'; +import type { Options } from 'body-parser'; +import type { IncomingMessage, ServerResponse } from 'http'; + +const rawBodyParser = ( + req: RawBodyRequest, + _res: ServerResponse, + buffer: Buffer, +) => { + if (Buffer.isBuffer(buffer)) { + req.rawBody = buffer; + } + return true; +}; + +export function getBodyParserOptions( + rawBody: boolean, + options?: ParserOptions | undefined, +): ParserOptions { + let parserOptions: ParserOptions = options ?? ({} as ParserOptions); + + if (rawBody === true) { + parserOptions = { + ...parserOptions, + verify: rawBodyParser, + }; + } + + return parserOptions; +} diff --git a/packages/platform-fastify/adapters/fastify-adapter.ts b/packages/platform-fastify/adapters/fastify-adapter.ts index 1043bfef4c4..e18ba56a892 100644 --- a/packages/platform-fastify/adapters/fastify-adapter.ts +++ b/packages/platform-fastify/adapters/fastify-adapter.ts @@ -41,6 +41,8 @@ import { InjectOptions, Response as LightMyRequestResponse, } from 'light-my-request'; +// `querystring` is used internally in fastify for registering urlencoded body parser. +import { parse as querystringParse } from 'querystring'; import { FastifyStaticOptions, PointOfViewOptions, @@ -454,13 +456,9 @@ export class FastifyAdapter< if (this._isParserRegistered) { return; } - this.register( - import('@fastify/formbody') as Parameters[0], - ); - if (rawBody) { - this.registerContentParserWithRawBody(); - } + this.registerUrlencodedContentParser(rawBody); + this.registerJsonContentParser(rawBody); this._isParserRegistered = true; } @@ -509,16 +507,18 @@ export class FastifyAdapter< return !('status' in response); } - private registerContentParserWithRawBody() { + private registerJsonContentParser(rawBody?: boolean) { + const { bodyLimit } = this.getInstance().initialConfig; + this.getInstance().addContentTypeParser( 'application/json', - { parseAs: 'buffer' }, + { parseAs: 'buffer', bodyLimit }, ( req: RawBodyRequest>, body: Buffer, done, ) => { - if (Buffer.isBuffer(body)) { + if (rawBody === true && Buffer.isBuffer(body)) { req.rawBody = body; } @@ -533,6 +533,26 @@ export class FastifyAdapter< ); } + private registerUrlencodedContentParser(rawBody?: boolean) { + const { bodyLimit } = this.getInstance().initialConfig; + + this.getInstance().addContentTypeParser( + 'application/x-www-form-urlencoded', + { parseAs: 'buffer', bodyLimit }, + ( + req: RawBodyRequest>, + body: Buffer, + done, + ) => { + if (rawBody === true && Buffer.isBuffer(body)) { + req.rawBody = body; + } + + done(null, querystringParse(body.toString())); + }, + ); + } + private async registerMiddie() { this.isMiddieRegistered = true; await this.register(