Skip to content

Commit 43a01a6

Browse files
committed
docs: simplify middleware-based sequence
Signed-off-by: mrmodise <[email protected]>
1 parent f6436cb commit 43a01a6

5 files changed

+607
-531
lines changed
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
---
2+
lang: en
3+
title: 'Advanced Middleware-based Sequence Topics'
4+
keywords: LoopBack 4.0, LoopBack 4, Node.js, TypeScript, OpenAPI, Middleware
5+
sidebar: lb4_sidebar
6+
permalink: /doc/en/lb4/Advanced-middleware-based-sequence-topics.html
7+
---
8+
9+
## Advanced topics
10+
11+
### Customizing Sequence Actions
12+
13+
There might be scenarios where the default sequence _ordering_ is not something
14+
you want to change, but rather the individual actions that the sequence will
15+
execute.
16+
17+
To do this, you'll need to override one or more of the sequence action bindings
18+
used by the `RestServer`, under the `RestBindings.SequenceActions` constants.
19+
20+
As an example, we'll implement a custom sequence action to replace the default
21+
"send" action. This action is responsible for returning the response from a
22+
controller to the client making the request.
23+
24+
To do this, we'll register a custom send action by binding a
25+
[Provider](https://loopback.io/doc/en/lb4/apidocs.context.provider.html) to the
26+
`RestBindings.SequenceActions.SEND` key.
27+
28+
First, let's create our `CustomSendProvider` class, which will provide the send
29+
function upon injection.
30+
31+
{% include code-caption.html content="/src/providers/custom-send.provider.ts" %}
32+
**custom-send.provider.ts**
33+
34+
```ts
35+
import {Send, Response} from '@loopback/rest';
36+
import {Provider, BoundValue, inject} from '@loopback/core';
37+
import {writeResultToResponse, RestBindings, Request} from '@loopback/rest';
38+
39+
// Note: This is an example class; we do not provide this for you.
40+
import {Formatter} from '../utils';
41+
42+
export class CustomSendProvider implements Provider<Send> {
43+
// In this example, the injection key for formatter is simple
44+
constructor(
45+
@inject('utils.formatter') public formatter: Formatter,
46+
@inject(RestBindings.Http.REQUEST) public request: Request,
47+
) {}
48+
49+
value() {
50+
// Use the lambda syntax to preserve the "this" scope for future calls!
51+
return (response: Response, result: OperationRetval) => {
52+
this.action(response, result);
53+
};
54+
}
55+
/**
56+
* Use the mimeType given in the request's Accept header to convert
57+
* the response object!
58+
* @param response - The response object used to reply to the client.
59+
* @param result - The result of the operation carried out by the controller's
60+
* handling function.
61+
*/
62+
action(response: Response, result: OperationRetval) {
63+
if (result) {
64+
// Currently, the headers interface doesn't allow arbitrary string keys!
65+
const headers = (this.request.headers as any) || {};
66+
const header = headers.accept || 'application/json';
67+
const formattedResult = this.formatter.convertToMimeType(result, header);
68+
response.setHeader('Content-Type', header);
69+
response.end(formattedResult);
70+
} else {
71+
response.end();
72+
}
73+
}
74+
}
75+
```
76+
77+
Our custom provider will automatically read the `Accept` header from the request
78+
context, and then transform the result object so that it matches the specified
79+
MIME type.
80+
81+
Next, in our application class, we'll inject this provider on the
82+
`RestBindings.SequenceActions.SEND` key.
83+
84+
{% include code-caption.html content="/src/application.ts" %}
85+
86+
```ts
87+
import {RestApplication, RestBindings} from '@loopback/rest';
88+
import {
89+
RepositoryMixin,
90+
Class,
91+
Repository,
92+
juggler,
93+
} from '@loopback/repository';
94+
import {CustomSendProvider} from './providers/custom-send.provider';
95+
import {Formatter} from './utils';
96+
import {BindingScope} from '@loopback/core';
97+
98+
export class YourApp extends RepositoryMixin(RestApplication) {
99+
constructor() {
100+
super();
101+
// Assume your controller setup and other items are in here as well.
102+
this.bind('utils.formatter')
103+
.toClass(Formatter)
104+
.inScope(BindingScope.SINGLETON);
105+
this.bind(RestBindings.SequenceActions.SEND).toProvider(CustomSendProvider);
106+
}
107+
}
108+
```
109+
110+
As a result, whenever the send action of the
111+
[`DefaultSequence`](https://loopback.io/doc/en/lb4/apidocs.rest.defaultsequence.html)
112+
is called, it will make use of your function instead! You can use this approach
113+
to override any of the actions listed under the `RestBindings.SequenceActions`
114+
namespace.
115+
116+
### Query string parameters and path parameters
117+
118+
OAI 3.0.x describes the data from a request’s header, query and path in an
119+
operation specification’s parameters property. In a Controller method, such an
120+
argument is typically decorated by @param(). We've made multiple shortcuts
121+
available to the `@param()` decorator in the form of
122+
`@param.<http_source>.<OAI_primitive_type>`. Using this notation, path
123+
parameters can be described as `@param.path.string`. Here is an example of a
124+
controller method which retrieves a Note model instance by obtaining the `id`
125+
from the path object.
126+
127+
```ts
128+
@get('/notes/{id}', {
129+
responses: {
130+
'200': {
131+
description: 'Note model instance',
132+
content: {
133+
'application/json': {
134+
schema: getModelSchemaRef(Note, {includeRelations: true}),
135+
},
136+
},
137+
},
138+
},
139+
})
140+
async findById(
141+
@param.path.string('id') id: string,
142+
@param.filter(Note, {exclude: 'where'}) filter?: FilterExcludingWhere<Note>
143+
): Promise<Note> {
144+
return this.noteRepository.findById(id, filter);
145+
}
146+
```
147+
148+
(Notice: the filter for `findById()` method only supports the `include` clause
149+
for now.)
150+
151+
You can also specify a parameter which is an object value encoded as a JSON
152+
string or in multiple nested keys. For a JSON string, a sample value would be
153+
`location={"lang": 23.414, "lat": -98.1515}`. For the same `location` object, it
154+
can also be represented as `location[lang]=23.414&location[lat]=-98.1515`. Here
155+
is the equivalent usage for `@param.query.object()` decorator. It takes in the
156+
name of the parameter and an optional schema or reference object for it.
157+
158+
```ts
159+
@param.query.object('location', {
160+
type: 'object',
161+
properties: {lat: {type: 'number', format: 'float'}, long: {type: 'number', format: 'float'}},
162+
})
163+
```
164+
165+
The parameters are retrieved as the result of `parseParams` Sequence action.
166+
Please note that deeply nested properties are not officially supported by OAS
167+
yet and is tracked by
168+
[OAI/OpenAPI-Specification#1706](https://github.com/OAI/OpenAPI-Specification/issues/1706).
169+
Therefore, our REST API Explorer does not allow users to provide values for such
170+
parameters and unfortunately has no visible indication of that. This problem is
171+
tracked and discussed in
172+
[swagger-api/swagger-js#1385](https://github.com/swagger-api/swagger-js/issues/1385).
173+
174+
### Parsing Requests
175+
176+
Parsing and validating arguments from the request url, headers, and body. See
177+
page [Parsing requests](Parsing-requests.md).
178+
179+
### Invoking controller methods
180+
181+
The `invoke` sequence action simply takes the parsed request parameters from the
182+
`parseParams` action along with non-decorated arguments, calls the corresponding
183+
controller method or route handler method, and returns the value from it. The
184+
default implementation of
185+
[invoke](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/invoke-method.provider.ts)
186+
action calls the handler function for the route with the request specific
187+
context and the arguments for the function. It is important to note that
188+
controller methods use `invokeMethod` from `@loopback/core` and can be used with
189+
global and custom interceptors. See
190+
[Interceptor docs](Interceptor.md#use-invokemethod-to-apply-interceptors) for
191+
more details. The request flow for two route flavours is explained below.
192+
193+
For controller methods:
194+
195+
- A controller instance is instantiated from the context. As part of the
196+
instantiation, constructor and property dependencies are injected. The
197+
appropriate controller method is invoked via the chain of interceptors.
198+
- Arguments decorated with `@param` are resolved using data parsed from the
199+
request. Arguments decorated with `@inject` are resolved from the context.
200+
Arguments with no decorators are set to undefined, which is replaced by the
201+
argument default value if it's provided.
202+
203+
For route handlers, the handler function is invoked via the chain of
204+
interceptors. The array of method arguments is constructed using OpenAPI spec
205+
provided at handler registration time (either via `.api()` for full schema or
206+
`.route()` for individual route registration).
207+
208+
### Writing the response
209+
210+
The
211+
[send](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/providers/send.provider.ts)
212+
sequence action is responsible for writing the result of the `invoke` action to
213+
the HTTP response object. The default sequence calls send with (transformed)
214+
data. Under the hood, send performs all steps required to send back the
215+
response, from content-negotiation to serialization of the response body. In
216+
Express, the handler is responsible for setting response status code, headers
217+
and writing the response body. In LoopBack, controller methods and route
218+
handlers return data describing the response and it's the responsibility of the
219+
Sequence to send that data back to the client. This design makes it easier to
220+
transform the response before it is sent.
221+
222+
LoopBack 4 does not yet provide first-class support for streaming responses, see
223+
[Issue#2230](https://github.com/strongloop/loopback-next/issues/2230). As a
224+
short-term workaround, controller methods are allowed to send the response
225+
directly, effectively bypassing send action. The default implementation of send
226+
is prepared to handle this case
227+
[here](https://github.com/strongloop/loopback-next/blob/bf07ff959a1f90577849b61221b292d3127696d6/packages/rest/src/writer.ts#L22-L26).
228+
229+
### Handling errors
230+
231+
There are many reasons why the application may not be able to handle an incoming
232+
request:
233+
234+
- The requested endpoint (method + URL path) was not found.
235+
- Parameters provided by the client were not valid.
236+
- A backend database or a service cannot be reached.
237+
- The response object cannot be converted to JSON because of cyclic
238+
dependencies.
239+
- A programmer made a mistake and a `TypeError` is thrown by the runtime.
240+
- And so on.
241+
242+
In the Sequence implementation described above, all errors are handled by a
243+
single `catch` block at the end of the sequence, using the Sequence Action
244+
called `reject`.
245+
246+
The default implementation of `reject` does the following steps:
247+
248+
- Call
249+
[strong-error-handler](https://github.com/strongloop/strong-error-handler) to
250+
send back an HTTP response describing the error.
251+
- Log the error to `stderr` if the status code was 5xx (an internal server
252+
error, not a bad request).
253+
254+
To prevent the application from leaking sensitive information like filesystem
255+
paths and server addresses, the error handler is configured to hide error
256+
details.
257+
258+
- For 5xx errors, the output contains only the status code and the status name
259+
from the HTTP specification. For example:
260+
261+
```json
262+
{
263+
"error": {
264+
"statusCode": 500,
265+
"message": "Internal Server Error"
266+
}
267+
}
268+
```
269+
270+
- For 4xx errors, the output contains the full error message (`error.message`)
271+
and the contents of the `details` property (`error.details`) that
272+
`ValidationError` typically uses to provide machine-readable details about
273+
validation problems. It also includes `error.code` to allow a machine-readable
274+
error code to be passed through which could be used, for example, for
275+
translation.
276+
277+
```json
278+
{
279+
"error": {
280+
"statusCode": 422,
281+
"name": "Unprocessable Entity",
282+
"message": "Missing required fields",
283+
"code": "MISSING_REQUIRED_FIELDS"
284+
}
285+
}
286+
```
287+
288+
During development and testing, it may be useful to see all error details in the
289+
HTTP response returned by the server. This behavior can be enabled by enabling
290+
the `debug` flag in error-handler configuration as shown in the code example
291+
below. See strong-error-handler
292+
[docs](https://github.com/strongloop/strong-error-handler#options) for a list of
293+
all available options.
294+
295+
```ts
296+
app.bind(RestBindings.ERROR_WRITER_OPTIONS).to({debug: true});
297+
```
298+
299+
An example error message when the debug mode is enabled:
300+
301+
```json
302+
{
303+
"error": {
304+
"statusCode": 500,
305+
"name": "Error",
306+
"message": "ENOENT: no such file or directory, open '/etc/passwords'",
307+
"errno": -2,
308+
"syscall": "open",
309+
"code": "ENOENT",
310+
"path": "/etc/passwords",
311+
"stack": "Error: a test error message\n at Object.openSync (fs.js:434:3)\n at Object.readFileSync (fs.js:339:35)"
312+
}
313+
}
314+
```
315+
316+
### Keeping your Sequences
317+
318+
For most use cases, the
319+
[default](https://github.com/strongloop/loopback-next/blob/6bafa0774662991199090219913c3dc77ad5b149/packages/rest/src/sequence.ts)
320+
sequence supplied with LoopBack 4 applications is good enough for
321+
request-response handling pipeline. Check out
322+
[Custom Sequences](https://loopback.io/doc/en/lb4/REST-middleware-sequence.html#custom-sequences)
323+
on how to extend it and implement custom actions.

0 commit comments

Comments
 (0)