Parsing Requests

This is an action in the default HTTP sequence, it parses arguments from anincoming request and uses them as inputs to invoke the corresponding controllermethod.

This action contains 3 steps:

  • Parses arguments from request query, body, path and header according to theoperation’s OpenAPI specification.
  • Coerces parameters from string to its corresponding JavaScript run-time type.
  • Performs validation on the parameters and body data.

Parsing Raw Data

The code below defines a typical endpoint by decorating a controller method withrest decorators.

  1. class TodoController {
  2. constructor(@repository(TodoRepository) protected todoRepo: TodoRepository) {}
  3. @put('/todos/{id}')
  4. async replaceTodo(
  5. @param.path.number('id') id: number,
  6. @requestBody() todo: Todo,
  7. ): Promise<boolean> {
  8. return this.todoRepo.replaceById(id, todo);
  9. }
  10. }

An OpenAPI operation specification will be generated in-memory to describe it,and raw data is parsed from request according to the specification. In theexample above, the first parameter is from source path, so its value will beparsed from a request’s path.

Controller documentation

See controllers for more details of defining an endpoint.

OpenAPI operation object

See OpenAPI operation objectto know more about its structure.

Coercion

The parameters parsed from path, header, and query of a http request are alwaysin the string format when using the http module in Node.js to handle requests.Therefore when invoking a controller function, a parameter need to be convertedto its corresponding JavaScript runtime type, which is inferred from itsparameter specification.

For example, the operation replaceTodo in sectionparsing raw data takes in a number id as the first input.Without coercion,id would have to be manually cast into the number type beforeit can be used as seen below:

  1. @put('/todos/{id}')
  2. async replaceTodo(
  3. @param.path.number('id') id: number,
  4. @requestBody() todo: Todo,
  5. ): Promise<boolean> {
  6. // NO need to do the "string to number" convertion now,
  7. // coercion automatically handles it for you.
  8. id = +id;
  9. return this.todoRepo.replaceById(id, todo);
  10. }

Object values

OpenAPI specification describes several ways how to encode object values into astring, seeStyle ValuesandStyle Examples.

At the moment, LoopBack supports object values for parameters in query stringswith style: "deepObject" only. Please note that this style does not preserveencoding of primitive types, numbers and booleans are always parsed as strings.

For example:

  1. GET /todos?filter[where][completed]=false
  2. // filter={where: {completed: 'false'}}

As an extension to the deep-object encoding described by OpenAPI, when theparameter is specified with style: "deepObject", we allow clients to providethe object value as a JSON-encoded string too.

For example:

  1. GET /todos?filter={"where":{"completed":false}}
  2. // filter={where: {completed: false}}

Validation

Validations are applied on the parameters and the request body data. They alsouse OpenAPI specification as the reference to infer the validation rules.

Parameters

We have the data type safety check for the parameters parsed from header, path,and query. For example, if a parameter should be an integer, then a number withdecimal like “1.23” would be rejected.

You can specify a parameter’s type by calling shortcut decorators of @paramlike @param.query.integer(). A list of available shortcuts can be found in theAPI Docs. Checkout the section on parameter decorators forinstructions on how to decorate the controller parameter.

Here are our default validation rules for each type:

  • number: validated by isNaN(Number(data)).
  • integer: validated by Number.isInteger(data).
  • long: validated by Number.isSafeInteger(data).
  • date-time: should be a valid date-time defined inRFC3339.
  • date: should be a valid full-date defined inRFC3339.
  • boolean: after converted to all upper case, should be one of the followingvalues: TRUE, 1, FALSE or 0.
  • object: should be a plain data object, not an array.

Request Body

The data from request body is validated against its OpenAPI schemaspecification. We use AJV module toperform the validation, which validates data with a JSON schema generated fromthe OpenAPI schema specification.

Take again the operation replaceTodo for instance:

  1. import {Todo} from './models';
  2. // class definition
  3. ...
  4. @put('/todos/{id}')
  5. async replaceTodo(
  6. @param.path.number('id') id: number,
  7. @requestBody() todo: Todo,
  8. ): Promise<boolean> {
  9. return this.todoRepo.replaceById(id, todo);
  10. }
  11. ...

The request body specification is defined by applying @requestBody() toargument todo, and the schema specification inside it is inferred from itstype Todo. The type is exported from a Todo model.

Model documentation

See model to know more details about how to decorate a model class.

When the PUT method on the /todo/{id} gets called, the todo instance fromthe request body will be validated with a well defined specification.

Validation of model objects is heavily dependent on its OpenAPI Schema definedin/by the @requestBody decorator. Please refer to the documentation on@requestBody decorator to get acomprehensive idea of defining custom validation rules for your models.

You can also specify the JSON schema validation rules in the model propertydecorator. The rules are added in a field called jsonSchema, like:

  1. @model()
  2. class Product extends Entity {
  3. @property({
  4. name: 'name',
  5. description: "The product's common name.",
  6. type: 'string',
  7. // Specify the JSON validation rules here
  8. jsonSchema: {
  9. maxLength: 30,
  10. minLength: 10,
  11. },
  12. })
  13. public name: string;
  14. }

A full list of validation keywords could be found in thedocumentation of AJV validation keywords.

One request body specification could contain multiple content types. Oursupported content types are json, urlencoded, and text. The client shouldset Content-Type http header to application/json,application/x-www-form-urlencoded, or text/plain. Its value is matchedagainst the list of media types defined in the requestBody.content object ofthe OpenAPI operation spec. If no matching media types is found or the type isnot supported yet, an UnsupportedMediaTypeError (http statusCode 415) will bereported.

