chore: resolve merge conflicts

This commit is contained in:
Kamil Myśliwiec
2023-02-01 13:03:19 +01:00
15 changed files with 383 additions and 24 deletions

View File

@@ -0,0 +1,95 @@
import { NestExpressApplication } from '@nestjs/platform-express';
import { Test } from '@nestjs/testing';
import { OptionsUrlencoded } from 'body-parser';
import { expect } from 'chai';
import * as request from 'supertest';
import { AppModule } from '../src/app.module';
describe('Body Parser (Express Application)', () => {
const moduleFixture = Test.createTestingModule({
imports: [AppModule],
});
let app: NestExpressApplication;
afterEach(async () => {
await app.close();
});
describe('application/json', () => {
const stringLimit = '{ "msg": "Hello, World" }';
const stringOverLimit = '{ "msg": "Hello, World!" }';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestExpressApplication>({
rawBody: true,
logger: false,
})
.useBodyParser('json', { limit: Buffer.from(stringLimit).byteLength });
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/json')
.send(stringLimit)
.expect(201);
expect(response.body).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/json')
.send(stringOverLimit)
.expect(413);
});
});
describe('application/x-www-form-urlencoded', () => {
const stringLimit = 'msg=Hello, World';
const stringOverLimit = 'msg=Hello, World!';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestExpressApplication>({
rawBody: true,
logger: false,
})
.useBodyParser<OptionsUrlencoded>('urlencoded', {
limit: Buffer.from(stringLimit).byteLength,
extended: true,
});
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(stringLimit)
.expect(201);
expect(response.body).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
await request(app.getHttpServer())
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(stringOverLimit)
.expect(413);
});
});
});

View File

@@ -0,0 +1,106 @@
import {
FastifyAdapter,
NestFastifyApplication,
} from '@nestjs/platform-fastify';
import { Test } from '@nestjs/testing';
import { expect } from 'chai';
import { AppModule } from '../src/app.module';
describe('Body Parser (Fastify Application)', () => {
const moduleFixture = Test.createTestingModule({
imports: [AppModule],
});
let app: NestFastifyApplication;
afterEach(async () => {
await app.close();
});
describe('application/json', () => {
const stringLimit = '{ "msg": "Hello, World" }';
const stringOverLimit = '{ "msg": "Hello, World!" }';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestFastifyApplication>(new FastifyAdapter(), {
rawBody: true,
logger: false,
})
.useBodyParser('application/json', {
bodyLimit: Buffer.from(stringLimit).byteLength,
});
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/json' },
payload: stringLimit,
});
expect(JSON.parse(response.body)).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/json' },
payload: stringOverLimit,
});
expect(response.statusCode).to.equal(413);
});
});
describe('application/x-www-form-urlencoded', () => {
const stringLimit = 'msg=Hello, World';
const stringOverLimit = 'msg=Hello, World!';
beforeEach(async () => {
const testFixture = await moduleFixture.compile();
app = testFixture
.createNestApplication<NestFastifyApplication>(new FastifyAdapter(), {
rawBody: true,
logger: false,
})
.useBodyParser('application/x-www-form-urlencoded', {
bodyLimit: Buffer.from(stringLimit).byteLength,
});
await app.init();
});
it('should allow request with matching body limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
payload: stringLimit,
});
expect(JSON.parse(response.body)).to.eql({
raw: stringLimit,
});
});
it('should fail if post body is larger than limit', async () => {
const response = await app.inject({
method: 'POST',
url: '/',
headers: { 'content-type': 'application/x-www-form-urlencoded' },
payload: stringOverLimit,
});
expect(response.statusCode).to.equal(413);
});
});
});

View File

@@ -0,0 +1,12 @@
import { Controller, Post, Req, RawBodyRequest } from '@nestjs/common';
import { IncomingMessage } from 'http';
@Controller()
export class AppController {
@Post()
index(@Req() req: RawBodyRequest<IncomingMessage>) {
return {
raw: req.rawBody?.toString(),
};
}
}

View File

@@ -0,0 +1,7 @@
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
@Module({
controllers: [AppController],
})
export class AppModule {}

