fix(core, middleware-body): incorrectly parsed querystrings
MODIFY
packages/@integration/test/integration.spec.ts
packages/@integration/test/integration.spec.ts
@@ -98,6 +98,15 @@ describe('API integration', () => {
.send({ user: { id: 'test_id' } })
.expect(200, { data: { user: { id: 'test_id' } } }));
+ // tslint:disable-next-line:max-line-length
+ test(`parses POST ${ContentType.APPLICATION_X_WWW_FORM_URLENCODED} body and echoes back for secured route: /api/v1/user`, async () =>
+ request(server)
+ .post('/api/v1/user')
+ .set('Authorization', 'Bearer FAKE')
+ .set('Content-Type', ContentType.APPLICATION_X_WWW_FORM_URLENCODED)
+ .send({ user: { id: 'test_id' } })
+ .expect(200, { data: { user: { id: 'test_id' } } }));
+
test(`returns static file as ${ContentType.TEXT_HTML}: /api/v1/static/index.html`, async () =>
request(server)
.get('/api/v1/static/index.html')

MODIFY
packages/core/package.json
packages/core/package.json
"rxjs": "~6.3.3"
},
"dependencies": {
+ "@types/qs": "^6.5.1",
"chalk": "~2.4.1",
"file-type": "^8.0.0",
+ "fp-ts": "1.12.2",
"mime": "^2.3.1",
"path-to-regexp": "^2.4.0",
- "fp-ts": "1.12.2"
+ "qs": "^6.6.0"
},
"devDependencies": {
"@types/file-type": "^5.2.1",

MODIFY
packages/core/src/router/router.query.factory.ts
packages/core/src/router/router.query.factory.ts
-import * as querystring from 'querystring';
-import { getHead } from '../+internal';
+import * as qs from 'qs';
import { QueryParameters } from '../http.interface';
import { fromNullable } from 'fp-ts/lib/Option';
-const getNestedQueryParam = (params: Object) => (key: string): QueryParameters => {
- const paramValue = params[key];
- const nestedKeys = /\[.*]/.exec(key);
-
- return fromNullable(nestedKeys)
- .chain(getHead)
- .map(_ => _.replace(/\[/g, ''))
- .map(_ => _.split(']'))
- .map(_ => _.filter(v => v.length > 0))
- .map(_ => _.reduceRight(
- (value, key) => ({ [key]: value }),
- paramValue,
- ))
- .chain(nestedQueryObject => fromNullable(nestedKeys)
- .chain(getHead)
- .map(head => key.replace(head, ''))
- .map(key => ({ [key]: nestedQueryObject }))
- )
- .getOrElse({ [key]: paramValue });
-};
-
-const extractNestedQueryParams = (queryParams: Object): QueryParameters =>
- Object.assign(
- {},
- ...Object.keys(queryParams).map(getNestedQueryParam(queryParams))
- );
-
export const queryParamsFactory = (queryParams: string | undefined | null): QueryParameters =>
fromNullable(queryParams)
- .map(querystring.parse)
- .map(extractNestedQueryParams)
+ .map(qs.parse)
.getOrElse({});

MODIFY
packages/middleware-body/package.json
packages/middleware-body/package.json
"rxjs": "~6.3.3"
},
"dependencies": {
+ "@types/qs": "^6.5.1",
"@types/type-is": "~1.6.2",
+ "qs": "^6.6.0",
"type-is": "~1.6.16"
},
"devDependencies": {

MODIFY
packages/middleware-body/src/parsers/url.body.parser.ts
packages/middleware-body/src/parsers/url.body.parser.ts
-import { compose } from '@marblejs/core/dist/+internal';
+import * as qs from 'qs';
import { RequestBodyParser } from '../body.model';
-const decodeComponent = (urlEncoded: string) => urlEncoded
- .split('&')
- .map(x => x.split('='))
- .reduce((data, [key, value]) => ({
- ...data,
- [key]: isNaN(+value) ? value : +value,
- }), {});
-
export const urlEncodedParser: RequestBodyParser = _ => body =>
- compose(decodeComponent, decodeURIComponent)(body.toString());
+ qs.parse(body.toString());

MODIFY
packages/middleware-body/src/specs/body.middleware.spec.ts
packages/middleware-body/src/specs/body.middleware.spec.ts
import { HttpRequest, HttpResponse, createStaticInjectionContainer, createEffectMetadata } from '@marblejs/core';
import { Marbles, ContentType } from '@marblejs/core/dist/+internal';
+import * as qs from 'qs';
import { of } from 'rxjs';
import { bodyParser$ } from '../body.middleware';
@@ -51,17 +52,65 @@ describe('bodyParser$ middleware', () => {
const req$ = of(request as HttpRequest);
const res = {} as HttpResponse;
const http$ = bodyParser$()(req$, res, effectMeta);
-
- http$.subscribe(data => {
- expect(data.body).toEqual({
+ const body = {
test: 'test',
'test-2': 'test-2',
- 'test-3': 3,
+ 'test-3': '3',
+ };
+
+ http$.subscribe(data => {
+ expect(data.body).toEqual(body);
+ done();
});
+
+ request.write(qs.stringify(body));
+ request.end();
+ });
+
+ test(`parses complex ${ContentType.APPLICATION_X_WWW_FORM_URLENCODED} body`, done => {
+ const request = new MockReq({
+ method: 'POST',
+ headers: { 'Content-Type': ContentType.APPLICATION_X_WWW_FORM_URLENCODED },
+ });
+ const req$ = of(request as HttpRequest);
+ const res = {} as HttpResponse;
+ const http$ = bodyParser$()(req$, res, effectMeta);
+ const body = {
+ test1: 'field=with=equals&and&ampersands',
+ test2: 'field=with=equals&and&ampersands',
+ };
+
+ http$.subscribe(data => {
+ expect(data.body).toEqual(body);
+ done();
+ });
+
+ request.write(qs.stringify(body));
+ request.end();
+ });
+
+ test(`parses array-like ${ContentType.APPLICATION_X_WWW_FORM_URLENCODED} body`, done => {
+ const request = new MockReq({
+ method: 'POST',
+ headers: { 'Content-Type': ContentType.APPLICATION_X_WWW_FORM_URLENCODED },
+ });
+ const req$ = of(request as HttpRequest);
+ const res = {} as HttpResponse;
+ const http$ = bodyParser$()(req$, res, effectMeta);
+ const body = {
+ children: [
+ { bar: 'foo', foo: 'bar' },
+ { bar: 'foo', foo: 'bar' },
+ ],
+ somekey: 'value',
+ };
+
+ http$.subscribe(data => {
+ expect(data.body).toEqual(body),
done();
});
- request.write('test=test&test-2=test-2&test-3=3');
+ request.write(qs.stringify(body));
request.end();
});

MODIFY
packages/middleware-body/test/bodyParser.integration.spec.ts
packages/middleware-body/test/bodyParser.integration.spec.ts
@@ -3,29 +3,29 @@ import * as request from 'supertest';
import { app } from './bodyParser.integration';
describe('@marblejs/middleware-body - integration', () => {
- const body = { id: 'id', name: 'name', age: 100 };
- const text = 'test message';
-
describe('POST /default-parser', () => {
test(`parses ${ContentType.APPLICATION_JSON} content-type`, async () =>
request(app)
.post('/default-parser')
.set({ 'Content-Type': ContentType.APPLICATION_JSON })
- .send(body)
- .expect(200, body)
+ .send({ id: 'id', name: 'name', age: 100 })
+ .expect(200, { id: 'id', name: 'name', age: 100 })
);
test(`parses ${ContentType.APPLICATION_X_WWW_FORM_URLENCODED} content-type`, async () =>
request(app)
.post('/default-parser')
.set({ 'Content-Type': ContentType.APPLICATION_X_WWW_FORM_URLENCODED })
- .send(body)
- .expect(200, body)
+ .send({ id: 'id', name: 'name', age: 100 })
+ .expect(200, { id: 'id', name: 'name', age: '100' })
);
});
describe('POST /multiple-parsers', () => {
+ const body = { id: 'id', name: 'name', age: 100 };
+ const text = 'test message';
+
test(`parses ${ContentType.APPLICATION_JSON} content-type`, async () =>
request(app)
.post('/multiple-parsers')
@@ -42,7 +42,7 @@ describe('@marblejs/middleware-body - integration', () => {
.expect(200, body)
);
- test(`parses custom ${ContentType.APPLICATION_VND_API_JSON} content-type`, async () =>
+ test(`parses ${ContentType.APPLICATION_VND_API_JSON} content-type`, async () =>
request(app)
.post('/multiple-parsers')
.set({ 'Content-Type': ContentType.APPLICATION_VND_API_JSON })
@@ -50,7 +50,7 @@ describe('@marblejs/middleware-body - integration', () => {
.expect(200, body)
);
- test(`parses custom ${ContentType.TEXT_PLAIN} content-type`, async () =>
+ test(`parses ${ContentType.TEXT_PLAIN} content-type`, async () =>
request(app)
.post('/multiple-parsers')
.set({ 'Content-Type': ContentType.TEXT_PLAIN })
@@ -58,7 +58,7 @@ describe('@marblejs/middleware-body - integration', () => {
.expect(200, `"${text}"`)
);
- test(`parses custom ${ContentType.APPLICATION_OCTET_STREAM} content-type`, async () =>
+ test(`parses ${ContentType.APPLICATION_OCTET_STREAM} content-type`, async () =>
request(app)
.post('/multiple-parsers')
.set({ 'Content-Type': ContentType.APPLICATION_OCTET_STREAM })

MODIFY
yarn.lock
yarn.lock
version "10.12.15"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.12.15.tgz#20e85651b62fd86656e57c9c9bc771ab1570bc59"
+"@types/qs@^6.5.1":
+ version "6.5.1"
+ resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.5.1.tgz#a38f69c62528d56ba7bd1f91335a8004988d72f7"
+ integrity sha512-mNhVdZHdtKHMMxbqzNK3RzkBcN1cux3AvuCYGTvjEIQT2uheH3eCAyYsbMbh2Bq8nXkeOWs1kyDiF7geWRFQ4Q==
+
"@types/superagent@*":
version "3.8.5"
resolved "https://registry.yarnpkg.com/@types/superagent/-/superagent-3.8.5.tgz#a8eb8ec5bce0234e50d6b8bee1941b5bb5686e2d"
@@ -5060,7 +5065,7 @@ q@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7"
-qs@^6.5.1:
+qs@^6.5.1, qs@^6.6.0:
version "6.6.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.6.0.tgz#a99c0f69a8d26bf7ef012f871cdabb0aee4424c2"

fix(core, middleware-body): incorrectly parsed querystrings