Skip to content

Commit 4b29875

Browse files
authored
Merge pull request #246 from Tronix117/feature/125-Error-Stack
feat(error debug) #125 - Return a `source.stack` key on response when error
2 parents 399e732 + 94b7435 commit 4b29875

File tree

3 files changed

+180
-9
lines changed

3 files changed

+180
-9
lines changed

README.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ Example:
118118
"host": "https://www.mydomain.com",
119119
"enable": true,
120120
"handleErrors": true,
121+
"errorStackInResponse": false,
121122
"exclude": [
122123
{"model": "comment"},
123124
{"methods": "find"},
@@ -201,6 +202,24 @@ out of the box with EmberJS.
201202
- Type: `boolean`
202203
- Default: `true`
203204

205+
### errorStackInResponse
206+
Along handleErrors, When true, this option will send the error stack if available within the error
207+
response. It will be stored under the `source.stack` key.
208+
209+
**Please be careful, this option should never be enabled in a production environment. Doing so can expose sensitive data.**
210+
211+
#### example
212+
```js
213+
{
214+
...
215+
"errorStackInResponse": NODE_ENV === 'development',
216+
...
217+
}
218+
```
219+
220+
- Type: `boolean`
221+
- Default: `false`
222+
204223
### exclude
205224
Allows blacklisting of models and methods.
206225
Define an array of blacklist objects. Blacklist objects can contain "model" key
@@ -419,6 +438,45 @@ module.exports = function (MyModel) {
419438
}
420439
```
421440

441+
## Custom Errors
442+
Generic errors respond with a 500, but sometimes you want to have a better control over the error that is returned to the client, taking advantages of fields provided by JSONApi.
443+
444+
**It is recommended that you extend the base Error constructor before throwing errors. Eg. BadRequestError**
445+
446+
`meta` and `source` fields needs to be objects.
447+
448+
#### example
449+
```js
450+
module.exports = function (MyModel) {
451+
MyModel.find = function () {
452+
var err = new Error('April 1st, 1998');
453+
454+
err.status = 418;
455+
err.name = 'I\'m a teapot';
456+
err.source = { model: 'Post', method: 'find' };
457+
err.detail = 'April 1st, 1998';
458+
err.code = 'i\'m a teapot';
459+
err.meta = { rfc: 'RFC2324' };
460+
461+
throw err
462+
}
463+
}
464+
465+
// This will be returned as :
466+
// {
467+
// errors: [
468+
// {
469+
// status: 418,
470+
// meta: { rfc: 'RFC2324' },
471+
// code: 'i\'m a teapot',
472+
// detail: 'April 1st, 1998',
473+
// title: 'I\'m a teapot',
474+
// source: { model: 'Post', method: 'find' }
475+
// }
476+
// ]
477+
// }
478+
```
479+
422480
##### function parameters
423481

424482
- `options` All config options set for the deserialization process.

lib/errors.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
'use strict'
22

33
var debug
4+
var errorStackInResponse
45
var statusCodes = require('http-status-codes')
6+
var _ = require('lodash')
57

68
module.exports = function (app, options) {
79
debug = options.debug
10+
errorStackInResponse = options.errorStackInResponse
811

912
if (options.handleErrors !== false) {
1013
debug(
@@ -49,7 +52,7 @@ function JSONAPIErrorHandler (err, req, res, next) {
4952
err.details.messages[key][0],
5053
err.details.codes[key][0],
5154
err.name,
52-
key
55+
{ pointer: 'data/attributes/' + key }
5356
)
5457
})
5558
} else if (err.message) {
@@ -79,8 +82,24 @@ function JSONAPIErrorHandler (err, req, res, next) {
7982
err.name = 'BadRequest'
8083
}
8184

85+
var errorSource = err.source && typeof err.source === 'object'
86+
? err.source
87+
: {}
88+
if (errorStackInResponse) {
89+
// We do not want to mutate err.source, so we clone it first
90+
errorSource = _.clone(errorSource)
91+
errorSource.stack = err.stack
92+
}
93+
8294
errors.push(
83-
buildErrorResponse(statusCode, err.message, err.code, err.name)
95+
buildErrorResponse(
96+
statusCode,
97+
err.message,
98+
err.code,
99+
err.name,
100+
errorSource,
101+
err.meta
102+
)
84103
)
85104
} else {
86105
debug(
@@ -111,21 +130,29 @@ function JSONAPIErrorHandler (err, req, res, next) {
111130
* @param {String} errorDetail error message for the user, human readable
112131
* @param {String} errorCode internal system error code
113132
* @param {String} errorName error title for the user, human readable
114-
* @param {String} propertyName for validation errors, name of property validation refers to
133+
* @param {String} errorSource Some information about the source of the issue
134+
* @param {String} errorMeta Some custom meta information to give to the error response
115135
* @return {Object}
116136
*/
117137
function buildErrorResponse (
118138
httpStatusCode,
119139
errorDetail,
120140
errorCode,
121141
errorName,
122-
propertyName
142+
errorSource,
143+
errorMeta
123144
) {
124-
return {
145+
var out = {
125146
status: httpStatusCode || statusCodes.INTERNAL_SERVER_ERROR,
126-
source: propertyName ? { pointer: 'data/attributes/' + propertyName } : '',
147+
source: errorSource || {},
127148
title: errorName || '',
128149
code: errorCode || '',
129150
detail: errorDetail || ''
130151
}
152+
153+
if (errorMeta && typeof errorMeta === 'object') {
154+
out.meta = errorMeta
155+
}
156+
157+
return out
131158
}

test/errors.test.js

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
var request = require('supertest')
44
var loopback = require('loopback')
5+
var _ = require('lodash')
56
var expect = require('chai').expect
67
var JSONAPIComponent = require('../')
78
var app
@@ -24,7 +25,7 @@ describe('disabling loopback-component-jsonapi error handler', function () {
2425
request(app).get('/posts/100').end(function (err, res) {
2526
expect(err).to.equal(null)
2627
expect(res.body).to.have.keys('error')
27-
expect(res.body.error).to.have.keys('name', 'message', 'statusCode')
28+
expect(res.body.error).to.contain.keys('name', 'message', 'statusCode')
2829
done()
2930
})
3031
})
@@ -87,7 +88,7 @@ describe('loopback json api errors', function () {
8788
status: 404,
8889
code: 'MODEL_NOT_FOUND',
8990
detail: 'Unknown "post" id "100".',
90-
source: '',
91+
source: {},
9192
title: 'Error'
9293
})
9394
done()
@@ -129,7 +130,7 @@ describe('loopback json api errors', function () {
129130
status: 422,
130131
code: 'presence',
131132
detail: 'JSON API resource object must contain `data.type` property',
132-
source: '',
133+
source: {},
133134
title: 'ValidationError'
134135
})
135136
done()
@@ -178,3 +179,88 @@ describe('loopback json api errors', function () {
178179
)
179180
})
180181
})
182+
183+
describe('loopback json api errors with advanced reporting', function () {
184+
var errorMetaMock = {
185+
status: 418,
186+
meta: { rfc: 'RFC2324' },
187+
code: "i'm a teapot",
188+
detail: 'April 1st, 1998',
189+
title: "I'm a teapot",
190+
source: { model: 'Post', method: 'find' }
191+
}
192+
193+
beforeEach(function () {
194+
app = loopback()
195+
app.set('legacyExplorer', false)
196+
var ds = loopback.createDataSource('memory')
197+
Post = ds.createModel('post', {
198+
id: { type: Number, id: true },
199+
title: String,
200+
content: String
201+
})
202+
203+
Post.find = function () {
204+
var err = new Error(errorMetaMock.detail)
205+
err.name = errorMetaMock.title
206+
err.meta = errorMetaMock.meta
207+
err.source = errorMetaMock.source
208+
err.statusCode = errorMetaMock.status
209+
err.code = errorMetaMock.code
210+
throw err
211+
}
212+
213+
app.model(Post)
214+
app.use(loopback.rest())
215+
JSONAPIComponent(app, { restApiRoot: '', errorStackInResponse: true })
216+
})
217+
218+
it(
219+
'should return the given meta and source in the error response when an Error with a meta and source object is thrown',
220+
function (done) {
221+
request(app)
222+
.get('/posts')
223+
.set('Content-Type', 'application/json')
224+
.end(function (err, res) {
225+
expect(err).to.equal(null)
226+
expect(res.body).to.have.keys('errors')
227+
expect(res.body.errors.length).to.equal(1)
228+
229+
expect(_.omit(res.body.errors[0], 'source.stack')).to.deep.equal(
230+
errorMetaMock
231+
)
232+
done()
233+
})
234+
}
235+
)
236+
237+
it(
238+
'should return the corresponding stack in error when `errorStackInResponse` enabled',
239+
function (done) {
240+
request(app)
241+
.post('/posts')
242+
.send({
243+
data: {
244+
attributes: { title: 'my post', content: 'my post content' }
245+
}
246+
})
247+
.set('Content-Type', 'application/json')
248+
.end(function (err, res) {
249+
expect(err).to.equal(null)
250+
expect(res.body).to.have.keys('errors')
251+
expect(res.body.errors.length).to.equal(1)
252+
253+
expect(res.body.errors[0].source).to.haveOwnProperty('stack')
254+
expect(res.body.errors[0].source.stack.length).to.be.above(100)
255+
256+
expect(_.omit(res.body.errors[0], 'source')).to.deep.equal({
257+
status: 422,
258+
code: 'presence',
259+
detail: 'JSON API resource object must contain `data.type` property',
260+
title: 'ValidationError'
261+
})
262+
done()
263+
})
264+
}
265+
)
266+
})

0 commit comments

Comments
 (0)