View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": false,
"noImplicitAny": false,
"removeComments": true,
"lib": ["dom"],
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es6",
"sourceMap": true,
"allowJs": true,
"outDir": "./dist"
},
"include": [
"src/**/*",
"e2e/**/*"
],
"exclude": [
"node_modules"
]
}

View File

@@ -30,6 +30,7 @@ export interface HttpServer<TRequest = any, TResponse = any> {
| RequestHandler<TRequest, TResponse>
| ErrorHandler<TRequest, TResponse>,
): any;
useBodyParser?(...args: any[]): any;
get(handler: RequestHandler<TRequest, TResponse>): any;
get(path: string, handler: RequestHandler<TRequest, TResponse>): any;
post(handler: RequestHandler<TRequest, TResponse>): any;

View File

@@ -266,6 +266,20 @@ export class NestApplication
return this;
}
public useBodyParser(...args: [any, any?]): this {
if (!('useBodyParser' in this.httpAdapter)) {
this.logger.warn('Your HTTP Adapter does not support `.useBodyParser`.');
return this;
}
const [parserType, ...otherArgs] = args;
const rawBody = !!this.appOptions.rawBody;
this.httpAdapter.useBodyParser(...[parserType, rawBody, ...otherArgs]);
return this;
}
public enableCors(options?: CorsOptions | CorsOptionsDelegate<any>): void {
this.httpAdapter.enableCors(options);
}

View File

