Skip to content

Commit

Permalink
Merge pull request #246 from Tronix117/feature/125-Error-Stack
Browse files Browse the repository at this point in the history
feat(error debug) #125 - Return a `source.stack` key on response when error
  • Loading branch information
digitalsadhu authored Nov 17, 2017
2 parents 399e732 + 94b7435 commit 4b29875
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 9 deletions.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ Example:
"host": "https://www.mydomain.com",
"enable": true,
"handleErrors": true,
"errorStackInResponse": false,
"exclude": [
{"model": "comment"},
{"methods": "find"},
Expand Down Expand Up @@ -201,6 +202,24 @@ out of the box with EmberJS.
- Type: `boolean`
- Default: `true`

### errorStackInResponse
Along handleErrors, When true, this option will send the error stack if available within the error
response. It will be stored under the `source.stack` key.

**Please be careful, this option should never be enabled in a production environment. Doing so can expose sensitive data.**

#### example
```js
{
...
"errorStackInResponse": NODE_ENV === 'development',
...
}
```

- Type: `boolean`
- Default: `false`

### exclude
Allows blacklisting of models and methods.
Define an array of blacklist objects. Blacklist objects can contain "model" key
Expand Down Expand Up @@ -419,6 +438,45 @@ module.exports = function (MyModel) {
}
```

## Custom Errors
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.

**It is recommended that you extend the base Error constructor before throwing errors. Eg. BadRequestError**

`meta` and `source` fields needs to be objects.

#### example
```js
module.exports = function (MyModel) {
MyModel.find = function () {
var err = new Error('April 1st, 1998');

err.status = 418;
err.name = 'I\'m a teapot';
err.source = { model: 'Post', method: 'find' };
err.detail = 'April 1st, 1998';
err.code = 'i\'m a teapot';
err.meta = { rfc: 'RFC2324' };

throw err
}
}

// This will be returned as :
// {
// errors: [
// {
// status: 418,
// meta: { rfc: 'RFC2324' },
// code: 'i\'m a teapot',
// detail: 'April 1st, 1998',
// title: 'I\'m a teapot',
// source: { model: 'Post', method: 'find' }
// }
// ]
// }
```

##### function parameters

- `options` All config options set for the deserialization process.
Expand Down
39 changes: 33 additions & 6 deletions lib/errors.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
'use strict'

var debug
var errorStackInResponse
var statusCodes = require('http-status-codes')
var _ = require('lodash')

module.exports = function (app, options) {
debug = options.debug
errorStackInResponse = options.errorStackInResponse

if (options.handleErrors !== false) {
debug(
Expand Down Expand Up @@ -49,7 +52,7 @@ function JSONAPIErrorHandler (err, req, res, next) {
err.details.messages[key][0],
err.details.codes[key][0],
err.name,
key
{ pointer: 'data/attributes/' + key }
)
})
} else if (err.message) {
Expand Down Expand Up @@ -79,8 +82,24 @@ function JSONAPIErrorHandler (err, req, res, next) {
err.name = 'BadRequest'
}

var errorSource = err.source && typeof err.source === 'object'
? err.source
: {}
if (errorStackInResponse) {
// We do not want to mutate err.source, so we clone it first
errorSource = _.clone(errorSource)
errorSource.stack = err.stack
}

errors.push(
buildErrorResponse(statusCode, err.message, err.code, err.name)
buildErrorResponse(
statusCode,
err.message,
err.code,
err.name,
errorSource,
err.meta
)
)
} else {
debug(
Expand Down Expand Up @@ -111,21 +130,29 @@ function JSONAPIErrorHandler (err, req, res, next) {
* @param {String} errorDetail error message for the user, human readable
* @param {String} errorCode internal system error code
* @param {String} errorName error title for the user, human readable
* @param {String} propertyName for validation errors, name of property validation refers to
* @param {String} errorSource Some information about the source of the issue
* @param {String} errorMeta Some custom meta information to give to the error response
* @return {Object}
*/
function buildErrorResponse (
httpStatusCode,
errorDetail,
errorCode,
errorName,
propertyName
errorSource,
errorMeta
) {
return {
var out = {
status: httpStatusCode || statusCodes.INTERNAL_SERVER_ERROR,
source: propertyName ? { pointer: 'data/attributes/' + propertyName } : '',
source: errorSource || {},
title: errorName || '',
code: errorCode || '',
detail: errorDetail || ''
}

if (errorMeta && typeof errorMeta === 'object') {
out.meta = errorMeta
}

return out
}
92 changes: 89 additions & 3 deletions test/errors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

var request = require('supertest')
var loopback = require('loopback')
var _ = require('lodash')
var expect = require('chai').expect
var JSONAPIComponent = require('../')
var app
Expand All @@ -24,7 +25,7 @@ describe('disabling loopback-component-jsonapi error handler', function () {
request(app).get('/posts/100').end(function (err, res) {
expect(err).to.equal(null)
expect(res.body).to.have.keys('error')
expect(res.body.error).to.have.keys('name', 'message', 'statusCode')
expect(res.body.error).to.contain.keys('name', 'message', 'statusCode')
done()
})
})
Expand Down Expand Up @@ -87,7 +88,7 @@ describe('loopback json api errors', function () {
status: 404,
code: 'MODEL_NOT_FOUND',
detail: 'Unknown "post" id "100".',
source: '',
source: {},
title: 'Error'
})
done()
Expand Down Expand Up @@ -129,7 +130,7 @@ describe('loopback json api errors', function () {
status: 422,
code: 'presence',
detail: 'JSON API resource object must contain `data.type` property',
source: '',
source: {},
title: 'ValidationError'
})
done()
Expand Down Expand Up @@ -178,3 +179,88 @@ describe('loopback json api errors', function () {
)
})
})

describe('loopback json api errors with advanced reporting', function () {
var errorMetaMock = {
status: 418,
meta: { rfc: 'RFC2324' },
code: "i'm a teapot",
detail: 'April 1st, 1998',
title: "I'm a teapot",
source: { model: 'Post', method: 'find' }
}

beforeEach(function () {
app = loopback()
app.set('legacyExplorer', false)
var ds = loopback.createDataSource('memory')
Post = ds.createModel('post', {
id: { type: Number, id: true },
title: String,
content: String
})

Post.find = function () {
var err = new Error(errorMetaMock.detail)
err.name = errorMetaMock.title
err.meta = errorMetaMock.meta
err.source = errorMetaMock.source
err.statusCode = errorMetaMock.status
err.code = errorMetaMock.code
throw err
}

app.model(Post)
app.use(loopback.rest())
JSONAPIComponent(app, { restApiRoot: '', errorStackInResponse: true })
})

it(
'should return the given meta and source in the error response when an Error with a meta and source object is thrown',
function (done) {
request(app)
.get('/posts')
.set('Content-Type', 'application/json')
.end(function (err, res) {
expect(err).to.equal(null)
expect(res.body).to.have.keys('errors')
expect(res.body.errors.length).to.equal(1)

expect(_.omit(res.body.errors[0], 'source.stack')).to.deep.equal(
errorMetaMock
)
done()
})
}
)

it(
'should return the corresponding stack in error when `errorStackInResponse` enabled',
function (done) {
request(app)
.post('/posts')
.send({
data: {
attributes: { title: 'my post', content: 'my post content' }
}
})
.set('Content-Type', 'application/json')
.end(function (err, res) {
expect(err).to.equal(null)
expect(res.body).to.have.keys('errors')
expect(res.body.errors.length).to.equal(1)

expect(res.body.errors[0].source).to.haveOwnProperty('stack')
expect(res.body.errors[0].source.stack.length).to.be.above(100)

expect(_.omit(res.body.errors[0], 'source')).to.deep.equal({
status: 422,
code: 'presence',
detail: 'JSON API resource object must contain `data.type` property',
title: 'ValidationError'
})
done()
})
}
)
})

0 comments on commit 4b29875

Please sign in to comment.