Please note that urlencoded media type does not support data typing. Forexample, key=3 is parsed as {key: '3'}. The raw result is then coerced byAJV based on the matching content schema. The coercion rules are described inAJV type coercion rules.

The qs is used to parse complex strings. Forexample, given the following request body definition:

  1. const requestBodyObject = {
  2. description: 'data',
  3. content: {
  4. 'application/x-www-form-urlencoded': {
  5. schema: {
  6. type: 'object',
  7. properties: {
  8. name: {type: 'string'},
  9. location: {
  10. type: 'object',
  11. properties: {
  12. lat: {type: 'number'},
  13. lng: {type: 'number'},
  14. },
  15. },
  16. tags: {
  17. type: 'array',
  18. items: {type: 'string'},
  19. },
  20. },
  21. },
  22. },
  23. },
  24. };

The encoded value'name=IBM%20HQ&location[lat]=0.741895&location[lng]=-73.989308&tags[0]=IT&tags[1]=NY'is parsed and coerced as:

  1. {
  2. name: 'IBM HQ',
  3. location: {lat: 0.741895, lng: -73.989308},
  4. tags: ['IT', 'NY'],
  5. }

The request body parser options (such as limit) can now be configured bybinding the value to RestBindings.REQUEST_BODY_PARSER_OPTIONS(‘rest.requestBodyParserOptions’). For example,

  1. server.bind(RestBindings.REQUEST_BODY_PARSER_OPTIONS).to({
  2. limit: '4MB',
  3. });

The options can be media type specific, for example:

  1. server.bind(RestBindings.REQUEST_BODY_PARSER_OPTIONS).to({
  2. json: {limit: '4MB'},
  3. text: {limit: '1MB'},
  4. });

The list of options can be found in thebody-parser module.

By default, the limit is 1MB. Any request with a body length exceeding thelimit will be rejected with http status code 413 (request entity too large).

A few tips worth mentioning:

  • If a model property’s type refers to another model, make sure it is alsodecorated with @model decorator.

  • If you’re using API first development approach, you can also provide therequest body specification in decorators like route() andapi(), this requires you to provide acompleted request body specification.

Extend Request Body Parsing

See Extending request body parsing formore details.

Specify Custom Parser by Controller Methods

In some cases, a controller method wants to handle request body parsing byitself, such as, to accept multipart/form-data for file uploads or stream-linea large json document. To bypass body parsing, the 'x-parser' extension can beset to 'stream' for a media type of the request body content. For example,

  1. class FileUploadController {
  2. async upload(
  3. @requestBody({
  4. description: 'multipart/form-data value.',
  5. required: true,
  6. content: {
  7. 'multipart/form-data': {
  8. // Skip body parsing
  9. 'x-parser': 'stream',
  10. schema: {type: 'object'},
  11. },
  12. },
  13. })
  14. request: Request,
  15. @inject(RestBindings.Http.RESPONSE) response: Response,
  16. ): Promise<object> {
  17. const storage = multer.memoryStorage();
  18. const upload = multer({storage});
  19. return new Promise<object>((resolve, reject) => {
  20. upload.any()(request, response, err => {
  21. if (err) reject(err);
  22. else {
  23. resolve({
  24. files: request.files,
  25. // eslint-disable-next-line @typescript-eslint/no-explicit-any
  26. fields: (request as any).fields,
  27. });
  28. }
  29. });
  30. });
  31. }
  32. }

The x-parser value can be one of the following:

  • Name of the parser, such as json, raw, or stream
  • stream: keeps the http request body as a stream without parsing
  • raw: parses the http request body as a Buffer
  1. {
  2. 'x-parser': 'stream'
  3. }
  • A body parser class
  1. {
  2. 'x-parser': JsonBodyParser
  3. }
  • A body parser function, for example:
  1. function parseJson(request: Request): Promise<RequestBody> {
  2. return new JsonBodyParser().parse(request);
  3. }
  4. {
  5. 'x-parser': parseJson
  6. }

Localizing Errors

A body data may break multiple validation rules, like missing required fields,data in a wrong type, data that exceeds the maximum length, etc…The validationerrors are returned in batch mode, and user can find all of them inerror.details, which describes errors in a machine-readable way.

Each element in the error.details array reports one error. It contains 4attributes:

  • path: The path to the invalid field.
  • code: A single word code represents the error’s type.
  • message: A human readable description of the error.
  • info: Some additional details that the 3 attributes above don’t cover.In most cases path shows which field in the body data is invalid. For example,if an object schema’s id field should be a string, while the data in body hasit as a number: {id: 1, name: 'Foo'}. Then the error entry is:
  1. {
  2. path: '.id',
  3. code: 'type',
  4. message: 'should be string',
  5. info: {type: 'string'},
  6. }

And in this case the error code is type. A reference of all the possible codecould be found inajv validation error keywords(codes).

In some exception scenarios, like a required field is missing, the path isempty, but the field location is easy to find in message and info. Forexample, id is a required field while it’s missing in a request body:{name: 'Foo'}, the error entry will be:

  1. {
  2. // `path` is empty
  3. path: '',
  4. code: 'required',
  5. message: "should have required property 'id'",
  6. // you can parse the missing field from `info.missingProperty`
  7. info: {missingProperty: 'id'},
  8. },