Skip to content

Commit 86bfcbc

Browse files
committed
feat(rest): allow body parsers to be extended
- add `bodyParser` sugar method - use `x-parser` to control custom body parsing - add docs for body parser extensions - add raw body parser
1 parent 084837f commit 86bfcbc

34 files changed

+2008
-409
lines changed
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
---
2+
lang: en
3+
title: 'Extending request body parsing'
4+
keywords: LoopBack 4.0, LoopBack 4
5+
sidebar: lb4_sidebar
6+
permalink: /doc/en/lb4/Extending-request-body-parsing.html
7+
---
8+
9+
## Parsing requests
10+
11+
LoopBack 4 uses the `Content-Type` header and `requestBody` of the OpenAPI spec
12+
to parse the body of http requests. Please see
13+
[Parsing requests](./Parsing-requests.md) for more details.
14+
15+
The `@loopback/rest` module ships a set of built-in body parsers:
16+
17+
- `json`: parses the http request body as a json value (object, array, string,
18+
number, boolean, null)
19+
- `urlencoded`: decodes the http request body from
20+
'application/x-www-form-urlencoded'
21+
- `text`: parses the http request body as a `string`
22+
- `stream`: keeps the http request body as a stream without parsing
23+
- `raw`: parses the http request body as a `Buffer`
24+
25+
To support more media types, LoopBack defines extension points to plug in body
26+
parsers to parse the request body. LoopBack's request body parsing capability
27+
can be extended in the following ways:
28+
29+
## Adding a new parser
30+
31+
To add a new body parser, follow the steps below:
32+
33+
1. Define a class that implements the `BodyParser` interface:
34+
35+
```ts
36+
/**
37+
* Interface to be implemented by body parser extensions
38+
*/
39+
export interface BodyParser {
40+
/**
41+
* Name of the parser
42+
*/
43+
name: string | symbol;
44+
/**
45+
* Indicate if the given media type is supported
46+
* @param mediaType Media type
47+
*/
48+
supports(mediaType: string): boolean;
49+
/**
50+
* Parse the request body
51+
* @param request http request
52+
*/
53+
parse(request: Request): Promise<RequestBody>;
54+
}
55+
```
56+
57+
A body parser implementation class will be instantiated by the LoopBack runtime
58+
within the context and it can leverage dependency injections. For example:
59+
60+
```ts
61+
export class JsonBodyParser implements BodyParser {
62+
name = 'json';
63+
private jsonParser: BodyParserMiddleware;
64+
65+
constructor(
66+
@inject(RestBindings.REQUEST_BODY_PARSER_OPTIONS, {optional: true})
67+
options: RequestBodyParserOptions = {},
68+
) {
69+
const jsonOptions = getParserOptions('json', options);
70+
this.jsonParser = json(jsonOptions);
71+
}
72+
// ...
73+
}
74+
```
75+
76+
See the complete code at
77+
https://github.com/strongloop/loopback-next/blob/master/packages/rest/src/body-parsers/body-parser.json.ts.
78+
79+
2. Bind the body parser class to your REST server/application:
80+
81+
For example,
82+
83+
```ts
84+
server.bodyParser(XmlBodyParser);
85+
```
86+
87+
The `bodyParser` api binds `XmlBodyParser` to the context with:
88+
89+
- key: `request.bodyParser.XmlBodyParser`
90+
- tag: `request.bodyParser`
91+
92+
Please note that newly added body parsers are always invoked before the built-in
93+
ones.
94+
95+
### Contribute a body parser from a component
96+
97+
A component can add one or more body parsers via its bindings property:
98+
99+
```ts
100+
import {createBodyParserBinding} from '@loopback/rest';
101+
102+
export class XmlComponent implements Component {
103+
bindings = [createBodyParserBinding(XmlBodyParser)];
104+
}
105+
```
106+
107+
### Customize parser options
108+
109+
The request body parser options is bound to
110+
`RestBindings.REQUEST_BODY_PARSER_OPTIONS`. To customize request body parser
111+
options, you can simply bind a new value to its key.
112+
113+
Built-in parsers retrieve their own options from the request body parser
114+
options. The parser specific properties override common ones. For example, given
115+
the following configuration:
116+
117+
```ts
118+
{
119+
limit: '1MB'
120+
json: {
121+
strict: false
122+
},
123+
text: {
124+
limit: '2MB'
125+
}
126+
}
127+
```
128+
129+
The json parser will be created with `{limit: '1MB', strict: false}` and the
130+
text parser with `{limit: '2MB'}`.
131+
132+
Custom parsers can choose to have its own `options` from the context by
133+
dependency injection, for example:
134+
135+
```ts
136+
export class XmlBodyParser implements BodyParser {
137+
name = 'xml';
138+
139+
constructor(
140+
@inject('request.bodyParsers.xml.options', {optional: true})
141+
options: XmlBodyParserOptions = {},
142+
) {
143+
...
144+
}
145+
// ...
146+
}
147+
```
148+
149+
## Replace an existing parser
150+
151+
An existing parser can be replaced by binding a different value to the
152+
application context.
153+
154+
```ts
155+
class MyJsonBodyParser implements BodyParser {
156+
// ...
157+
}
158+
app.bodyParser(MyJsonBodyParser, RestBindings.REQUEST_BODY_PARSER_JSON);
159+
```
160+
161+
## Remove an existing parser
162+
163+
An existing parser can be removed from the application by unbinding the
164+
corresponding key. For example, the following code removes the built-in JSON
165+
body parser.
166+
167+
```ts
168+
app.unbind(RestBindings.REQUEST_BODY_PARSER_JSON);
169+
```

