88 isReferenceObject ,
99 OperationObject ,
1010 ParameterObject ,
11+ ReferenceObject ,
12+ SchemaObject ,
1113 SchemasObject ,
1214} from '@loopback/openapi-v3-types' ;
1315import * as debugModule from 'debug' ;
@@ -19,8 +21,15 @@ import {promisify} from 'util';
1921import { coerceParameter } from './coercion/coerce-parameter' ;
2022import { RestHttpErrors } from './index' ;
2123import { ResolvedRoute } from './router/routing-table' ;
22- import { OperationArgs , PathParameterValues , Request } from './types' ;
24+ import {
25+ OperationArgs ,
26+ PathParameterValues ,
27+ Request ,
28+ RequestBodyParserOptions ,
29+ } from './types' ;
2330import { validateRequestBody } from './validation/request-body.validator' ;
31+ import { is } from 'type-is' ;
32+ import * as qs from 'qs' ;
2433
2534type HttpError = HttpErrors . HttpError ;
2635
@@ -29,26 +38,30 @@ const debug = debugModule('loopback:rest:parser');
2938export const QUERY_NOT_PARSED = { } ;
3039Object . freeze ( QUERY_NOT_PARSED ) ;
3140
32- // tslint:disable-next-line:no-any
33- type MaybeBody = any | undefined ;
41+ // tslint:disable:no-any
42+ export type RequestBody = {
43+ value : any | undefined ;
44+ coercionRequired ?: boolean ;
45+ mediaType ?: string ;
46+ schema ?: SchemaObject | ReferenceObject ;
47+ } ;
48+
49+ const parseJsonBody : (
50+ req : IncomingMessage ,
51+ options : { } ,
52+ ) => Promise < any > = promisify ( require ( 'body/json' ) ) ;
3453
35- const parseJsonBody : ( req : IncomingMessage ) => Promise < MaybeBody > = promisify (
36- require ( 'body/json' ) ,
37- ) ;
54+ const parseFormBody : (
55+ req : IncomingMessage ,
56+ options : { } ,
57+ ) => Promise < any > = promisify ( require ( 'body/form' ) ) ;
3858
3959/**
4060 * Get the content-type header value from the request
4161 * @param req Http request
4262 */
4363function getContentType ( req : Request ) : string | undefined {
44- const val = req . headers [ 'content-type' ] ;
45- if ( typeof val === 'string' ) {
46- return val ;
47- } else if ( Array . isArray ( val ) ) {
48- // Assume only one value is present
49- return val [ 0 ] ;
50- }
51- return undefined ;
64+ return req . get ( 'content-type' ) ;
5265}
5366
5467/**
@@ -61,11 +74,12 @@ function getContentType(req: Request): string | undefined {
6174export async function parseOperationArgs (
6275 request : Request ,
6376 route : ResolvedRoute ,
77+ options : RequestBodyParserOptions = { } ,
6478) : Promise < OperationArgs > {
6579 debug ( 'Parsing operation arguments for route %s' , route . describe ( ) ) ;
6680 const operationSpec = route . spec ;
6781 const pathParams = route . pathParams ;
68- const body = await loadRequestBodyIfNeeded ( operationSpec , request ) ;
82+ const body = await loadRequestBodyIfNeeded ( operationSpec , request , options ) ;
6983 return buildOperationArguments (
7084 operationSpec ,
7185 request ,
@@ -75,32 +89,111 @@ export async function parseOperationArgs(
7589 ) ;
7690}
7791
78- async function loadRequestBodyIfNeeded (
92+ function normalizeParsingError ( err : HttpError ) {
93+ debug ( 'Cannot parse request body %j' , err ) ;
94+ if ( ! err . statusCode || err . statusCode >= 500 ) {
95+ err . statusCode = 400 ;
96+ }
97+ return err ;
98+ }
99+
100+ export async function loadRequestBodyIfNeeded (
79101 operationSpec : OperationObject ,
80102 request : Request ,
81- ) : Promise < MaybeBody > {
82- if ( ! operationSpec . requestBody ) return Promise . resolve ( ) ;
103+ options : RequestBodyParserOptions = { } ,
104+ ) : Promise < RequestBody > {
105+ const requestBody : RequestBody = {
106+ value : undefined ,
107+ } ;
108+ if ( ! operationSpec . requestBody ) return Promise . resolve ( requestBody ) ;
109+
110+ debug ( 'Request body parser options: %j' , options ) ;
83111
84- const contentType = getContentType ( request ) ;
112+ const contentType = getContentType ( request ) || 'application/json' ;
85113 debug ( 'Loading request body with content type %j' , contentType ) ;
86- if ( contentType && ! / j s o n / . test ( contentType ) ) {
87- throw new HttpErrors . UnsupportedMediaType (
88- `Content-type ${ contentType } is not supported.` ,
114+
115+ // the type of `operationSpec.requestBody` could be `RequestBodyObject`
116+ // or `ReferenceObject`, resolving a `$ref` value is not supported yet.
117+ if ( isReferenceObject ( operationSpec . requestBody ) ) {
118+ throw new Error ( '$ref requestBody is not supported yet.' ) ;
119+ }
120+
121+ let content = operationSpec . requestBody . content || { } ;
122+ if ( ! Object . keys ( content ) . length ) {
123+ content = {
124+ // default to allow json and urlencoded
125+ 'application/json' : { schema : { type : 'object' } } ,
126+ 'application/x-www-form-urlencoded' : { schema : { type : 'object' } } ,
127+ } ;
128+ }
129+
130+ // Check of the request content type matches one of the expected media
131+ // types in the request body spec
132+ let matchedMediaType : string | false = false ;
133+ for ( const type in content ) {
134+ matchedMediaType = is ( contentType , type ) ;
135+ if ( matchedMediaType ) {
136+ requestBody . mediaType = type ;
137+ requestBody . schema = content [ type ] . schema ;
138+ break ;
139+ }
140+ }
141+
142+ if ( ! matchedMediaType ) {
143+ // No matching media type found, fail fast
144+ throw RestHttpErrors . unsupportedMediaType (
145+ contentType ,
146+ Object . keys ( content ) ,
89147 ) ;
90148 }
91149
92- return await parseJsonBody ( request ) . catch ( ( err : HttpError ) => {
93- debug ( 'Cannot parse request body %j' , err ) ;
94- err . statusCode = 400 ;
95- throw err ;
96- } ) ;
150+ if ( is ( matchedMediaType , 'urlencoded' ) ) {
151+ try {
152+ const body = await parseFormBody (
153+ request ,
154+ // use `qs` modules to handle complex objects
155+ Object . assign (
156+ {
157+ querystring : {
158+ parse : ( str : string , cb : Function ) => {
159+ cb ( null , qs . parse ( str ) ) ;
160+ } ,
161+ } ,
162+ } ,
163+ options ,
164+ ) ,
165+ ) ;
166+ return Object . assign ( requestBody , {
167+ // form parser returns an object without prototype
168+ // create a new copy to simplify shouldjs assertions
169+ value : Object . assign ( { } , body ) ,
170+ // urlencoded body only provide string values
171+ // set the flag so that AJV can coerce them based on the schema
172+ coercionRequired : true ,
173+ } ) ;
174+ } catch ( err ) {
175+ throw normalizeParsingError ( err ) ;
176+ }
177+ }
178+
179+ if ( is ( matchedMediaType , 'json' ) ) {
180+ try {
181+ const jsonBody = await parseJsonBody ( request , options ) ;
182+ requestBody . value = jsonBody ;
183+ return requestBody ;
184+ } catch ( err ) {
185+ throw normalizeParsingError ( err ) ;
186+ }
187+ }
188+
189+ throw RestHttpErrors . unsupportedMediaType ( matchedMediaType ) ;
97190}
98191
99192function buildOperationArguments (
100193 operationSpec : OperationObject ,
101194 request : Request ,
102195 pathParams : PathParameterValues ,
103- body : MaybeBody ,
196+ body : RequestBody ,
104197 globalSchemas : SchemasObject ,
105198) : OperationArgs {
106199 let requestBodyIndex : number = - 1 ;
@@ -131,7 +224,7 @@ function buildOperationArguments(
131224 debug ( 'Validating request body - value %j' , body ) ;
132225 validateRequestBody ( body , operationSpec . requestBody , globalSchemas ) ;
133226
134- if ( requestBodyIndex > - 1 ) paramArgs . splice ( requestBodyIndex , 0 , body ) ;
227+ if ( requestBodyIndex > - 1 ) paramArgs . splice ( requestBodyIndex , 0 , body . value ) ;
135228 return paramArgs ;
136229}
137230
0 commit comments