UNPKG

@loopback/rest

Version:

Expose controllers as REST endpoints and route REST API requests to controller methods

212 lines (194 loc) 6.27 kB
// Copyright IBM Corp. and LoopBack contributors 2018,2020. All Rights Reserved. // Node module: @loopback/rest // This file is licensed under the MIT License. // License text available at https://opensource.org/licenses/MIT import { compareByOrder, Constructor, Context, filterByTag, inject, instantiateClass, } from '@loopback/core'; import {isReferenceObject, OperationObject} from '@loopback/openapi-v3'; import debugModule from 'debug'; import {is} from 'type-is'; import {RestHttpErrors} from '../rest-http-error'; import {Request} from '../types'; import { builtinParsers, getContentType, normalizeParsingError, } from './body-parser.helpers'; import { BodyParser, BodyParserFunction, RequestBody, REQUEST_BODY_PARSER_TAG, } from './types'; const debug = debugModule('loopback:rest:body-parser'); export class RequestBodyParser { readonly parsers: BodyParser[]; constructor( @inject(filterByTag(REQUEST_BODY_PARSER_TAG), {optional: true}) parsers?: BodyParser[], @inject.context() private readonly ctx?: Context, ) { this.parsers = sortParsers(parsers ?? []); if (debug.enabled) { debug( 'Body parsers: ', this.parsers.map(p => p.name), ); } } async loadRequestBodyIfNeeded( operationSpec: OperationObject, request: Request, ): Promise<RequestBody> { const {requestBody, customParser} = await this._matchRequestBodySpec( operationSpec, request, ); if (!operationSpec.requestBody) return requestBody; const matchedMediaType = requestBody.mediaType!; try { if (customParser) { // Invoke the custom parser const body = await this._invokeCustomParser(customParser, request); debug('Parsed request body', body); return Object.assign(requestBody, body); } else { const parser = this._findParser(matchedMediaType); if (parser) { const body = await parser.parse(request); debug('Parsed request body', body); return Object.assign(requestBody, body); } } } catch (err) { debug('Request body parsing error', err); throw normalizeParsingError(err); } throw RestHttpErrors.unsupportedMediaType(matchedMediaType); } /** * Match the http request to a given media type of the request body spec */ private async _matchRequestBodySpec( operationSpec: OperationObject, request: Request, ) { const requestBody: RequestBody = { value: undefined, }; if (!operationSpec.requestBody) return {requestBody}; const contentType = getContentType(request) ?? 'application/json'; debug('Loading request body with content type %j', contentType); // the type of `operationSpec.requestBody` could be `RequestBodyObject` // or `ReferenceObject`, resolving a `$ref` value is not supported yet. if (isReferenceObject(operationSpec.requestBody)) { throw new Error('$ref requestBody is not supported yet.'); } let content = operationSpec.requestBody.content || {}; if (!Object.keys(content).length) { content = { // default to allow json and urlencoded 'application/json': {schema: {type: 'object'}}, 'application/x-www-form-urlencoded': {schema: {type: 'object'}}, }; } // Check of the request content type matches one of the expected media // types in the request body spec let matchedMediaType: string | false = false; let customParser = undefined; for (const type in content) { matchedMediaType = is(contentType, type); if (matchedMediaType) { debug('Matched media type: %s -> %s', type, contentType); requestBody.mediaType = contentType; requestBody.schema = content[type].schema; customParser = content[type]['x-parser']; break; } } if (!matchedMediaType) { // No matching media type found, fail fast throw RestHttpErrors.unsupportedMediaType( contentType, Object.keys(content), ); } return {requestBody, customParser}; } /** * Find a body parser that supports the media type * @param matchedMediaType - Media type */ private _findParser(matchedMediaType: string) { for (const parser of this.parsers) { if (!parser.supports(matchedMediaType)) { debug( 'Body parser %s does not support %s', parser.name, matchedMediaType, ); continue; } debug('Body parser %s found for %s', parser.name, matchedMediaType); return parser; } } /** * Resolve and invoke a custom parser * @param customParser - The parser name, class or function * @param request - Http request */ private async _invokeCustomParser( customParser: string | Constructor<BodyParser> | BodyParserFunction, request: Request, ) { if (typeof customParser === 'string') { const parser = this.parsers.find( p => p.name === customParser || p.name === builtinParsers.mapping[customParser], ); if (parser) { debug('Using custom parser %s', customParser); return parser.parse(request); } } else if (typeof customParser === 'function') { if (isBodyParserClass(customParser)) { debug('Using custom parser class %s', customParser.name); const parser = await instantiateClass<BodyParser>( customParser as Constructor<BodyParser>, this.ctx!, ); return parser.parse(request); } else { debug('Using custom parser function %s', customParser.name); return customParser(request); } } throw new Error('Custom parser not found: ' + customParser); } } /** * Test if a function is a body parser class or plain function * @param fn */ function isBodyParserClass( fn: Constructor<BodyParser> | BodyParserFunction, ): fn is Constructor<BodyParser> { return fn.toString().startsWith('class '); } /** * Sort body parsers so that built-in ones are used after extensions * @param parsers */ function sortParsers(parsers: BodyParser[]) { return parsers.sort((a, b) => compareByOrder(a.name, b.name, builtinParsers.names), ); }