Skip to content

Commit 4eacd87

Browse files
Merge pull request #9926 from tolgap/fix/9901-urlencoded-raw-body
fix(express,fastify): raw body for urlencoded requests
2 parents 0b24aff + 50716eb commit 4eacd87

File tree

5 files changed

+183
-73
lines changed

5 files changed

+183
-73
lines changed

integration/nest-application/raw-body/e2e/express.spec.ts

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { ExpressModule } from '../src/express.module';
66

77
describe('Raw body (Express Application)', () => {
88
let app: NestExpressApplication;
9-
const body = '{ "amount":0.0 }';
109

1110
beforeEach(async () => {
1211
const moduleFixture = await Test.createTestingModule({
@@ -16,33 +15,63 @@ describe('Raw body (Express Application)', () => {
1615
app = moduleFixture.createNestApplication<NestExpressApplication>({
1716
rawBody: true,
1817
});
19-
});
20-
21-
it('should return exact post body', async () => {
22-
await app.init();
23-
const response = await request(app.getHttpServer())
24-
.post('/')
25-
.set('Content-Type', 'application/json')
26-
.send(body)
27-
.expect(201);
28-
29-
expect(response.body).to.eql({
30-
parsed: {
31-
amount: 0,
32-
},
33-
raw: '{ "amount":0.0 }',
34-
});
35-
});
3618

37-
it('should work if post body is empty', async () => {
3819
await app.init();
39-
await request(app.getHttpServer())
40-
.post('/')
41-
.set('Content-Type', 'application/json')
42-
.expect(201);
4320
});
4421

4522
afterEach(async () => {
4623
await app.close();
4724
});
25+
26+
describe('application/json', () => {
27+
const body = '{ "amount":0.0 }';
28+
29+
it('should return exact post body', async () => {
30+
const response = await request(app.getHttpServer())
31+
.post('/')
32+
.set('Content-Type', 'application/json')
33+
.send(body)
34+
.expect(201);
35+
36+
expect(response.body).to.eql({
37+
parsed: {
38+
amount: 0,
39+
},
40+
raw: body,
41+
});
42+
});
43+
44+
it('should work if post body is empty', async () => {
45+
await request(app.getHttpServer())
46+
.post('/')
47+
.set('Content-Type', 'application/json')
48+
.expect(201);
49+
});
50+
});
51+
52+
describe('application/x-www-form-urlencoded', () => {
53+
const body = 'content=this is a post\'s content by "Nest"';
54+
55+
it('should return exact post body', async () => {
56+
const response = await request(app.getHttpServer())
57+
.post('/')
58+
.set('Content-Type', 'application/x-www-form-urlencoded')
59+
.send(body)
60+
.expect(201);
61+
62+
expect(response.body).to.eql({
63+
parsed: {
64+
content: 'this is a post\'s content by "Nest"',
65+
},
66+
raw: body,
67+
});
68+
});
69+
70+
it('should work if post body is empty', async () => {
71+
await request(app.getHttpServer())
72+
.post('/')
73+
.set('Content-Type', 'application/x-www-form-urlencoded')
74+
.expect(201);
75+
});
76+
});
4877
});

integration/nest-application/raw-body/e2e/fastify.spec.ts

Lines changed: 64 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { FastifyModule } from '../src/fastify.module';
88

99
describe('Raw body (Fastify Application)', () => {
1010
let app: NestFastifyApplication;
11-
const body = '{ "amount":0.0 }';
1211

1312
beforeEach(async () => {
1413
const moduleFixture = await Test.createTestingModule({
@@ -21,43 +20,79 @@ describe('Raw body (Fastify Application)', () => {
2120
rawBody: true,
2221
},
2322
);
24-
});
2523

26-
it('should return exact post body', async () => {
2724
await app.init();
28-
const response = await app.inject({
29-
method: 'POST',
30-
url: '/',
31-
headers: { 'content-type': 'application/json' },
32-
payload: body,
25+
});
26+
27+
afterEach(async () => {
28+
await app.close();
29+
});
30+
31+
describe('application/json', () => {
32+
const body = '{ "amount":0.0 }';
33+
34+
it('should return exact post body', async () => {
35+
const response = await app.inject({
36+
method: 'POST',
37+
url: '/',
38+
headers: { 'content-type': 'application/json' },
39+
payload: body,
40+
});
41+
42+
expect(JSON.parse(response.body)).to.eql({
43+
parsed: {
44+
amount: 0,
45+
},
46+
raw: body,
47+
});
3348
});
3449

35-
expect(JSON.parse(response.body)).to.eql({
36-
parsed: {
37-
amount: 0,
38-
},
39-
raw: '{ "amount":0.0 }',
50+
it('should fail if post body is empty', async () => {
51+
const response = await app.inject({
52+
method: 'POST',
53+
url: '/',
54+
headers: {
55+
'content-type': 'application/json',
56+
accept: 'application/json',
57+
},
58+
});
59+
60+
// Unlike Express, when you send a POST request without a body
61+
// with Fastify, Fastify will throw an error because it isn't valid
62+
// JSON. See fastify/fastify#297.
63+
expect(response.statusCode).to.equal(400);
4064
});
4165
});
4266

43-
it('should fail if post body is empty', async () => {
44-
await app.init();
45-
const response = await app.inject({
46-
method: 'POST',
47-
url: '/',
48-
headers: {
49-
'content-type': 'application/json',
50-
accept: 'application/json',
51-
},
67+
describe('application/x-www-form-urlencoded', () => {
68+
const body = 'content=this is a post\'s content by "Nest"';
69+
70+
it('should return exact post body', async () => {
71+
const response = await app.inject({
72+
method: 'POST',
73+
url: '/',
74+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
75+
payload: body,
76+
});
77+
78+
expect(JSON.parse(response.body)).to.eql({
79+
parsed: {
80+
content: 'this is a post\'s content by "Nest"',
81+
},
82+
raw: body,
83+
});
5284
});
5385

54-
// Unlike Express, when you send a POST request without a body
55-
// with Fastify, Fastify will throw an error because it isn't valid
56-
// JSON. See fastify/fastify#297.
57-
expect(response.statusCode).to.equal(400);
58-
});
86+
it('should work if post body is empty', async () => {
87+
const response = await app.inject({
88+
method: 'POST',
89+
url: '/',
90+
headers: {
91+
'content-type': 'application/x-www-form-urlencoded',
92+
},
93+
});
5994

60-
afterEach(async () => {
61-
await app.close();
95+
expect(response.statusCode).to.equal(201);
96+
});
6297
});
6398
});

packages/platform-express/adapters/express-adapter.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ import { RouterMethodFactory } from '@nestjs/core/helpers/router-method-factory'
2525
import {
2626
json as bodyParserJson,
2727
OptionsJson,
28+
OptionsUrlencoded,
2829
urlencoded as bodyParserUrlencoded,
2930
} from 'body-parser';
3031
import * as cors from 'cors';
3132
import * as express from 'express';
3233
import * as http from 'http';
3334
import * as https from 'https';
3435
import { ServeStaticOptions } from '../interfaces/serve-static-options.interface';
36+
import { getBodyParserOptions } from './utils/get-body-parser-options.util';
3537

3638
type VersionedRoute = <
3739
TRequest extends Record<string, any> = any,
@@ -194,21 +196,15 @@ export class ExpressAdapter extends AbstractHttpAdapter {
194196
}
195197

196198
public registerParserMiddleware(prefix?: string, rawBody?: boolean) {
197-
let bodyParserJsonOptions: OptionsJson | undefined;
198-
if (rawBody === true) {
199-
bodyParserJsonOptions = {
200-
verify: (req: RawBodyRequest<http.IncomingMessage>, _res, buffer) => {
201-
if (Buffer.isBuffer(buffer)) {
202-
req.rawBody = buffer;
203-
}
204-
return true;
205-
},
206-
};
207-
}
199+
const bodyParserJsonOptions = getBodyParserOptions<OptionsJson>(rawBody);
200+
const bodyParserUrlencodedOptions = getBodyParserOptions<OptionsUrlencoded>(
201+
rawBody,
202+
{ extended: true },
203+
);
208204

209205
const parserMiddleware = {
210206
jsonParser: bodyParserJson(bodyParserJsonOptions),
211-
urlencodedParser: bodyParserUrlencoded({ extended: true }),
207+
urlencodedParser: bodyParserUrlencoded(bodyParserUrlencodedOptions),
212208
};
213209
Object.keys(parserMiddleware)
214210
.filter(parser => !this.isMiddlewareApplied(parser))
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { RawBodyRequest } from '@nestjs/common';
2+
import type { Options } from 'body-parser';
3+
import type { IncomingMessage, ServerResponse } from 'http';
4+
5+
const rawBodyParser = (
6+
req: RawBodyRequest<IncomingMessage>,
7+
_res: ServerResponse,
8+
buffer: Buffer,
9+
) => {
10+
if (Buffer.isBuffer(buffer)) {
11+
req.rawBody = buffer;
12+
}
13+
return true;
14+
};
15+
16+
export function getBodyParserOptions<ParserOptions extends Options>(
17+
rawBody: boolean,
18+
options?: ParserOptions | undefined,
19+
): ParserOptions {
20+
let parserOptions: ParserOptions = options ?? ({} as ParserOptions);
21+
22+
if (rawBody === true) {
23+
parserOptions = {
24+
...parserOptions,
25+
verify: rawBodyParser,
26+
};
27+
}
28+
29+
return parserOptions;
30+
}

packages/platform-fastify/adapters/fastify-adapter.ts

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ import {
4141
InjectOptions,
4242
Response as LightMyRequestResponse,
4343
} from 'light-my-request';
44+
// `querystring` is used internally in fastify for registering urlencoded body parser.
45+
import { parse as querystringParse } from 'querystring';
4446
import {
4547
FastifyStaticOptions,
4648
PointOfViewOptions,
@@ -454,13 +456,9 @@ export class FastifyAdapter<
454456
if (this._isParserRegistered) {
455457
return;
456458
}
457-
this.register(
458-
import('@fastify/formbody') as Parameters<TInstance['register']>[0],
459-
);
460459

461-
if (rawBody) {
462-
this.registerContentParserWithRawBody();
463-
}
460+
this.registerUrlencodedContentParser(rawBody);
461+
this.registerJsonContentParser(rawBody);
464462

465463
this._isParserRegistered = true;
466464
}
@@ -509,16 +507,18 @@ export class FastifyAdapter<
509507
return !('status' in response);
510508
}
511509

512-
private registerContentParserWithRawBody() {
510+
private registerJsonContentParser(rawBody?: boolean) {
511+
const { bodyLimit } = this.getInstance().initialConfig;
512+
513513
this.getInstance().addContentTypeParser<Buffer>(
514514
'application/json',
515-
{ parseAs: 'buffer' },
515+
{ parseAs: 'buffer', bodyLimit },
516516
(
517517
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
518518
body: Buffer,
519519
done,
520520
) => {
521-
if (Buffer.isBuffer(body)) {
521+
if (rawBody === true && Buffer.isBuffer(body)) {
522522
req.rawBody = body;
523523
}
524524

@@ -533,6 +533,26 @@ export class FastifyAdapter<
533533
);
534534
}
535535

536+
private registerUrlencodedContentParser(rawBody?: boolean) {
537+
const { bodyLimit } = this.getInstance().initialConfig;
538+
539+
this.getInstance().addContentTypeParser<Buffer>(
540+
'application/x-www-form-urlencoded',
541+
{ parseAs: 'buffer', bodyLimit },
542+
(
543+
req: RawBodyRequest<FastifyRequest<unknown, TServer, TRawRequest>>,
544+
body: Buffer,
545+
done,
546+
) => {
547+
if (rawBody === true && Buffer.isBuffer(body)) {
548+
req.rawBody = body;
549+
}
550+
551+
done(null, querystringParse(body.toString()));
552+
},
553+
);
554+
}
555+
536556
private async registerMiddie() {
537557
this.isMiddieRegistered = true;
538558
await this.register(

0 commit comments

Comments
 (0)