docs/site/Parsing-requests.md

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,13 @@ in/by the `@requestBody` decorator. Please refer to the documentation on
179179
[@requestBody decorator](Decorators.md#requestbody-decorator) to get a
180180
comprehensive idea of defining custom validation rules for your models.
181181

182-
We support `json` and `urlencoded` content types. The client should set
183-
`Content-Type` http header to `application/json` or
184-
`application/x-www-form-urlencoded`. Its value is matched against the list of
185-
media types defined in the `requestBody.content` object of the OpenAPI operation
186-
spec. If no matching media types is found or the type is not supported yet, an
187-
UnsupportedMediaTypeError (http statusCode 415) will be reported.
182+
We support `json`, `urlencoded`, and `text` content types. The client should set
183+
`Content-Type` http header to `application/json`,
184+
`application/x-www-form-urlencoded`, or `text/plain`. Its value is matched
185+
against the list of media types defined in the `requestBody.content` object of
186+
the OpenAPI operation spec. If no matching media types is found or the type is
187+
not supported yet, an `UnsupportedMediaTypeError` (http statusCode 415) will be
188+
reported.
188189

189190
Please note that `urlencoded` media type does not support data typing. For
190191
example, `key=3` is parsed as `{key: '3'}`. The raw result is then coerced by
@@ -238,17 +239,25 @@ binding the value to `RestBindings.REQUEST_BODY_PARSER_OPTIONS`
238239
('rest.requestBodyParserOptions'). For example,
239240

240241
```ts
241-
server
242-
.bind(RestBindings.REQUEST_BODY_PARSER_OPTIONS)
243-
.to({limit: 4 * 1024 * 1024}); // Set limit to 4MB
242+
server.bind(RestBindings.REQUEST_BODY_PARSER_OPTIONS).to({
243+
limit: '4MB',
244+
});
244245
```
245246

246-
The list of options can be found in the [body](https://github.com/Raynos/body)
247-
module.
247+
The options can be media type specific, for example:
248248

249-
By default, the `limit` is `1024 * 1024` (1MB). Any request with a body length
250-
exceeding the limit will be rejected with http status code 413 (request entity
251-
too large).
249+
```ts
250+
server.bind(RestBindings.REQUEST_BODY_PARSER_OPTIONS).to({
251+
json: {limit: '4MB'},
252+
text: {limit: '1MB'},
253+
});
254+
```
255+
256+
The list of options can be found in the
257+
[body-parser](https://github.com/expressjs/body-parser/#options) module.
258+
259+
By default, the `limit` is `1MB`. Any request with a body length exceeding the
260+
limit will be rejected with http status code 413 (request entity too large).
252261

253262
A few tips worth mentioning:
254263

@@ -260,6 +269,86 @@ A few tips worth mentioning:
260269
[`api()`](Decorators.md#api-decorator), this requires you to provide a
261270
completed request body specification.
262271

272+
#### Extend Request Body Parsing
273+
274+
See [Extending request body parsing](./Extending-request-body-parsing.md) for
275+
more details.
276+
277+
#### Specify Custom Parser by Controller Methods
278+
279+
In some cases, a controller method wants to handle request body parsing by
280+
itself, such as, to accept `multipart/form-data` for file uploads or stream-line
281+
a large json document. To bypass body parsing, the `'x-parser'` extension can be
282+
set to `'stream'` for a media type of the request body content. For example,
283+
284+
```ts
285+
class FileUploadController {
286+
async upload(
287+
@requestBody({
288+
description: 'multipart/form-data value.',
289+
required: true,
290+
content: {
291+
'multipart/form-data': {
292+
// Skip body parsing
293+
'x-parser': 'stream',
294+
schema: {type: 'object'},
295+
},
296+
},
297+
})
298+
request: Request,
299+
@inject(RestBindings.Http.RESPONSE) response: Response,
300+
): Promise<object> {
301+
const storage = multer.memoryStorage();
302+
const upload = multer({storage});
303+
return new Promise<object>((resolve, reject) => {
304+
upload.any()(request, response, err => {
305+
if (err) reject(err);
306+
else {
307+
resolve({
308+
files: request.files,
309+
// tslint:disable-next-line:no-any
310+
fields: (request as any).fields,
311+
});
312+
}
313+
});
314+
});
315+
}
316+
}
317+
```
318+
319+
The `x-parser` value can be one of the following:
320+
321+
1. Name of the parser, such as `json`, `raw`, or `stream`
322+
323+
- `stream`: keeps the http request body as a stream without parsing
324+
- `raw`: parses the http request body as a `Buffer`
325+
326+
```ts
327+
{
328+
'x-parser': 'stream'
329+
}
330+
```
331+
332+
2. A body parser class
333+
334+
```ts
335+
{
336+
'x-parser': JsonBodyParser
337+
}
338+
```
339+
340+
3. A body parser function, for example:
341+
342+
```ts
343+
function parseJson(request: Request): Promise<RequestBody> {
344+
return new JsonBodyParser().parse(request);
345+
}
346+
347+
{
348+
'x-parser': parseJson
349+
}
350+
```
351+
263352
#### Localizing Errors
264353

265354
A body data may break multiple validation rules, like missing required fields,

docs/site/sidebars/lb4_sidebar.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,10 @@ children:
307307
url: Creating-servers.html
308308
output: 'web, pdf'
309309

310+
- title: 'Extending Request Body Parsing'
311+
url: Extending-request-body-parsing.html
312+
output: 'web, pdf'
313+
310314
- title: 'Testing your extension'
311315
url: Testing-your-extension.html
312316
output: 'web, pdf'

packages/context/src/binding.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,21 @@
33
// This file is licensed under the MIT License.
44
// License text available at https://opensource.org/licenses/MIT
55

6+
import * as debugModule from 'debug';
7+
import {BindingAddress, BindingKey} from './binding-key';
68
import {Context} from './context';
7-
import {BindingKey} from './binding-key';
9+
import {Provider} from './provider';
810
import {ResolutionSession} from './resolution-session';
911
import {instantiateClass} from './resolver';
1012
import {
13+
BoundValue,
1114
Constructor,
1215
isPromiseLike,
13-
BoundValue,
14-
ValueOrPromise,
1516
MapObject,
1617
transformValueOrPromise,
18+
ValueOrPromise,
1719
} from './value-promise';
18-
import {Provider} from './provider';
1920

20-
import * as debugModule from 'debug';
2121
const debug = debugModule('loopback:context:binding');
2222

2323
/**
@@ -443,7 +443,7 @@ export class Binding<T = BoundValue> {
443443
* easy to read.
444444
* @param key Binding key
445445
*/
446-
static bind(key: string): Binding {
447-
return new Binding(key);
446+
static bind<T = unknown>(key: BindingAddress<T>): Binding {
447+
return new Binding(key.toString());
448448
}
449449
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This file is used for file-upload acceptance test.

packages/rest/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@
5858
"@types/debug": "0.0.30",
5959
"@types/js-yaml": "^3.11.1",
6060
"@types/lodash": "^4.14.106",
61+
"@types/multer": "^1.3.7",
6162
"@types/node": "^10.11.2",
6263
"@types/qs": "^6.5.1",
63-
"@types/serve-static": "1.13.2",
64-
"@types/type-is": "^1.6.2"
64+
"multer": "^1.4.1"
6565
},
6666
"files": [
6767
"README.md",

0 commit comments

Comments
 (0)