Merge pull request #90 from strongloop/root-property
feat: Add options.rootProperty
This commit is contained in:
commit
3eeaaef75a
|
@ -1,6 +1,6 @@
|
||||||
sudo: false
|
sudo: false
|
||||||
language: node_js
|
language: node_js
|
||||||
node_js:
|
node_js:
|
||||||
- "8"
|
|
||||||
- "10"
|
- "10"
|
||||||
- "12"
|
- "12"
|
||||||
|
- "14"
|
||||||
|
|
51
README.md
51
README.md
|
@ -14,9 +14,9 @@ In debug mode, `strong-error-handler` returns full error stack traces and intern
|
||||||
|
|
||||||
## Supported versions
|
## Supported versions
|
||||||
|
|
||||||
Current|Long Term Support|Maintenance
|
| Current | Long Term Support | Maintenance |
|
||||||
:-:|:-:|:-:
|
| :-----: | :---------------: | :---------: |
|
||||||
3.x|2.x|1.x
|
| 4.x | 3.x | 2.x |
|
||||||
|
|
||||||
Learn more about our LTS plan in [docs](http://loopback.io/doc/en/contrib/Long-term-support.html).
|
Learn more about our LTS plan in [docs](http://loopback.io/doc/en/contrib/Long-term-support.html).
|
||||||
|
|
||||||
|
@ -108,24 +108,25 @@ The content type of the response depends on the request's `Accepts` header.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| ---- | ---- | ---- | ---- |
|
| -------------------- | ------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| debug | Boolean | `false` | If `true`, HTTP responses include all error properties, including sensitive data such as file paths, URLs and stack traces. See [Example output](#example) below. |
|
| debug | Boolean | `false` | If `true`, HTTP responses include all error properties, including sensitive data such as file paths, URLs and stack traces. See [Example output](#example) below. |
|
||||||
| log | Boolean | `true` | If `true`, all errors are printed via `console.error`, including an array of fields (custom error properties) that are safe to include in response messages (both 4xx and 5xx). <br/> If `false`, sends only the error back in the response. |
|
| log | Boolean | `true` | If `true`, all errors are printed via `console.error`, including an array of fields (custom error properties) that are safe to include in response messages (both 4xx and 5xx). <br/> If `false`, sends only the error back in the response. |
|
||||||
| 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. |
|
| 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.
|
| defaultType | String | `"json"` | Specifies 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.
|
| rootProperty | String or false | `"error"` | Specifies the root property name for json or xml. If the value is set to `false`, no wrapper will be added to the json object. The false value is ignored by XML as a root element is always required. |
|
||||||
|
| 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. |
|
||||||
|
|
||||||
### Customizing log format
|
### 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:
|
For example, in an Express application:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
app.use(myErrorLogger());
|
app.use(myErrorLogger());
|
||||||
app.use(errorHandler({ log: false }));
|
app.use(errorHandler({log: false}));
|
||||||
```
|
```
|
||||||
|
|
||||||
In general, add `strong-error-handler` as the last middleware function, just before calling `app.listen()`.
|
In general, add `strong-error-handler` as the last middleware function, just before calling `app.listen()`.
|
||||||
|
@ -234,7 +235,7 @@ To migrate a LoopBack 2.x application to use `strong-error-handler`:
|
||||||
}
|
}
|
||||||
</pre>
|
</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).
|
[Migrating apps to LoopBack 3.0](http://loopback.io/doc/en/lb3/Migrating-to-3.0.html#update-use-of-rest-error-handler).
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
@ -252,17 +253,17 @@ The same error generated when `debug: true` :
|
||||||
{ statusCode: 500,
|
{ statusCode: 500,
|
||||||
name: 'Error',
|
name: 'Error',
|
||||||
message: 'a test error message',
|
message: 'a test error message',
|
||||||
stack: 'Error: a test error message
|
stack: 'Error: a test error message
|
||||||
at Context.<anonymous> (User/strong-error-handler/test/handler.test.js:220:21)
|
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 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 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 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 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 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 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 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 Immediate._onImmediate (User/strong-error-handler/node_modules/mocha/lib/runner.js:320:5)
|
||||||
at tryOnImmediate (timers.js:543:15)
|
at tryOnImmediate (timers.js:543:15)
|
||||||
at processImmediate [as _immediateCallback] (timers.js:523:5)' }}
|
at processImmediate [as _immediateCallback] (timers.js:523:5)' }}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
@ -53,6 +53,7 @@ declare namespace errorHandlerFactory {
|
||||||
safeFields?: string[];
|
safeFields?: string[];
|
||||||
defaultType?: string;
|
defaultType?: string;
|
||||||
negotiateContentType?: boolean;
|
negotiateContentType?: boolean;
|
||||||
|
rootProperty?: string | false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -10,7 +10,6 @@ const SG = require('strong-globalize');
|
||||||
SG.SetRootDir(path.resolve(__dirname, '..'));
|
SG.SetRootDir(path.resolve(__dirname, '..'));
|
||||||
const buildResponseData = require('./data-builder');
|
const buildResponseData = require('./data-builder');
|
||||||
const debug = require('debug')('strong-error-handler');
|
const debug = require('debug')('strong-error-handler');
|
||||||
const format = require('util').format;
|
|
||||||
const logToConsole = require('./logger');
|
const logToConsole = require('./logger');
|
||||||
const negotiateContentProducer = require('./content-negotiation');
|
const negotiateContentProducer = require('./content-negotiation');
|
||||||
|
|
||||||
|
@ -50,7 +49,7 @@ function writeErrorToResponse(err, req, res, options) {
|
||||||
|
|
||||||
options = options || {};
|
options = options || {};
|
||||||
|
|
||||||
if (res._header) {
|
if (res.headersSent) {
|
||||||
debug('Response was already sent, closing the underlying connection');
|
debug('Response was already sent, closing the underlying connection');
|
||||||
return req.socket.destroy();
|
return req.socket.destroy();
|
||||||
}
|
}
|
||||||
|
@ -66,7 +65,7 @@ function writeErrorToResponse(err, req, res, options) {
|
||||||
res.statusCode = data.statusCode;
|
res.statusCode = data.statusCode;
|
||||||
|
|
||||||
const sendResponse = negotiateContentProducer(req, warn, options);
|
const sendResponse = negotiateContentProducer(req, warn, options);
|
||||||
sendResponse(res, data);
|
sendResponse(res, data, options);
|
||||||
|
|
||||||
function warn(msg) {
|
function warn(msg) {
|
||||||
res.header('X-Warning', msg);
|
res.header('X-Warning', msg);
|
||||||
|
|
|
@ -17,10 +17,10 @@ const compiledTemplates = {
|
||||||
module.exports = sendHtml;
|
module.exports = sendHtml;
|
||||||
|
|
||||||
function sendHtml(res, data, options) {
|
function sendHtml(res, data, options) {
|
||||||
const toRender = {options: {}, data: data};
|
const toRender = {options, data};
|
||||||
// TODO: ability to call non-default template functions from options
|
// TODO: ability to call non-default template functions from options
|
||||||
const body = compiledTemplates.default(toRender);
|
const body = compiledTemplates.default(toRender);
|
||||||
sendReponse(res, body);
|
sendResponse(res, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -41,7 +41,7 @@ function loadDefaultTemplates() {
|
||||||
return compileTemplate(defaultTemplate);
|
return compileTemplate(defaultTemplate);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendReponse(res, body) {
|
function sendResponse(res, body) {
|
||||||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||||||
res.end(body);
|
res.end(body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,14 @@
|
||||||
|
|
||||||
const safeStringify = require('fast-safe-stringify');
|
const safeStringify = require('fast-safe-stringify');
|
||||||
|
|
||||||
module.exports = function sendJson(res, data) {
|
module.exports = function sendJson(res, data, options) {
|
||||||
const content = safeStringify({error: data});
|
options = options || {};
|
||||||
|
// Set `options.rootProperty` to not wrap the data into an `error` object
|
||||||
|
const err = options.rootProperty === false ? data : {
|
||||||
|
// Use `options.rootProperty`, if not set, default to `error`
|
||||||
|
[options.rootProperty || 'error']: data,
|
||||||
|
};
|
||||||
|
const content = safeStringify(err);
|
||||||
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
||||||
res.end(content, 'utf-8');
|
res.end(content, 'utf-8');
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,12 @@
|
||||||
|
|
||||||
const js2xmlparser = require('js2xmlparser');
|
const js2xmlparser = require('js2xmlparser');
|
||||||
|
|
||||||
module.exports = function sendXml(res, data) {
|
module.exports = function sendXml(res, data, options) {
|
||||||
const content = js2xmlparser.parse('error', data);
|
options = options || {};
|
||||||
|
// Xml always requires a root element.
|
||||||
|
// `options.rootProperty === false` is not honored
|
||||||
|
const root = options.rootProperty || 'error';
|
||||||
|
const content = js2xmlparser.parse(root, data);
|
||||||
res.setHeader('Content-Type', 'text/xml; charset=utf-8');
|
res.setHeader('Content-Type', 'text/xml; charset=utf-8');
|
||||||
res.end(content, 'utf-8');
|
res.end(content, 'utf-8');
|
||||||
};
|
};
|
||||||
|
|
10
package.json
10
package.json
|
@ -4,7 +4,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"version": "3.4.0",
|
"version": "3.4.0",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=10"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
@ -20,18 +20,18 @@
|
||||||
"@types/express": "^4.16.0",
|
"@types/express": "^4.16.0",
|
||||||
"accepts": "^1.3.3",
|
"accepts": "^1.3.3",
|
||||||
"debug": "^4.1.1",
|
"debug": "^4.1.1",
|
||||||
"ejs": "^2.6.1",
|
"ejs": "^3.1.3",
|
||||||
"fast-safe-stringify": "^2.0.6",
|
"fast-safe-stringify": "^2.0.6",
|
||||||
"http-status": "^1.1.2",
|
"http-status": "^1.1.2",
|
||||||
"js2xmlparser": "^4.0.0",
|
"js2xmlparser": "^4.0.0",
|
||||||
"strong-globalize": "^5.0.2"
|
"strong-globalize": "^6.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"chai": "^4.1.2",
|
"chai": "^4.1.2",
|
||||||
"eslint": "^6.5.1",
|
"eslint": "^7.0.0",
|
||||||
"eslint-config-loopback": "^13.1.0",
|
"eslint-config-loopback": "^13.1.0",
|
||||||
"express": "^4.16.3",
|
"express": "^4.16.3",
|
||||||
"mocha": "^6.2.1",
|
"mocha": "^7.1.2",
|
||||||
"supertest": "^4.0.2"
|
"supertest": "^4.0.2"
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
|
|
|
@ -78,7 +78,7 @@ describe('strong-error-handler', function() {
|
||||||
request.get('/').expect(
|
request.get('/').expect(
|
||||||
507,
|
507,
|
||||||
{error: {statusCode: 507, message: 'Insufficient Storage'}},
|
{error: {statusCode: 507, message: 'Insufficient Storage'}},
|
||||||
done
|
done,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -146,7 +146,7 @@ describe('strong-error-handler', function() {
|
||||||
it('handles array argument', function(done) {
|
it('handles array argument', function(done) {
|
||||||
givenErrorHandlerForError(
|
givenErrorHandlerForError(
|
||||||
[new TypeError('ERR1'), new Error('ERR2')],
|
[new TypeError('ERR1'), new Error('ERR2')],
|
||||||
{log: true}
|
{log: true},
|
||||||
);
|
);
|
||||||
|
|
||||||
request.get('/api').end(function(err) {
|
request.get('/api').end(function(err) {
|
||||||
|
@ -541,6 +541,32 @@ describe('strong-error-handler', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('honors rootProperty', function(done) {
|
||||||
|
givenErrorHandlerForError('Error Message', {rootProperty: 'data'});
|
||||||
|
requestJson().expect(500).end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
expect(res.body.data).to.eql({
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Internal Server Error',
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('honors rootProperty=false', function(done) {
|
||||||
|
givenErrorHandlerForError('Error Message', {rootProperty: false});
|
||||||
|
requestJson().expect(500).end(function(err, res) {
|
||||||
|
if (err) return done(err);
|
||||||
|
|
||||||
|
expect(res.body).to.eql({
|
||||||
|
statusCode: 500,
|
||||||
|
message: 'Internal Server Error',
|
||||||
|
});
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function requestJson(url) {
|
function requestJson(url) {
|
||||||
return request.get(url || '/')
|
return request.get(url || '/')
|
||||||
.set('Accept', 'text/plain')
|
.set('Accept', 'text/plain')
|
||||||
|
@ -581,10 +607,10 @@ describe('strong-error-handler', function() {
|
||||||
expect(res.statusCode).to.eql(404);
|
expect(res.statusCode).to.eql(404);
|
||||||
const body = res.error.text;
|
const body = res.error.text;
|
||||||
expect(body).to.match(
|
expect(body).to.match(
|
||||||
/<title>Error<img onerror=alert\(1\) src=a><\/title>/
|
/<title>Error<img onerror=alert\(1\) src=a><\/title>/,
|
||||||
);
|
);
|
||||||
expect(body).to.match(
|
expect(body).to.match(
|
||||||
/with id <img onerror=alert\(1\) src=a> found for Model/
|
/with id <img onerror=alert\(1\) src=a> found for Model/,
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -602,7 +628,7 @@ describe('strong-error-handler', function() {
|
||||||
.expect(/<title>ErrorWithProps<\/title>/)
|
.expect(/<title>ErrorWithProps<\/title>/)
|
||||||
.expect(
|
.expect(
|
||||||
/500(.*?)a test error message<img onerror=alert\(1\) src=a>/,
|
/500(.*?)a test error message<img onerror=alert\(1\) src=a>/,
|
||||||
done
|
done,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -696,7 +722,7 @@ describe('strong-error-handler', function() {
|
||||||
expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
|
expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
|
||||||
expect(body).to.match(/<name>ValidationError<\/name>/);
|
expect(body).to.match(/<name>ValidationError<\/name>/);
|
||||||
expect(body).to.match(
|
expect(body).to.match(
|
||||||
/<message>The model instance is not valid.<\/message>/
|
/<message>The model instance is not valid.<\/message>/,
|
||||||
);
|
);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
@ -728,6 +754,54 @@ describe('strong-error-handler', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('honors options.rootProperty', function(done) {
|
||||||
|
const error = new ErrorWithProps({
|
||||||
|
name: 'ValidationError',
|
||||||
|
message: 'The model instance is not valid.',
|
||||||
|
statusCode: 422,
|
||||||
|
details: 'some details',
|
||||||
|
extra: 'sensitive data',
|
||||||
|
});
|
||||||
|
givenErrorHandlerForError(error, {rootProperty: 'myRoot'});
|
||||||
|
requestXML()
|
||||||
|
.end(function(err, res) {
|
||||||
|
expect(res.statusCode).to.eql(422);
|
||||||
|
const body = res.error.text;
|
||||||
|
expect(body).to.match(/<myRoot>/);
|
||||||
|
expect(body).to.match(/<details>some details<\/details>/);
|
||||||
|
expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
|
||||||
|
expect(body).to.match(/<name>ValidationError<\/name>/);
|
||||||
|
expect(body).to.match(
|
||||||
|
/<message>The model instance is not valid.<\/message>/,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores options.rootProperty = false', function(done) {
|
||||||
|
const error = new ErrorWithProps({
|
||||||
|
name: 'ValidationError',
|
||||||
|
message: 'The model instance is not valid.',
|
||||||
|
statusCode: 422,
|
||||||
|
details: 'some details',
|
||||||
|
extra: 'sensitive data',
|
||||||
|
});
|
||||||
|
givenErrorHandlerForError(error, {rootProperty: false});
|
||||||
|
requestXML()
|
||||||
|
.end(function(err, res) {
|
||||||
|
expect(res.statusCode).to.eql(422);
|
||||||
|
const body = res.error.text;
|
||||||
|
expect(body).to.match(/<error>/);
|
||||||
|
expect(body).to.match(/<details>some details<\/details>/);
|
||||||
|
expect(body).to.not.match(/<extra>sensitive data<\/extra>/);
|
||||||
|
expect(body).to.match(/<name>ValidationError<\/name>/);
|
||||||
|
expect(body).to.match(
|
||||||
|
/<message>The model instance is not valid.<\/message>/,
|
||||||
|
);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function requestXML(url) {
|
function requestXML(url) {
|
||||||
return request.get(url || '/')
|
return request.get(url || '/')
|
||||||
.set('Accept', 'text/xml')
|
.set('Accept', 'text/xml')
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset='utf-8'>
|
<meta charset='utf-8'>
|
||||||
<title><%= data.name || data.message %></title>
|
<title><%= data.name || data.message %></title>
|
||||||
<style><%- include style.css %></style>
|
<style><%- include('style.css') %></style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="wrapper">
|
<div id="wrapper">
|
||||||
|
|
Loading…
Reference in New Issue