fix(middleware-body): proper content-type charset support
MODIFY
packages/middleware-body/package.json
packages/middleware-body/package.json
"dependencies": {
"@types/qs": "^6.5.1",
"@types/type-is": "~1.6.2",
+ "@types/content-type": "1.1.2",
"qs": "^6.6.0",
+ "content-type": "~1.0.4",
"type-is": "~1.6.16"
},
"devDependencies": {

MODIFY
packages/middleware-body/src/body.middleware.ts
packages/middleware-body/src/body.middleware.ts
import { HttpError, HttpStatus, HttpMiddlewareEffect } from '@marblejs/core';
-import { of, throwError, iif } from 'rxjs';
+import { of, throwError } from 'rxjs';
import { catchError, map, tap, mapTo, mergeMap } from 'rxjs/operators';
+import { pipe } from 'fp-ts/lib/function';
import { defaultParser } from './parsers';
import { RequestBodyParser } from './body.model';
import { matchType, getBody, hasBody, isMultipart } from './body.util';
@@ -17,21 +18,19 @@ export const bodyParser$ = ({
parser = defaultParser,
}: BodyParserOptions = {}): HttpMiddlewareEffect => req$ =>
req$.pipe(
- mergeMap(req => iif(
- () =>
+ mergeMap(req =>
PARSEABLE_METHODS.includes(req.method)
&& !hasBody(req)
&& !isMultipart(req)
- && matchType(type)(req),
- of(req).pipe(
- mergeMap(getBody),
+ && matchType(type)(req)
+ ? pipe(
+ getBody(req),
map(parser(req)),
tap(body => req.body = body),
mapTo(req),
- catchError(() => throwError(
- new HttpError('Request body parse error', HttpStatus.BAD_REQUEST, undefined, req),
- ))
+ catchError(error => throwError(
+ new HttpError(`Request body parse error: "${error.toString()}"`, HttpStatus.BAD_REQUEST, undefined, req),
+ )))
+ : of(req),
),
- of(req),
- )),
);

MODIFY
packages/middleware-body/src/body.util.ts
packages/middleware-body/src/body.util.ts
import * as typeIs from 'type-is';
+import { pipe } from 'fp-ts/lib/function';
+import { Observable } from 'rxjs';
+import { map, toArray } from 'rxjs/operators';
import { HttpRequest } from '@marblejs/core';
import { getContentType } from '@marblejs/core/dist/+internal/http';
import { fromReadableStream } from '@marblejs/core/dist/+internal/observable';
-import { map, toArray } from 'rxjs/operators';
-export const matchType = (type: string[]) => (req: HttpRequest) =>
+export const matchType = (type: string[]) => (req: HttpRequest): boolean =>
!!typeIs.is(getContentType(req.headers), type);
export const isMultipart = (req: HttpRequest): boolean =>
@@ -13,8 +15,9 @@ export const isMultipart = (req: HttpRequest): boolean =>
export const hasBody = (req: HttpRequest): boolean =>
req.body !== undefined && req.body !== null;
-export const getBody = (req: HttpRequest) =>
- fromReadableStream(req).pipe(
+export const getBody = (req: HttpRequest): Observable<Buffer> =>
+ pipe(
+ fromReadableStream(req),
toArray(),
map(chunks => Buffer.concat(chunks)),
);

MODIFY
packages/middleware-body/src/parsers/default.body.parser.ts
packages/middleware-body/src/parsers/default.body.parser.ts
-import { ContentType, getContentType } from '@marblejs/core/dist/+internal/http';
+import * as typeis from 'type-is';
+import { getContentType } from '@marblejs/core/dist/+internal/http';
import { RequestBodyParser } from '../body.model';
import { jsonParser } from './json.body.parser';
import { textParser } from './text.body.parser';
import { rawParser } from './raw.body.parser';
import { urlEncodedParser } from './url.body.parser';
+const SUPPORTED_CONTENT_TYPES = ['json', 'urlencoded', 'application/octet-stream', 'text', 'html'];
+
export const defaultParser: RequestBodyParser = reqOrContentType => body => {
const contentType = typeof reqOrContentType === 'string'
? reqOrContentType
: getContentType(reqOrContentType.headers);
- switch (contentType) {
- case ContentType.APPLICATION_JSON:
+ switch (typeis.is(contentType, SUPPORTED_CONTENT_TYPES)) {
+ case 'json':
return jsonParser(reqOrContentType)(body);
- case ContentType.APPLICATION_X_WWW_FORM_URLENCODED:
+ case 'urlencoded':
return urlEncodedParser(reqOrContentType)(body);
- case ContentType.APPLICATION_OCTET_STREAM:
+ case 'application/octet-stream':
return rawParser(reqOrContentType)(body);
- case ContentType.TEXT_PLAIN:
+ case 'text':
+ case 'html':
return textParser(reqOrContentType)(body);
default:
return undefined;

MODIFY
packages/middleware-body/src/parsers/json.body.parser.ts
packages/middleware-body/src/parsers/json.body.parser.ts
+import * as contentType from 'content-type';
+import { pipe } from 'fp-ts/lib/function';
import { RequestBodyParser } from '../body.model';
-export const jsonParser: RequestBodyParser = _ => body =>
- JSON.parse(body.toString());
+export const jsonParser: RequestBodyParser = reqOrContentType => body =>
+ pipe(
+ contentType.parse(reqOrContentType),
+ parsedContentType => body.toString(parsedContentType.parameters.charset),
+ stringifiedBody => JSON.parse(stringifiedBody),
+ );

MODIFY
packages/middleware-body/src/parsers/text.body.parser.ts
packages/middleware-body/src/parsers/text.body.parser.ts
+import * as contentType from 'content-type';
+import { pipe } from 'fp-ts/lib/function';
import { RequestBodyParser } from '../body.model';
-export const textParser: RequestBodyParser = _ => body =>
- body.toString();
+export const textParser: RequestBodyParser = reqOrContentType => body =>
+ pipe(
+ contentType.parse(reqOrContentType),
+ parsedContentType => body.toString(parsedContentType.parameters.charset),
+ );

MODIFY
packages/middleware-body/src/parsers/url.body.parser.ts
packages/middleware-body/src/parsers/url.body.parser.ts
import * as qs from 'qs';
+import * as contentType from 'content-type';
+import { pipe } from 'fp-ts/lib/function';
import { RequestBodyParser } from '../body.model';
-export const urlEncodedParser: RequestBodyParser = _ => body =>
- qs.parse(body.toString());
+export const urlEncodedParser: RequestBodyParser = reqOrContentType => body =>
+ pipe(
+ contentType.parse(reqOrContentType),
+ parsedContentType => body.toString(parsedContentType.parameters.charset),
+ stringifiedBody => qs.parse(stringifiedBody),
+ );

MODIFY
yarn.lock
yarn.lock
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
+"@types/content-type@1.1.2":
+ version "1.1.2"
+ resolved "https://registry.yarnpkg.com/@types/content-type/-/content-type-1.1.2.tgz#bff0a1b7aa5bd48516c8ed7a6efefb1e85813245"
+ integrity sha512-w2d7fBCYbCBUBTGtkC4JfX1FicTtgEmq7wTTjc7rC5RA/JdB1Bi7o88nKzUqAnIIBXJVmq0n4tTmF3PJN8QqCg==
+
"@types/events@*":
version "1.2.0"
resolved "https://registry.npmjs.org/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
@@ -2919,6 +2924,11 @@ contains-path@^0.1.0:
resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a"
integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=
+content-type@~1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
+ integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+
conventional-changelog-angular@^1.3.3:
version "1.6.6"
resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-1.6.6.tgz#b27f2b315c16d0a1f23eb181309d0e6a4698ea0f"

fix(middleware-body): proper content-type charset support