@loopback/rest
Version:
Expose controllers as REST endpoints and route REST API requests to controller methods
212 lines (194 loc) • 6.27 kB
text/typescript
// 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(
true})
parsers?: BodyParser[],
.context() private readonly ctx?: Context,
) {
(filterByTag(REQUEST_BODY_PARSER_TAG), {optional: 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),
);
}