koa-better-error-handler
Advanced tools
Comparing version 5.0.0 to 6.0.0
291
index.js
const fs = require('fs'); | ||
const path = require('path'); | ||
const fastSafeStringify = require('fast-safe-stringify'); | ||
const Boom = require('@hapi/boom'); | ||
const Debug = require('debug'); | ||
const camelCase = require('camelcase'); | ||
@@ -32,4 +32,2 @@ const capitalize = require('capitalize'); | ||
const debug = new Debug('koa-better-error-handler'); | ||
const passportLocalMongooseErrorNames = [ | ||
@@ -55,175 +53,180 @@ 'AuthenticationError', | ||
// eslint-disable-next-line complexity | ||
async function errorHandler(err) { | ||
if (!err) return; | ||
function errorHandler( | ||
cookiesKey = false, | ||
_logger = console, | ||
useCtxLogger = true, // useful if you have ctx.logger (e.g. you're using Cabin's middleware) | ||
stringify = fastSafeStringify // you could alternatively use JSON.stringify | ||
) { | ||
// eslint-disable-next-line complexity | ||
return async function (err) { | ||
if (!err) return; | ||
if (!_isError(err)) err = new Error(err); | ||
const logger = useCtxLogger && this.logger ? this.logger : _logger; | ||
const type = this.accepts(['text', 'json', 'html']); | ||
if (!_isError(err)) err = new Error(err); | ||
if (!type) { | ||
debug('invalid type, sending 406 error'); | ||
err.status = 406; | ||
err.message = Boom.notAcceptable().output.payload; | ||
} | ||
const type = this.accepts(['text', 'json', 'html']); | ||
// parse mongoose validation errors | ||
err = parseValidationError(this, err); | ||
if (!type) { | ||
logger.warn('invalid type, sending 406 error'); | ||
err.status = 406; | ||
err.message = Boom.notAcceptable().output.payload; | ||
} | ||
// check if we threw just a status code in order to keep it simple | ||
const val = parseInt(err.message, 10); | ||
if (_isNumber(val) && val >= 400) | ||
err = Boom[camelCase(toIdentifier(statuses.message[val]))](); | ||
// parse mongoose validation errors | ||
err = parseValidationError(this, err); | ||
// check if we have a boom error that specified | ||
// a status code already for us (and then use it) | ||
if (_isObject(err.output) && _isNumber(err.output.statusCode)) | ||
err.status = err.output.statusCode; | ||
// check if we threw just a status code in order to keep it simple | ||
const val = parseInt(err.message, 10); | ||
if (_isNumber(val) && val >= 400) | ||
err = Boom[camelCase(toIdentifier(statuses.message[val]))](); | ||
if (!_isNumber(err.status)) err.status = 500; | ||
// check if we have a boom error that specified | ||
// a status code already for us (and then use it) | ||
if (_isObject(err.output) && _isNumber(err.output.statusCode)) | ||
err.status = err.output.statusCode; | ||
// check if there is flash messaging | ||
const hasFlash = _isFunction(this.flash); | ||
debug('hasFlash', hasFlash); | ||
if (!_isNumber(err.status)) err.status = 500; | ||
// check if there is a view rendering engine binding `this.render` | ||
const hasRender = _isFunction(this.render); | ||
debug('hasRender', hasRender); | ||
// check if there is flash messaging | ||
const hasFlash = _isFunction(this.flash); | ||
// check if we're about to go into a possible endless redirect loop | ||
const noReferrer = this.get('Referrer') === ''; | ||
// check if there is a view rendering engine binding `this.render` | ||
const hasRender = _isFunction(this.render); | ||
// nothing we can do here other | ||
// than delegate to the app-level | ||
// handler and log. | ||
if (this.headerSent || !this.writable) { | ||
debug('headers were already sent, returning early'); | ||
err.headerSent = true; | ||
return; | ||
} | ||
// check if we're about to go into a possible endless redirect loop | ||
const noReferrer = this.get('Referrer') === ''; | ||
// populate the status and body with `boom` error message payload | ||
// (e.g. you can do `ctx.throw(404)` and it will output a beautiful err obj) | ||
err.status = err.status || 500; | ||
err.statusCode = err.status; | ||
this.statusCode = err.statusCode; | ||
this.status = this.statusCode; | ||
// nothing we can do here other | ||
// than delegate to the app-level | ||
// handler and log. | ||
if (this.headerSent || !this.writable) { | ||
logger.error(new Error('Headers were already sent, returning early')); | ||
err.headerSent = true; | ||
return; | ||
} | ||
const friendlyAPIMessage = makeAPIFriendly(this, err.message); | ||
// populate the status and body with `boom` error message payload | ||
// (e.g. you can do `ctx.throw(404)` and it will output a beautiful err obj) | ||
err.status = err.status || 500; | ||
err.statusCode = err.status; | ||
this.statusCode = err.statusCode; | ||
this.status = this.statusCode; | ||
this.body = new Boom.Boom(friendlyAPIMessage, { | ||
statusCode: err.status | ||
}).output.payload; | ||
const friendlyAPIMessage = makeAPIFriendly(this, err.message); | ||
// set any additional error headers specified | ||
// (e.g. for BasicAuth we use `basic-auth` which specifies WWW-Authenticate) | ||
if (_isObject(err.headers) && Object.keys(err.headers).length > 0) | ||
this.set(err.headers); | ||
this.body = new Boom.Boom(friendlyAPIMessage, { | ||
statusCode: err.status | ||
}).output.payload; | ||
debug('status code was %d', this.status); | ||
// set any additional error headers specified | ||
// (e.g. for BasicAuth we use `basic-auth` which specifies WWW-Authenticate) | ||
if (_isObject(err.headers) && Object.keys(err.headers).length > 0) | ||
this.set(err.headers); | ||
this.app.emit('error', err, this); | ||
this.app.emit('error', err, this); | ||
// fix page title and description | ||
if (!this.api) { | ||
this.state.meta = this.state.meta || {}; | ||
this.state.meta.title = this.body.error; | ||
this.state.meta.description = err.message; | ||
debug('set `this.state.meta.title` to %s', this.state.meta.title); | ||
debug('set `this.state.meta.desc` to %s', this.state.meta.description); | ||
} | ||
// fix page title and description | ||
if (!this.api) { | ||
this.state.meta = this.state.meta || {}; | ||
this.state.meta.title = this.body.error; | ||
this.state.meta.description = err.message; | ||
} | ||
debug('type was %s', type); | ||
switch (type) { | ||
case 'html': | ||
this.type = 'html'; | ||
switch (type) { | ||
case 'html': | ||
this.type = 'html'; | ||
if (this.status === 404) { | ||
// render the 404 page | ||
// https://github.com/koajs/koa/issues/646 | ||
if (hasRender) { | ||
try { | ||
debug('rendering 404 page'); | ||
await this.render('404'); | ||
} catch (err_) { | ||
debug('could not find 404 page, using built-in 404 html', err_); | ||
if (this.status === 404) { | ||
// render the 404 page | ||
// https://github.com/koajs/koa/issues/646 | ||
if (hasRender) { | ||
try { | ||
await this.render('404'); | ||
} catch (err_) { | ||
logger.error(err_); | ||
this.body = _404; | ||
} | ||
} else { | ||
this.body = _404; | ||
} | ||
} else { | ||
this.body = _404; | ||
} | ||
} else if (noReferrer || this.status === 500) { | ||
// this prevents a redirect loop by detecting an empty Referrer | ||
// ...otherwise it would reach the next conditional block which | ||
// would endlessly rediret the user with `this.redirect('back')` | ||
if (noReferrer) debug('prevented endless redirect loop!'); | ||
} else if (noReferrer || this.status === 500) { | ||
// this prevents a redirect loop by detecting an empty Referrer | ||
// ...otherwise it would reach the next conditional block which | ||
// would endlessly rediret the user with `this.redirect('back')` | ||
// flash an error message | ||
if (hasFlash) this.flash('error', err.message); | ||
// flash an error message | ||
if (hasFlash) this.flash('error', err.message); | ||
// render the 500 page | ||
if (hasRender) { | ||
try { | ||
debug('rendering 500 page'); | ||
await this.render('500'); | ||
} catch (err_) { | ||
debug('could not find 500 page, using built-in 500 html', err_); | ||
// render the 500 page | ||
if (hasRender) { | ||
try { | ||
await this.render('500'); | ||
} catch (err_) { | ||
logger.error(err_); | ||
this.body = _500; | ||
} | ||
} else { | ||
this.body = _500; | ||
} | ||
} else { | ||
this.body = _500; | ||
} | ||
} else { | ||
// flash an error message | ||
if (hasFlash) this.flash('error', err.message); | ||
// flash an error message | ||
if (hasFlash) this.flash('error', err.message); | ||
// TODO: until the issue is resolved, we need to add this here | ||
// <https://github.com/koajs/generic-session/pull/95#issuecomment-246308544> | ||
if ( | ||
this.sessionStore && | ||
this.sessionId && | ||
this.session && | ||
this.state.cookiesKey | ||
) { | ||
await co | ||
.wrap(this.sessionStore.set) | ||
.call(this.sessionStore, this.sessionId, this.session); | ||
this.cookies.set( | ||
this.state.cookiesKey, | ||
this.sessionId, | ||
this.session.cookie | ||
// TODO: until the issue is resolved, we need to add this here | ||
// <https://github.com/koajs/generic-session/pull/95#issuecomment-246308544> | ||
if ( | ||
this.sessionStore && | ||
this.sessionId && | ||
this.session && | ||
cookiesKey | ||
) { | ||
try { | ||
await co | ||
.wrap(this.sessionStore.set) | ||
.call(this.sessionStore, this.sessionId, this.session); | ||
this.cookies.set(cookiesKey, this.sessionId, this.session.cookie); | ||
} catch (err) { | ||
logger.error(err); | ||
// eslint-disable-next-line max-depth | ||
if (err.code === 'ERR_HTTP_HEADERS_SENT') return; | ||
} | ||
} | ||
/* | ||
// TODO: we need to add support for `koa-session-store` here | ||
// <https://github.com/koajs/generic-session/pull/95#issuecomment-246308544> | ||
// | ||
// these comments may no longer be valid and need reconsidered: | ||
// | ||
// if we're using `koa-session-store` we need to add | ||
// `this._session = new Session()`, and then run this: | ||
await co.wrap(this._session._store.save).call( | ||
this._session._store, | ||
this._session._sid, | ||
stringify(this.session) | ||
); | ||
this.cookies.set(this._session._name, stringify({ | ||
_sid: this._session._sid | ||
}), this._session._cookieOpts); | ||
*/ | ||
// redirect the user to the page they were just on | ||
this.redirect('back'); | ||
} | ||
/* | ||
// if we're using `koa-session-store` we need to add | ||
// `this._session = new Session()`, and then run this: | ||
await co.wrap(this._session._store.save).call( | ||
this._session._store, | ||
this._session._sid, | ||
JSON.stringify(this.session) | ||
); | ||
this.cookies.set(this._session._name, JSON.stringify({ | ||
_sid: this._session._sid | ||
}), this._session._cookieOpts); | ||
*/ | ||
break; | ||
case 'json': | ||
this.type = 'json'; | ||
this.body = stringify(this.body, null, 2); | ||
break; | ||
default: | ||
this.type = this.api ? 'json' : 'text'; | ||
this.body = stringify(this.body, null, 2); | ||
break; | ||
} | ||
// redirect the user to the page they were just on | ||
this.redirect('back'); | ||
} | ||
break; | ||
case 'json': | ||
this.type = 'json'; | ||
this.body = JSON.stringify(this.body, null, 2); | ||
break; | ||
default: | ||
this.type = this.api ? 'json' : 'text'; | ||
this.body = JSON.stringify(this.body, null, 2); | ||
break; | ||
} | ||
this.length = Buffer.byteLength(this.body); | ||
this.res.end(this.body); | ||
this.length = Buffer.byteLength(this.body); | ||
this.res.end(this.body); | ||
}; | ||
} | ||
@@ -230,0 +233,0 @@ |
{ | ||
"name": "koa-better-error-handler", | ||
"description": "A better error-handler for Lad and Koa. Makes `ctx.throw` awesome (best used with koa-404-handler)", | ||
"version": "5.0.0", | ||
"version": "6.0.0", | ||
"author": { | ||
@@ -43,3 +43,3 @@ "name": "Nick Baugh", | ||
"co": "^4.6.0", | ||
"debug": "^4.1.1", | ||
"fast-safe-stringify": "^2.0.7", | ||
"html-to-text": "^5.1.1", | ||
@@ -58,7 +58,7 @@ "humanize-string": "^2.1.0", | ||
"devDependencies": { | ||
"@commitlint/cli": "^9.0.1", | ||
"@commitlint/config-conventional": "^9.0.1", | ||
"@koa/router": "^9.3.1", | ||
"ava": "3.8.2", | ||
"codecov": "^3.7.0", | ||
"@commitlint/cli": "^9.1.2", | ||
"@commitlint/config-conventional": "^9.1.2", | ||
"@koa/router": "^9.4.0", | ||
"ava": "3.11.1", | ||
"codecov": "^3.7.2", | ||
"cross-env": "^7.0.2", | ||
@@ -76,12 +76,13 @@ "eslint-config-xo-lass": "^1.0.3", | ||
"lint-staged": "^10.2.11", | ||
"lodash": "^4.17.15", | ||
"lodash": "^4.17.20", | ||
"nyc": "^15.1.0", | ||
"redis": "^3.0.2", | ||
"remark-cli": "^8.0.0", | ||
"remark-preset-github": "^1.0.1", | ||
"remark-cli": "^8.0.1", | ||
"remark-preset-github": "^3.0.0", | ||
"rimraf": "^3.0.2", | ||
"supertest": "^4.0.2", | ||
"xo": "^0.32.1" | ||
"xo": "^0.33.0" | ||
}, | ||
"engines": { | ||
"node": ">= 12" | ||
"node": ">= 10.14" | ||
}, | ||
@@ -146,8 +147,10 @@ "homepage": "https://github.com/ladjs/koa-better-error-handler", | ||
"scripts": { | ||
"coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov", | ||
"coverage": "nyc report --reporter=text-lcov > coverage.lcov", | ||
"lint": "npm run lint:js", | ||
"lint:js": "xo", | ||
"lint:md": "remark . -qfo", | ||
"postcoverage": "codecov", | ||
"precommit": "lint-staged && npm tes-ci", | ||
"pretest-ci": "npm run lint", | ||
"pretest-cov": "rimraf .nyc_output", | ||
"test": "ava", | ||
@@ -154,0 +157,0 @@ "test-ci": "npm run test-cov", |
@@ -49,2 +49,15 @@ # koa-better-error-handler | ||
The package exports a function which accepts four arguments (in order): | ||
* `cookiesKey` - defaults to `false` | ||
* `logger` - defaults to `console` | ||
* `useCtxLogger` - defaults to `true` | ||
* `stringify` - defaults to `fast-safe-stringify` (you can also use `JSON.stringify` or another option here if preferred) | ||
If you pass a `cookiesKey` then support for sessions will be added. You should always set this argument's value if you are using cookies and sessions (e.g. web server). | ||
We recommend to use [Cabin][] for your `logger` and also you should use its middleware too, as it will auto-populate `ctx.logger` for you to make context-based logs easy. | ||
Note that this package only supports `koa-generic-session`, and does not yet support `koa-session-store` (see the code in [index.js](index.js) for more insight, pull requests are welcome). | ||
### API | ||
@@ -64,3 +77,3 @@ | ||
// override koa's undocumented error handler | ||
app.context.onerror = errorHandler; | ||
app.context.onerror = errorHandler(); | ||
@@ -120,5 +133,7 @@ // specify that this is our api | ||
// add sessions to our app | ||
const cookiesKey = 'lad.sid'; | ||
app.use( | ||
convert( | ||
session({ | ||
key: cookiesKey, | ||
store: redisStore | ||
@@ -133,3 +148,3 @@ }) | ||
// override koa's undocumented error handler | ||
app.context.onerror = errorHandler; | ||
app.context.onerror = errorHandler({ cookiesKey }); | ||
@@ -251,1 +266,3 @@ // use koa-404-handler | ||
[koa-404-handler]: https://github.com/ladjs/koa-404-handler | ||
[cabin]: https://cabinjs.com |
@@ -13,6 +13,6 @@ const http = require('http'); | ||
const statusCodes = _.keys(http.STATUS_CODES) | ||
.map(code => { | ||
.map((code) => { | ||
return parseInt(code, 10); | ||
}) | ||
.filter(code => code >= 400); | ||
.filter((code) => code >= 400); | ||
@@ -24,3 +24,3 @@ // this doesn't ensure 100% code coverage, but ensures that | ||
test.beforeEach(t => { | ||
test.beforeEach((t) => { | ||
// initialize our app | ||
@@ -30,3 +30,3 @@ t.context.app = new Koa(); | ||
// override koa's undocumented error handler | ||
t.context.app.context.onerror = errorHandler; | ||
t.context.app.context.onerror = errorHandler(); | ||
@@ -37,11 +37,11 @@ // set up some routes | ||
// throw an error anywhere you want! | ||
_.each(statusCodes, code => { | ||
router.get(`/${code}`, ctx => ctx.throw(code)); | ||
_.each(statusCodes, (code) => { | ||
router.get(`/${code}`, (ctx) => ctx.throw(code)); | ||
}); | ||
router.get('/basic-auth', auth({ name: 'tj', pass: 'tobi' }), ctx => { | ||
router.get('/basic-auth', auth({ name: 'tj', pass: 'tobi' }), (ctx) => { | ||
ctx.body = 'Hello World'; | ||
}); | ||
router.get('/break-headers-sent', ctx => { | ||
router.get('/break-headers-sent', (ctx) => { | ||
ctx.type = 'text/html'; | ||
@@ -61,5 +61,5 @@ ctx.body = 'foo'; | ||
// check for response types | ||
_.each(['text/html', 'application/json', 'text/plain'], type => { | ||
_.each(statusCodes, code => { | ||
test.cb(`responds with ${type} for ${code} request`, t => { | ||
_.each(['text/html', 'application/json', 'text/plain'], (type) => { | ||
_.each(statusCodes, (code) => { | ||
test.cb(`responds with ${type} for ${code} request`, (t) => { | ||
request(t.context.app.listen()) | ||
@@ -75,3 +75,3 @@ .get(`/${code}`) | ||
test.cb("Won't throw after sending headers", t => { | ||
test.cb("Won't throw after sending headers", (t) => { | ||
request(t.context.app.listen()) | ||
@@ -84,3 +84,3 @@ .get('/break-headers-sent') | ||
test.cb('Throws with WWW-Authenticate header on basic auth fail', t => { | ||
test.cb('Throws with WWW-Authenticate header on basic auth fail', (t) => { | ||
request(t.context.app.listen()) | ||
@@ -87,0 +87,0 @@ .get('/basic-auth') |
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
30071
393
264
1
25
+ Addedfast-safe-stringify@^2.0.7
+ Addedfast-safe-stringify@2.1.1(transitive)
- Removeddebug@^4.1.1
- Removeddebug@4.3.7(transitive)
- Removedms@2.1.3(transitive)