-
Notifications
You must be signed in to change notification settings - Fork 36
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
Add custom response renderer options. #67
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 |
---|---|---|
|
@@ -84,12 +84,16 @@ The content type of the response depends on the request's `Accepts` header. | |
| safeFields | [String] | `[]` | Specifies property names on errors that are allowed to be passed through in 4xx and 5xx responses. See [Safe error fields](#safe-error-fields) below. | | ||
| defaultType | String | `"json"` | Specify the default response content type to use when the client does not provide any Accepts header. | ||
| negotiateContentType | Boolean | true | Negotiate the response content type via Accepts request header. When disabled, strong-error-handler will always use the default content type when producing responses. Disabling content type negotiation is useful if you want to see JSON-formatted error responses in browsers, because browsers usually prefer HTML and XML over other content types. | ||
| htmlRenderer | String | 'send-html' | Operation function to render HTML with signature `fn(res, data)`. Defaults to `./send-html` (`lib/send-html.js`). | | ||
| jsonRenderer | String | 'send-json' | Operation function to render JSON with signature `fn(res, data)`. Defaults to `./send-json` (`lib/send-json.js`). | | ||
| textRenderer | String | 'send-text' | Operation function to render text with signature `fn(res, data)`. Defaults to `./send-text` (`lib/send-text.js`). | | ||
| xmlRenderer | String | 'send-xml' | Operation function to render XML with signature `fn(res, data)`. Defaults to `./send-xml` (`lib/send-xml.js`). | | ||
|
||
### Customizing log format | ||
|
||
**Express** | ||
**Express** | ||
|
||
To use a different log format, add your own custom error-handling middleware then disable `errorHandler.log`. | ||
To use a different log format, add your own custom error-handling middleware then disable `errorHandler.log`. | ||
For example, in an Express application: | ||
|
||
```js | ||
|
@@ -157,6 +161,34 @@ Using the above configuration, an error containing an `errorCode` property will | |
} | ||
``` | ||
|
||
### Custom Renderer Functions | ||
|
||
You can switch out the default renderers for HTML, JSON, text and XML responses with custom renderers: | ||
|
||
``` | ||
{ | ||
// ... | ||
"final:after": { | ||
"strong-error-handler": { | ||
"params": { | ||
"htmlRenderer": "$!./lib/send-html" | ||
} | ||
} | ||
} | ||
``` | ||
|
||
The `$!` characters indicate that the path is relative to the location of `middleware.json`. | ||
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. Please add a note that |
||
|
||
*./lib/send-html.js:* | ||
|
||
``` | ||
module.exports = function(res, data) { | ||
res.send("Hello."); | ||
} | ||
``` | ||
|
||
Using the above configuration, a request with `Content-type: text/html` that is handled by strong-error-handler will return a `Content-type: text/html` and a response body of `Hello.`. | ||
|
||
## Migration from old LoopBack error handler | ||
|
||
NOTE: This is only required for applications scaffolded with old versions of the `slc loopback` tool. | ||
|
@@ -203,7 +235,7 @@ To migrate a LoopBack 2.x application to use `strong-error-handler`: | |
} | ||
</pre> | ||
|
||
For more information, see | ||
For more information, see | ||
[Migrating apps to LoopBack 3.0](http://loopback.io/doc/en/lb3/Migrating-to-3.0.html#update-use-of-rest-error-handler). | ||
|
||
## Example | ||
|
@@ -221,17 +253,17 @@ The same error generated when `debug: true` : | |
{ statusCode: 500, | ||
name: 'Error', | ||
message: 'a test error message', | ||
stack: 'Error: a test error message | ||
at Context.<anonymous> (User/strong-error-handler/test/handler.test.js:220:21) | ||
at callFnAsync (User/strong-error-handler/node_modules/mocha/lib/runnable.js:349:8) | ||
at Test.Runnable.run (User/strong-error-handler/node_modules/mocha/lib/runnable.js:301:7) | ||
at Runner.runTest (User/strong-error-handler/node_modules/mocha/lib/runner.js:422:10) | ||
at User/strong-error-handler/node_modules/mocha/lib/runner.js:528:12 | ||
at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:342:14) | ||
at User/strong-error-handler/node_modules/mocha/lib/runner.js:352:7 | ||
at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:284:14) | ||
at Immediate._onImmediate (User/strong-error-handler/node_modules/mocha/lib/runner.js:320:5) | ||
at tryOnImmediate (timers.js:543:15) | ||
stack: 'Error: a test error message | ||
at Context.<anonymous> (User/strong-error-handler/test/handler.test.js:220:21) | ||
at callFnAsync (User/strong-error-handler/node_modules/mocha/lib/runnable.js:349:8) | ||
at Test.Runnable.run (User/strong-error-handler/node_modules/mocha/lib/runnable.js:301:7) | ||
at Runner.runTest (User/strong-error-handler/node_modules/mocha/lib/runner.js:422:10) | ||
at User/strong-error-handler/node_modules/mocha/lib/runner.js:528:12 | ||
at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:342:14) | ||
at User/strong-error-handler/node_modules/mocha/lib/runner.js:352:7 | ||
at next (User/strong-error-handler/node_modules/mocha/lib/runner.js:284:14) | ||
at Immediate._onImmediate (User/strong-error-handler/node_modules/mocha/lib/runner.js:320:5) | ||
at tryOnImmediate (timers.js:543:15) | ||
at processImmediate [as _immediateCallback] (timers.js:523:5)' }} | ||
``` | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,11 +6,13 @@ | |
'use strict'; | ||
var accepts = require('accepts'); | ||
var debug = require('debug')('strong-error-handler:http-response'); | ||
var sendJson = require('./send-json'); | ||
var sendHtml = require('./send-html'); | ||
var sendXml = require('./send-xml'); | ||
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. We prefer to keep all 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. Your suggestion would make me happier than this current method. |
||
var util = require('util'); | ||
|
||
var sendHtml, | ||
sendJson, | ||
sendText, | ||
sendXml; | ||
|
||
module.exports = negotiateContentProducer; | ||
|
||
/** | ||
|
@@ -26,6 +28,7 @@ function negotiateContentProducer(req, logWarning, options) { | |
var SUPPORTED_TYPES = [ | ||
'application/json', 'json', | ||
'text/html', 'html', | ||
'text/plain', 'text', | ||
'text/xml', 'xml', | ||
]; | ||
|
||
|
@@ -43,6 +46,12 @@ function negotiateContentProducer(req, logWarning, options) { | |
} | ||
} | ||
|
||
// resolve renderer functions | ||
sendHtml = require(options.htmlRenderer || './send-html'); | ||
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. Overriding I am proposing a different approach here:
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. Very good. I'll combine this with the note above about top-level requires and figure it out. |
||
sendJson = require(options.jsonRenderer || './send-json'); | ||
sendText = require(options.textRenderer || './send-text'); | ||
sendXml = require(options.xmlRenderer || './send-xml'); | ||
|
||
// decide to use resolvedType or defaultType | ||
// Please note that accepts assumes the order of content-type is provided | ||
// in the priority returned | ||
|
@@ -96,6 +105,9 @@ function resolveOperation(contentType) { | |
case 'text/html': | ||
case 'html': | ||
return sendHtml; | ||
case 'text/plain': | ||
case 'text': | ||
return sendText; | ||
case 'text/xml': | ||
case 'xml': | ||
return sendXml; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright IBM Corp. 2016. All Rights Reserved. | ||
// Node module: strong-error-handler | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
'use strict'; | ||
|
||
var format = require('util').format; | ||
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. 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. Happy to do so. I think this is the case in quite a few places. I only saw 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. Yeah, we are conservative when it comes to code style changes and haven't upgrade the existing codebase for our new ES6 conventions. I know it can be confusing for new contributors. |
||
|
||
module.exports = function sendText(res, data) { | ||
var content = format('%s: %s [%s]', data.name, data.message, | ||
data.statusCode); | ||
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. This is a good start 👍 I am concerned that this format is missing important additional information, e.g. Please check how the built-in html renderer is iterating over error properties and implement a similar algorithm here. 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. 👍 |
||
res.setHeader('Content-Type', 'text/plain; charset=utf-8'); | ||
res.end(content, 'utf-8'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
// Copyright IBM Corp. 2016. All Rights Reserved. | ||
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. The copyright should say 2018 in all newly added files.
Please use file names that are different from files containing the built-in renderers, for example:
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. 👍 |
||
// Node module: strong-error-handler | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
'use strict'; | ||
|
||
module.exports = function sendHtml(res, data) { | ||
var content = '<html><body>' + data.message + '</body></html>'; | ||
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. Use 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. 👍 |
||
res.setHeader('Content-Type', 'text/html; charset=utf-8'); | ||
res.end(content, 'utf-8'); | ||
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. Interesting. You are proposing that custom renderer functions are not only responsible for rendering the error data object into a representation to be sent back to the client, but you are also asking the rendered to actually send the HTTP response. I think that's adding unnecessary complexity to custom renderer functions and also allow our users to shoot their own feet by setting wrong content-type (e.g. setting content-type to I am proposing to simplify the design of custom renderers, so that these functions only render the response body (return back the string to be sent). I understand there may be situations where the user wants to configure the HTTP response headers too, but I think we should find a different solution for that, and only after there is user demand for this feature. Thoughts? 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. For what it's worth, I was trying to copy the current implementation of sending the response from https://github.com/strongloop/strong-error-handler/blob/master/lib/send-html.js. I think I would prefer to allow users to have the full But, thinking through it as you've suggested, it does seem like bad design that a consumer would have to know to 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.
That was a sensible thing to do! It's just that the existing design was not meant for custom user-supplied renderers.
Here is what I am thinking:
|
||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright IBM Corp. 2016. All Rights Reserved. | ||
// Node module: strong-error-handler | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
'use strict'; | ||
|
||
module.exports = function sendJson(res, data) { | ||
var content = JSON.stringify({ | ||
error: data, | ||
custom: 'custom data', | ||
}); | ||
res.setHeader('Content-Type', 'application/json; charset=utf-8'); | ||
res.end(content, 'utf-8'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright IBM Corp. 2016. All Rights Reserved. | ||
// Node module: strong-error-handler | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
'use strict'; | ||
|
||
var format = require('util').format; | ||
|
||
module.exports = function sendText(res, data) { | ||
var content = format('%s: %s {%s}', data.name, data.message, | ||
data.statusCode); | ||
res.setHeader('Content-Type', 'text/plain; charset=utf-8'); | ||
res.end(content, 'utf-8'); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
// Copyright IBM Corp. 2016. All Rights Reserved. | ||
// Node module: strong-error-handler | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
'use strict'; | ||
|
||
var js2xmlparser = require('js2xmlparser'); | ||
|
||
module.exports = function sendXml(res, data) { | ||
data.custom = 'custom data'; | ||
var content = js2xmlparser.parse('error', data); | ||
res.setHeader('Content-Type', 'text/xml; charset=utf-8'); | ||
res.end(content, 'utf-8'); | ||
}; |
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.
I like the idea of customizable renderer functions 👍
I am concerned about the design you are proposing though. Passing in a string path may seem like a convenient solution in the context of LoopBack's
middleware.json
, but there are drawbacks too:When using strong-error-handler in vanilla Express applications, configuring it directly from JavaScript code, the usual convention is to pass functions, not paths to source code files.
This can be addressed by modifying the implementation to recognize both String and Function values.
Using a path that's directly passed to
require
can lead to situation difficult to debug. Consider what happens when you forget to include$!
prefix in yourmiddleware.json
entry: the path will be resolved relatively to strong-error-handler. Either a wrong file will be loaded, or what's more likely, "module not found" error will be printed.I am proposing to address this usability issue by checking whether the user-supplied path is absolute, and throwing an error with a helpful error message otherwise.
Thoughts?
Also please change the description of the default value. Consumers of this module should not need to understand our internal implementation. IMO, it's better to tell them that by default the built-in renderer is used. In fact, I think the rendered options should not have any default value per se, instead we should interpret a missing value as "use the built-in renderer".
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.
Absolutely agree, would prefer it be definable as a function as well.
If a path is supplied, are you suggesting it would be required to start with
$!
or else we'd throw a helpful error? I was thinking it would behave however core LoopBack behaves in this regard, where I thought you could do it with or without the$!
.Good point.
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.
The prefix
$!
is something that loopback-boot recognizes and expands into an absolute path. LoopBack runtime itself (loopback, strong-remoting, express) never see that value!$!
in the examples, that's how this new feature should be used in LoopBack appsThoughts?