Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(express,fastify): raw body for urlencoded requests #9926

Merged
merged 1 commit into from Jul 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
75 changes: 52 additions & 23 deletions integration/nest-application/raw-body/e2e/express.spec.ts
Expand Up @@ -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({
Expand All @@ -16,33 +15,63 @@ describe('Raw body (Express Application)', () => {
app = moduleFixture.createNestApplication<NestExpressApplication>({
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);
});
});
});
93 changes: 64 additions & 29 deletions integration/nest-application/raw-body/e2e/fastify.spec.ts
Expand Up @@ -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({
Expand All @@ -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);
});
});
});
20 changes: 8 additions & 12 deletions packages/platform-express/adapters/express-adapter.ts
Expand Up @@ -25,13 +25,15 @@ 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';
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<string, any> = any,
Expand Down Expand Up @@ -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<http.IncomingMessage>, _res, buffer) => {
if (Buffer.isBuffer(buffer)) {
req.rawBody = buffer;
}
return true;
},
};
}
const bodyParserJsonOptions = getBodyParserOptions<OptionsJson>(rawBody);
const bodyParserUrlencodedOptions = getBodyParserOptions<OptionsUrlencoded>(
rawBody,
{ extended: true },
);

const parserMiddleware = {
jsonParser: bodyParserJson(bodyParserJsonOptions),
urlencodedParser: bodyParserUrlencoded({ extended: true }),
urlencodedParser: bodyParserUrlencoded(bodyParserUrlencodedOptions),
};
Object.keys(parserMiddleware)
.filter(parser => !this.isMiddlewareApplied(parser))
Expand Down
@@ -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<IncomingMessage>,
_res: ServerResponse,
buffer: Buffer,
) => {
if (Buffer.isBuffer(buffer)) {
req.rawBody = buffer;
}
return true;
};

export function getBodyParserOptions<ParserOptions extends Options>(
rawBody: boolean,
options?: ParserOptions | undefined,
): ParserOptions {
let parserOptions: ParserOptions = options ?? ({} as ParserOptions);

if (rawBody === true) {
parserOptions = {
...parserOptions,
verify: rawBodyParser,
};
}

return parserOptions;
}
38 changes: 29 additions & 9 deletions packages/platform-fastify/adapters/fastify-adapter.ts
Expand Up @@ -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,
Expand Down Expand Up @@ -454,13 +456,9 @@ export class FastifyAdapter<
if (this._isParserRegistered) {
return;
}
this.register(
import('@fastify/formbody') as Parameters<TInstance['register']>[0],
);

if (rawBody) {
this.registerContentParserWithRawBody();
}
this.registerUrlencodedContentParser(rawBody);
this.registerJsonContentParser(rawBody);

this._isParserRegistered = true;
}
Expand Down Expand Up @@ -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<Buffer>(
'application/json',
{ parseAs: 'buffer' },
{ parseAs: 'buffer', bodyLimit },
(
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
body: Buffer,
done,
) => {
if (Buffer.isBuffer(body)) {
if (rawBody === true && Buffer.isBuffer(body)) {
req.rawBody = body;
}

Expand All @@ -533,6 +533,26 @@ export class FastifyAdapter<
);
}

private registerUrlencodedContentParser(rawBody?: boolean) {
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;
}

done(null, querystringParse(body.toString()));
},
);
}

private async registerMiddie() {
this.isMiddieRegistered = true;
await this.register(
Expand Down