-
Notifications
You must be signed in to change notification settings - Fork 387
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Rework current-context docs #227
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,156 +10,147 @@ permalink: /doc/en/lb3/Using-current-context.html | |
summary: | ||
--- | ||
|
||
{% include warning.html content="Using the current context feature is not recommended! | ||
|
||
The current implementation of loopback-context is based on the module [continuation-local-storage](https://www.npmjs.com/package/continuation-local-storage) | ||
which is known to have many problems, (for example, see [issue #59](https://github.com/othiym23/node-continuation-local-storage/issues/59)). | ||
As a result, loopback-context does not work in many situations, as can be seen from issues reported in LoopBack's | ||
[issue tracker](https://github.com/strongloop/loopback/issues?utf8=%E2%9C%93&q=is%3Aissue%20getCurrentContext). | ||
|
||
See [loopback issue #1495](https://github.com/strongloop/loopback/issues/1495) updates and an alternative solution. | ||
" %} | ||
|
||
LoopBack applications sometimes need to access context information to implement the business logic, for example to: | ||
|
||
* Access the currently logged-in user. | ||
* Access the HTTP request (such as URL and headers). | ||
|
||
A typical request to invoke a LoopBack model method travels through multiple layers with chains of asynchronous callbacks. It's not always possible to pass all the information through method parameters. | ||
|
||
**See also**: [Example in LoopBack repository](https://github.com/strongloop/loopback/blob/master/example/context/app.js). | ||
|
||
## Error messages | ||
|
||
LoopBack 3.0 [removed current-context APIs](3.0-Release-Notes.html#current-context-api-and-middleware-removed). | ||
An application that uses current-context will print the following error message | ||
when it receives its first HTTP request: | ||
|
||
``` | ||
Unhandled error for request GET /api/Users: | ||
Error: remoting.context option was removed in version 3.0. | ||
For more information, see https://loopback.io/doc/en/lb3/Using-current-context.html | ||
for more details. | ||
at restApiHandler (.../node_modules/loopback/server/middleware/rest.js:44:15) | ||
at Layer.handle [as handle_request] (.../node_modules/express/lib/router/layer.js:95:5) | ||
... | ||
``` | ||
|
||
To remove this warning, disable the context middleware added by the built-in REST handler. Set the `remoting.context` property in `server/config.json` to **false**; for example: | ||
|
||
{% include code-caption.html content="server/config.json" %} | ||
```javascript | ||
LoopBack 2.x introduced current-context APIs using the module | ||
[continuation-local-storage](https://www.npmjs.com/package/continuation-local-storage) | ||
to provide a context object preserved across asynchronous operations. | ||
Unfortunately, this module is not reliable and has many known problems (for | ||
example, see [issue #59](https://github.com/othiym23/node-continuation-local-storage/issues/59)). | ||
As a result, the current-context feature does not work in many situations, | ||
see [loopback-context issues](https://github.com/strongloop/loopback-context/issues) | ||
and [related issues in loopback](https://github.com/strongloop/loopback/issues?utf8=%E2%9C%93&q=is%3Aissue%20getCurrentContext). | ||
|
||
To address this problem, LoopBack 3.0 moves all current-context-related code to | ||
[loopback-context](https://github.com/strongloop/loopback-context) module | ||
and removed all current-context APIs (see | ||
[Release Notes](3.0-Release-Notes.html#current-context-api-and-middleware-removed)). | ||
|
||
However, applications clearly need to acces information like the currently | ||
logged-in user in application logic, for example in | ||
[Operation hooks](Operation-hooks.html). Until there is a reliable | ||
implementation of continuation-local-storage available for Node.js, | ||
explicitly pass any additional context via `options` parameter | ||
of (remote) methods. | ||
|
||
Built-in methods such as | ||
[PersistedModel.find](http://apidocs.strongloop.com/loopback/#persistedmodel-find) | ||
or | ||
[PersistedModel.create](http://apidocs.strongloop.com/loopback/#persistedmodel-create) | ||
accept an `options` argument. | ||
|
||
[Operation hooks](Operation-hooks.html) expose the `options` argument | ||
as `context.options`. | ||
|
||
You must safely initialize the `options` parameter when a method is invoked | ||
via REST API, ensuring that clients cannot override sensitive information like | ||
the currently logged-in user. Doing so requires two steps: | ||
- Annotate "options" parameter in remoting metadata | ||
- Customize the value provided to "options" | ||
|
||
## Annotate "options" parameter in remoting metadata | ||
|
||
Methods accepting an `options` argument must declare this argument in their | ||
remoting metadata and set the `http` property to the special string value | ||
`"optionsFromRequest"`. | ||
|
||
```json | ||
{ | ||
"remoting": { | ||
"context": false, | ||
... | ||
}, | ||
... | ||
"arg": "options", | ||
"type": "object", | ||
"http": "optionsFromRequest" | ||
} | ||
``` | ||
|
||
If your application relies on `loopback.getCurrentContext`, rework your code to use `loopback-context` directly, per the following instructions. | ||
|
||
## Install loopback-context | ||
Under the hood, `Model.remoteMethod` converts this special string value | ||
to a function that will be called by strong-remoting for each incoming request | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Under the hood, |
||
to build the value for this parameter. | ||
|
||
Add `loopback-context` to your project dependencies | ||
{% include tip.html content=' | ||
Computed "accepts" parameters have been around for a while and they are well supported by LoopBack tooling. For example, the [Swagger generator](Swagger-generator.htm) excludes computed properties from the API endpoint description. As a result, the "options" parameter will not be described in the Swagger documentation. | ||
' %} | ||
|
||
``` | ||
$ npm install --save loopback-context | ||
``` | ||
All built-in method have been already modified to include this new "options" | ||
parameter. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Methods of built-in model accept this new "options" parameter. |
||
|
||
## Configure context propagation | ||
{% include note.html content=' | ||
In LoopBack 2.x, this feature is disabled by default for compatibility reasons. To enable, add `"injectOptionsFromRemoteContext": true` to your model JSON file. | ||
' %} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To enable, add |
||
|
||
To setup your LoopBack application to create a new context for each incoming HTTP request, configure `per-context` middleware in your `server/middleware.json` as follows: | ||
## Customize the value provided to "options" | ||
|
||
```javascript | ||
{ | ||
"initial": { | ||
"loopback-context#per-request": {}, | ||
} | ||
... | ||
} | ||
``` | ||
When strong-remoting resolves the "options" argument, it calls model's | ||
`createOptionsFromRemotingContext` method. The default implementation of this | ||
method returns an object with a single property `accessToken` containing | ||
the `AccessToken` instance used to authenticate the request. | ||
|
||
{% include important.html content="By default, the HTTP req/res objects are not set onto the current context. You need to set `enableHttpContext` to true to enable automatic population of req/res objects. | ||
" %} | ||
There are several ways to customize this value: | ||
- Override `createOptionsFromRemotingContext` in your model. | ||
- Use a "beforeRemote" hook. | ||
- Use a custom strong-remoting phase. | ||
|
||
## Use the current context | ||
### Override `createOptionsFromRemotingContext` in your model | ||
|
||
Once you've enabled context propagation, you can access the current context object using `LoopBackContext.getCurrentContext()`. | ||
The context will be available in middleware (if it is loaded after the context middleware), remoting hooks, model hooks, and custom methods. | ||
```js | ||
MyModel.createOptionsFromRemotingContext = function(ctx) { | ||
var base = this.base.createOptionsFromRemotingContext(ctx); | ||
return extend(base, { | ||
currentUserId: base.accessToken && base.accessToken.userId, | ||
}); | ||
}; | ||
``` | ||
|
||
```javascript | ||
var LoopBackContext = require('loopback-context'); | ||
A better approach is to write a mixin that overrides this method and that can | ||
be shared between multiple models. | ||
|
||
MyModel.myMethod = function(cb) { | ||
var ctx = LoopBackContext.getCurrentContext(); | ||
// Get the current access token | ||
var accessToken = ctx && ctx.get('accessToken'); | ||
... | ||
// Set more information on current context | ||
ctx.set('foo', { bar: 'val' } ); | ||
### Use a "beforeRemote" hook | ||
|
||
... | ||
} | ||
``` | ||
Because the "options" parameter is a regular method parameter, you can access | ||
it from remote hooks via `ctx.args.options`. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the "options" parameter is a regular method parameter, you can access it rrom remote hooks via |
||
## Use current authenticated user in remote methods | ||
|
||
In advanced use cases, for example when you want to add custom middleware, you have to add the context middleware | ||
at the right position in the middleware chain (before the middleware that depends on `LoopBackContext.getCurrentContext`). | ||
|
||
{% include important.html content=" | ||
|
||
`LoopBackContext.perRequest()` detects the situation when it is invoked multiple times on the same request and returns immediately in subsequent runs. | ||
|
||
" %} | ||
|
||
Here's sample code which uses a middleware function to place the currently authenticated user into the context so that remote methods may use it: | ||
|
||
{% include code-caption.html content="/server/server.js" %} | ||
```javascript | ||
... | ||
// -- Add your pre-processing middleware here -- | ||
app.use(LoopBackContext.perRequest()); | ||
app.use(loopback.token()); | ||
app.use(function setCurrentUser(req, res, next) { | ||
if (!req.accessToken) { | ||
return next(); | ||
} | ||
app.models.UserModel.findById(req.accessToken.userId, function(err, user) { | ||
if (err) { | ||
return next(err); | ||
} | ||
if (!user) { | ||
return next(new Error('No user with this access token was found.')); | ||
} | ||
var loopbackContext = LoopBackContext.getCurrentContext(); | ||
if (loopbackContext) { | ||
loopbackContext.set('currentUser', user); | ||
} | ||
```js | ||
MyModel.beforeRemote('saveOptions', function(ctx, unused, next) { | ||
if (!ctx.args.options.accessToken) return next(); | ||
User.findById(ctx.args.options.accessToken.userId, function(err, user) { | ||
if (err) return next(err); | ||
ctx.args.options.currentUser = user; | ||
next(); | ||
}); | ||
}); | ||
|
||
// boot scripts mount components like REST API | ||
... | ||
}) | ||
``` | ||
|
||
{% include code-caption.html content="/common/models/YourModel.js" %} | ||
```javascript | ||
var loopback = require('loopback'); | ||
var LoopBackContext = require('loopback-context'); | ||
module.exports = function(YourModel) { | ||
... | ||
//remote method | ||
YourModel.someRemoteMethod = function(arg1, arg2, cb) { | ||
var ctx = LoopBackContext.getCurrentContext(); | ||
var currentUser = ctx && ctx.get('currentUser'); | ||
console.log('currentUser.username: ', currentUser.username); // voila! | ||
... | ||
cb(null); | ||
}; | ||
... | ||
Again, a hook like this can be reused by placing the code in a mixin. | ||
|
||
It may not always be possible to control the order in which remote hooks are | ||
executed. If you need to control the order, then use a custom | ||
strong-remoting phase as described in the following section. | ||
|
||
### Use a custom strong-remoting phase | ||
|
||
Internally, strong-remoting uses phases similar to [middleware | ||
phases](https://loopback.io/doc/en/lb3/Defining-middleware.html). The framework | ||
defines two built-in phases: `auth` and `invoke`. All remote hooks are run in | ||
the second phase `invoke`. | ||
|
||
Applications can define a custom phase to run code before any remote hooks are | ||
invoked, such code can be placed in a boot script for example. | ||
|
||
```js | ||
module.exports = function(app) { | ||
app.remotes().phases | ||
.addBefore('invoke', 'options-from-request') | ||
.use(function(ctx, next) { | ||
if (!ctx.args.options.accessToken) return next(); | ||
User.findById(ctx.args.options.accessToken.userId, function(err, user) { | ||
if (err) return next(err); | ||
ctx.args.options.currentUser = user; | ||
next(); | ||
}); | ||
}); | ||
}; | ||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
However, applications clearly need to acces information like the currently logged-in user in application logic, for example in operation hooks. Until there is a reliable implementation of continuation-local-storage available for Node.js, explicitly pass any additional context in the
options
parameter of (remote) methods.