@@ -29,11 +29,13 @@ import {
OptionsUrlencoded,
urlencoded as bodyParserUrlencoded,
} from 'body-parser';
import * as bodyparser from 'body-parser';
import * as cors from 'cors';
import * as express from 'express';
import * as http from 'http';
import * as https from 'https';
import { Duplex, pipeline } from 'stream';
import { NestExpressBodyParserOptions } from '../interfaces/nest-express-body-parser-options.interface';
import { ServeStaticOptions } from '../interfaces/serve-static-options.interface';
import { getBodyParserOptions } from './utils/get-body-parser-options.util';
@@ -235,6 +237,19 @@ export class ExpressAdapter extends AbstractHttpAdapter {
.forEach(parserKey => this.use(parserMiddleware[parserKey]));
}
public useBodyParser<Options extends bodyparser.Options = bodyparser.Options>(
type: keyof bodyparser.BodyParser,
rawBody: boolean,
options?: NestExpressBodyParserOptions<Options>,
): this {
const parserOptions = getBodyParserOptions(rawBody, options || {});
const parser = bodyparser[type](parserOptions);
this.use(parser);
return this;
}
public setLocal(key: string, value: any) {
this.instance.locals[key] = value;
return this;

View File

@@ -1 +1,2 @@
export * from './nest-express-application.interface';
export * from './nest-express-body-parser-options.interface';

View File

@@ -1,5 +1,7 @@
import { Server } from 'net';
import { INestApplication } from '@nestjs/common';
import * as bodyparser from 'body-parser';
import { NestExpressBodyParserOptions } from './nest-express-body-parser-options.interface';
import { ServeStaticOptions } from './serve-static-options.interface';
/**
@@ -73,6 +75,24 @@ export interface NestExpressApplication extends INestApplication {
*/
useStaticAssets(path: string, options?: ServeStaticOptions): this;
/**
* Register Express body parsers on the fly. Will respect
* the application's `rawBody` option.
*
* @example
* const app = await NestFactory.create<NestExpressApplication>(
* AppModule,
* { rawBody: true }
* );
* app.useBodyParser('json', { limit: '50mb' });
*
* @returns {this}
*/
useBodyParser<Options extends bodyparser.Options = bodyparser.Options>(
parser: keyof bodyparser.BodyParser,
options?: NestExpressBodyParserOptions<Options>,
): this;
/**
* Sets one or multiple base directories for templates (views).
*

View File

@@ -0,0 +1,6 @@
import type { Options } from 'body-parser';
export type NestExpressBodyParserOptions<T extends Options = Options> = Omit<
T,
'verify'
>;

View File

@@ -45,6 +45,7 @@ import {
} from 'light-my-request';
// `querystring` is used internally in fastify for registering urlencoded body parser.
import { parse as querystringParse } from 'querystring';
import { NestFastifyBodyParserOptions } from '../interfaces';
import {
FastifyStaticOptions,
FastifyViewOptions,
@@ -465,6 +466,43 @@ export class FastifyAdapter<
this._isParserRegistered = true;
}
public useBodyParser(
type: string | string[] | RegExp,
rawBody: boolean,
options?: NestFastifyBodyParserOptions,
parser?: FastifyBodyParser<Buffer, TServer>,
) {
const parserOptions = {
...(options || {}),
parseAs: 'buffer' as const,
};
this.getInstance().addContentTypeParser<Buffer>(
type,
parserOptions,
(
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
body: Buffer,
done,
) => {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}
if (parser) {
parser(req, body, done);
return;
}
done(null, body);
},
);
// To avoid the Nest application init to override our custom
// body parser, we mark the parsers as registered.
this._isParserRegistered = true;
}
public async createMiddlewareFactory(
requestMethod: RequestMethod,
): Promise<(path: string, callback: Function) => any> {
@@ -510,20 +548,15 @@ export class FastifyAdapter<
}
private registerJsonContentParser(rawBody?: boolean) {
const contentType = 'application/json';
const withRawBody = !!rawBody;
const { bodyLimit } = this.getInstance().initialConfig;
this.getInstance().addContentTypeParser<Buffer>(
'application/json',
{ parseAs: 'buffer', bodyLimit },
(
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
body: Buffer,
done,
) => {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}
this.useBodyParser(
contentType,
withRawBody,
{ bodyLimit },
(req, body, done) => {
const { onProtoPoisoning, onConstructorPoisoning } =
this.instance.initialConfig;
const defaultJsonParser = this.instance.getDefaultJsonParser(
@@ -536,20 +569,15 @@ export class FastifyAdapter<
}
private registerUrlencodedContentParser(rawBody?: boolean) {
const contentType = 'application/x-www-form-urlencoded';
const withRawBody = !!rawBody;
const { bodyLimit } = this.getInstance().initialConfig;
this.getInstance().addContentTypeParser<Buffer>(
'application/x-www-form-urlencoded',
{ parseAs: 'buffer', bodyLimit },
(
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
body: Buffer,
done,
) => {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}
this.useBodyParser(
contentType,
withRawBody,
{ bodyLimit },
(_req, body, done) => {
done(null, querystringParse(body.toString()));
},
);

View File

@@ -1 +1,2 @@
export * from './nest-fastify-application.interface';
export * from './nest-fastify-body-parser-options.interface';

View File

@@ -1,10 +1,12 @@
import { INestApplication } from '@nestjs/common';
import {
FastifyBodyParser,
FastifyInstance,
FastifyPluginAsync,
FastifyPluginCallback,
FastifyPluginOptions,
FastifyRegisterOptions,
RawServerBase,
} from 'fastify';
import {
Chain as LightMyRequestChain,
@@ -12,6 +14,7 @@ import {
Response as LightMyRequestResponse,
} from 'light-my-request';
import { FastifyStaticOptions, FastifyViewOptions } from './external';
import { NestFastifyBodyParserOptions } from './nest-fastify-body-parser-options.interface';
export interface NestFastifyApplication extends INestApplication {
/**
@@ -28,6 +31,27 @@ export interface NestFastifyApplication extends INestApplication {
opts?: FastifyRegisterOptions<Options>,
): Promise<FastifyInstance>;
/**
* Register Fastify body parsers on the fly. Will respect
* the application's `rawBody` option.
*
* @example
* const app = await NestFactory.create<NestFastifyApplication>(
* AppModule,
* new FastifyAdapter(),
* { rawBody: true }
* );
* // enable the json parser with a parser limit of 50mb
* app.useBodyParser('application/json', { bodyLimit: 50 * 1000 * 1024 });
*
* @returns {this}
*/
useBodyParser<TServer extends RawServerBase = RawServerBase>(
type: string | string[] | RegExp,
options?: NestFastifyBodyParserOptions,
parser?: FastifyBodyParser<Buffer, TServer>,
): this;
/**
* Sets a base directory for public assets.
* Example `app.useStaticAssets({ root: 'public' })`

View File

@@ -0,0 +1,6 @@
import type { AddContentTypeParser } from 'fastify';
export type NestFastifyBodyParserOptions = Omit<
Parameters<AddContentTypeParser>[1],
'parseAs'
>;