├── .gitignore ├── LICENSE ├── README.md ├── error-handler.js ├── examples ├── app.js └── restify.js ├── gruntfile.js ├── package.json ├── renovate.json └── test ├── runtests.js └── test-static.html /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Eric Elliott 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | express-error-handler 2 | ===================== 3 | 4 | A graceful error handler for Express applications. This also patches a DOS exploit where users can manually trigger bad request errors that shut down your app. 5 | 6 | 7 | ## Quick start: 8 | 9 | ```js 10 | var express = require('express'), 11 | errorHandler = require('../error-handler.js'), 12 | app = express(), 13 | env = process.env, 14 | port = env.myapp_port || 3000, 15 | http = require('http'), 16 | server; 17 | 18 | // Route that triggers a sample error: 19 | app.get('/error', function createError(req, 20 | res, next) { 21 | var err = new Error('Sample error'); 22 | err.status = 500; 23 | next(err); 24 | }); 25 | 26 | // Create the server object that we can pass 27 | // in to the error handler: 28 | server = http.createServer(app); 29 | 30 | // Log the error 31 | app.use(function (err, req, res, next) { 32 | console.log(err); 33 | next(err); 34 | }); 35 | 36 | // Respond to errors and conditionally shut 37 | // down the server. Pass in the server object 38 | // so the error handler can shut it down 39 | // gracefully: 40 | app.use( errorHandler({server: server}) ); 41 | 42 | server.listen(port, function () { 43 | console.log('Listening on port ' + port); 44 | }); 45 | ``` 46 | 47 | ## Configuration errorHandler(options) 48 | 49 | Here are the parameters you can pass into the `errorHandler()` middleware: 50 | 51 | * @param {object} [options] 52 | 53 | * @param {object} [options.handlers] Custom handlers for specific status codes. 54 | * @param {object} [options.views] View files to render in response to specific status codes. Specify a default with `options.views.default` 55 | * @param {object} [options.static] Static files to send in response to specific status codes. Specify a default with options.static.default. 56 | * @param {number} [options.timeout] Delay between the graceful shutdown attempt and the forced shutdown timeout. 57 | * @param {number} [options.exitStatus] Custom process exit status code. 58 | * @param {object} [options.server] The app server object for graceful shutdowns. 59 | * @param {function} [options.shutdown] An alternative shutdown function if the graceful shutdown fails. 60 | * @param {function} serializer a function to customize the JSON error object. Usage: serializer(err) return errObj 61 | * @param {function} framework Either 'express' (default) or 'restify'. 62 | * @return {function} errorHandler Express error handling middleware. 63 | 64 | ### Examples: 65 | 66 | `express-error-handler` lets you specify custom templates, static pages, or error handlers for your errors. It also does other useful error-handling things that every app should implement, like protect against 4xx error DOS attacks, and graceful shutdown on unrecoverable errors. Here's how you do what you're asking for: 67 | 68 | 69 | ```js 70 | var errorHandler = require('express-error-handler'), 71 | handler = errorHandler({ 72 | handlers: { 73 | '404': function err404() { 74 | // do some custom thing here... 75 | } 76 | } 77 | }); 78 | 79 | // After all your routes... 80 | // Pass a 404 into next(err) 81 | app.use( errorHandler.httpError(404) ); 82 | 83 | // Handle all unhandled errors: 84 | app.use( handler ); 85 | ``` 86 | 87 | Or for a static page: 88 | 89 | ```js 90 | handler = errorHandler({ 91 | static: { 92 | '404': function err404() { 93 | // do some custom thing here... 94 | } 95 | } 96 | }); 97 | ``` 98 | 99 | Or for a custom view: 100 | ```js 101 | handler = errorHandler({ 102 | views: { 103 | '404': function err404() { 104 | // do some custom thing here... 105 | } 106 | } 107 | }); 108 | ``` 109 | 110 | Or for a custom JSON object: 111 | ```js 112 | var errorHandler = require('express-error-handler'), 113 | handler = errorHandler({ 114 | serializer: function(err) { 115 | var body = { 116 | status: err.status, 117 | message: err.message 118 | }; 119 | if (createHandler.isClientError(err.status)) { 120 | ['code', 'name', 'type', 'details'].forEach(function(prop) { 121 | if (err[prop]) body[prop] = err[prop]; 122 | }); 123 | } 124 | return body; 125 | } 126 | }); 127 | ``` 128 | 129 | [More examples](https://github.com/dilvie/express-error-handler/tree/master/examples) are available in the examples folder. 130 | 131 | ## errorHandler.isClientError(status) 132 | 133 | Return true if the error status represents a client error that should not trigger a restart. 134 | 135 | * @param {number} status 136 | * @return {boolean} 137 | 138 | 139 | ### Example 140 | 141 | ```js 142 | errorHandler.isClientError(404); // returns true 143 | errorHandler.isClientError(500); // returns false 144 | ``` 145 | 146 | 147 | ## errorHandler.httpError(status, [message]) 148 | 149 | Take an error status and return a route that sends an error with the appropriate status and message to an error handler via `next(err)`. 150 | 151 | * @param {number} status 152 | * @param {string} message 153 | * @return {function} Express route handler 154 | 155 | ```js 156 | // Define supported routes 157 | app.get( '/foo', handleFoo() ); 158 | // 405 for unsupported methods. 159 | app.all( '/foo', createHandler.httpError(405) ); 160 | ``` 161 | 162 | ## Restify support 163 | 164 | Restify error handling works different from Express. To trigger restify mode, you'll need to pass the `framework` parameter when you create the errorHandler: 165 | 166 | ```js 167 | var handleError = errorHandler({ 168 | server: server 169 | framework: 'restify' 170 | }); 171 | ``` 172 | 173 | In restify, `next(err)` is synonymous with `res.send(status, error)`. This means that you should *only use `next(err)` to report errors to users*, and not as a way to aggregate errors to a common error handler. Instead, you can invoke an error handler directly to aggregate your error handling in one place. 174 | 175 | There is no error handling middleware. Instead, use `server.on('uncaughtException', handleError)` 176 | 177 | See the examples in `./examples/restify.js` 178 | 179 | 180 | ## Credit and Thanks 181 | 182 | Written by [Eric Elliott](http://ericelliottjs.com/) for the book, ["Programming JavaScript Applications"](http://pjabook.com) (O'Reilly) 183 | 184 | * [Nam Nguyen](https://github.com/gdbtek) for bringing the Express DOS exploit to my attention. 185 | * [Samuel Reed](https://github.com/strml) for helpful suggestions. 186 | -------------------------------------------------------------------------------- /error-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * express-error-handler 3 | * 4 | * A graceful error handler for Express 5 | * applications. 6 | * 7 | * Copyright (C) 2013 Eric Elliott 8 | * 9 | * Written for 10 | * "Programming JavaScript Applications" 11 | * (O'Reilly) 12 | * 13 | * MIT License 14 | **/ 15 | 16 | 'use strict'; 17 | 18 | var mixIn = require('mout/object/mixIn'), 19 | createObject = require('mout/lang/createObject'), 20 | path = require('path'), 21 | fs = require('fs'), 22 | statusCodes = require('http').STATUS_CODES, 23 | 24 | /** 25 | * Return true if the error status represents 26 | * a client error that should not trigger a 27 | * restart. 28 | * 29 | * @param {number} status 30 | * @return {boolean} 31 | */ 32 | isClientError = function isClientError(status) { 33 | return (status >= 400 && status <= 499); 34 | }, 35 | 36 | /** 37 | * Attempt a graceful shutdown, and then time 38 | * out if the connections fail to drain in time. 39 | * 40 | * @param {object} o options 41 | * @param {object} o.server server object 42 | * @param {object} o.timeout timeout in ms 43 | * @param {function} exit - force kill function 44 | */ 45 | close = function close(o, exit) { 46 | // We need to kill the server process so 47 | // the app can repair itself. Your process 48 | // should be monitored in production and 49 | // restarted when it shuts down. 50 | // 51 | // That can be accomplished with modules 52 | // like forever, forky, etc... 53 | // 54 | // First, try a graceful shutdown: 55 | if (o.server && typeof o.server.close === 56 | 'function') { 57 | try { 58 | o.server.close(function () { 59 | process.exit(o.exitStatus); 60 | }); 61 | } 62 | finally { 63 | process.exit(o.exitStatus); 64 | } 65 | } 66 | 67 | // Just in case the server.close() callback 68 | // never fires, this will wait for a timeout 69 | // and then terminate. Users can override 70 | // this function by passing options.shutdown: 71 | exit(o); 72 | }, 73 | 74 | /** 75 | * Take an error status and return a route that 76 | * sends an error with the appropriate status 77 | * and message to an error handler via 78 | * `next(err)`. 79 | * 80 | * @param {number} status 81 | * @param {string} message 82 | * @return {function} Express route handler 83 | */ 84 | httpError = function httpError (status, message) { 85 | var err = new Error(); 86 | err.status = status; 87 | err.message = message || 88 | statusCodes[status] || 89 | 'Internal server error'; 90 | 91 | return function httpErr(req, res, next) { 92 | next(err); 93 | }; 94 | }, 95 | 96 | sendFile = function sendFile (staticFile, res) { 97 | var filePath = path.resolve(staticFile), 98 | stream = fs.createReadStream(filePath); 99 | stream.pipe(res); 100 | }, 101 | 102 | send = function send(statusCode, err, res, o) { 103 | var body = { 104 | status: statusCode, 105 | message: err.message || 106 | statusCodes[statusCode] 107 | }; 108 | 109 | body = (o.serializer) ? 110 | o.serializer(createObject(err, body)) : 111 | body; 112 | 113 | res.status(statusCode); 114 | res.send(body); 115 | }, 116 | 117 | defaults = { 118 | handlers: {}, 119 | views: {}, 120 | static: {}, 121 | timeout: 3 * 1000, 122 | exitStatus: 1, 123 | server: undefined, 124 | shutdown: undefined, 125 | serializer: undefined, 126 | framework: 'express' 127 | }, 128 | createHandler; 129 | 130 | /** 131 | * A graceful error handler for Express 132 | * applications. 133 | * 134 | * @param {object} [options] 135 | * 136 | * @param {object} [options.handlers] Custom 137 | * handlers for specific status codes. 138 | * 139 | * @param {object} [options.views] View files to 140 | * render in response to specific status 141 | * codes. Specify a default with 142 | * options.views.default. 143 | * 144 | * @param {object} [options.static] Static files 145 | * to send in response to specific status 146 | * codes. Specify a default with 147 | * options.static.default. 148 | * 149 | * @param {number} [options.timeout] Delay 150 | * between the graceful shutdown 151 | * attempt and the forced shutdown 152 | * timeout. 153 | * 154 | * @param {number} [options.exitStatus] Custom 155 | * process exit status code. 156 | * 157 | * @param {object} [options.server] The app server 158 | * object for graceful shutdowns. 159 | * 160 | * @param {function} [options.shutdown] An 161 | * alternative shutdown function if the 162 | * graceful shutdown fails. 163 | * 164 | * @param {function} serializer A function to 165 | * customize the JSON error object. 166 | * Usage: serializer(err) return errObj 167 | * 168 | * @param {function} framework Either 'express' 169 | * (default) or 'restify'. 170 | * 171 | * @return {function} errorHandler Express error 172 | * handling middleware. 173 | */ 174 | createHandler = function createHandler(options) { 175 | 176 | var o = mixIn({}, defaults, options), 177 | 178 | /** 179 | * In case of an error, wait for a timer to 180 | * elapse, and then terminate. 181 | * @param {object} options 182 | * @param {number} o.exitStatus 183 | * @param {number} o.timeout 184 | */ 185 | exit = o.shutdown || function exit(o){ 186 | 187 | // Give the app time for graceful shutdown. 188 | setTimeout(function () { 189 | process.exit(o.exitStatus); 190 | }, o.timeout); 191 | 192 | }, 193 | express = o.framework === 'express', 194 | restify = o.framework === 'restify', 195 | errorHandler; 196 | 197 | /** 198 | * Express error handler to handle any 199 | * uncaught express errors. For error logging, 200 | * see bunyan-request-logger. 201 | * 202 | * @param {object} err 203 | * @param {object} req 204 | * @param {object} res 205 | * @param {function} next 206 | */ 207 | errorHandler = function errorHandler(err, req, 208 | res, next) { 209 | 210 | var defaultView = o.views['default'], 211 | defaultStatic = o.static['default'], 212 | status = err && err.status || 213 | res && res.statusCode, 214 | handler = o.handlers[status], 215 | view = o.views[status], 216 | staticFile = o.static[status], 217 | 218 | renderDefault = function 219 | renderDefault(statusCode) { 220 | 221 | res.statusCode = statusCode; 222 | 223 | if (defaultView) { 224 | return res.render(defaultView, err); 225 | } 226 | 227 | if (defaultStatic) { 228 | return sendFile(defaultStatic, res); 229 | } 230 | 231 | if (restify) { 232 | send(statusCode, err, res, o); 233 | } 234 | 235 | if (express) { 236 | return res.format({ 237 | json: function () { 238 | send(statusCode, err, res, { 239 | serializer: o.serializer || function (o) { 240 | return o; 241 | } 242 | }); 243 | }, 244 | text: function () { 245 | send(statusCode, err, res, { 246 | serializer: function (o) { 247 | return o.message; 248 | } 249 | }); 250 | }, 251 | html: function () { 252 | send(statusCode, err, res, { 253 | serializer: function (o) { 254 | return o.message; 255 | } 256 | }); 257 | } 258 | }); 259 | } 260 | }, 261 | 262 | resumeOrClose = function 263 | resumeOrClose(status) { 264 | if (!isClientError(status)) { 265 | return close(o, exit); 266 | } 267 | }; 268 | 269 | if (!res) { 270 | return resumeOrClose(status); 271 | } 272 | 273 | // If there's a custom handler defined, 274 | // use it and return. 275 | if (typeof handler === 'function') { 276 | handler(err, req, res, next); 277 | return resumeOrClose(status); 278 | } 279 | 280 | // If there's a custom view defined, 281 | // render it. 282 | if (view) { 283 | res.render(view, err); 284 | return resumeOrClose(status); 285 | } 286 | 287 | // If there's a custom static file defined, 288 | // render it. 289 | if (staticFile) { 290 | sendFile(staticFile, res); 291 | return resumeOrClose(status); 292 | } 293 | 294 | // If the error is user generated, send 295 | // a helpful error message, and don't shut 296 | // down. 297 | // 298 | // If we shutdown on user errors, 299 | // attackers can send malformed requests 300 | // for the purpose of creating a Denial 301 | // Of Service (DOS) attack. 302 | if (isClientError(status)) { 303 | return renderDefault(status); 304 | } 305 | 306 | // For all other errors, deliver a 500 307 | // error and shut down. 308 | renderDefault(500); 309 | 310 | close(o, exit); 311 | }; 312 | 313 | if (express) { 314 | return errorHandler; 315 | } 316 | 317 | if (restify) { 318 | return function (req, res, route, err) { 319 | return errorHandler(err, req, res); 320 | }; 321 | } 322 | }; 323 | 324 | createHandler.isClientError = isClientError; 325 | createHandler.clientError = function () { 326 | var args = [].slice.call(arguments); 327 | 328 | console.log('WARNING: .clientError() is ' + 329 | 'deprecated. Use isClientError() instead.'); 330 | 331 | return this.isClientError.apply(this, args); 332 | }; 333 | 334 | // HTTP error generating route. 335 | createHandler.httpError = httpError; 336 | 337 | module.exports = createHandler; 338 | -------------------------------------------------------------------------------- /examples/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var express = require('express'), 4 | logger = require('bunyan-request-logger'), 5 | noCache = require('connect-cache-control'), 6 | errorHandler = require('../error-handler.js'), 7 | log = logger(), 8 | app = express(), 9 | env = process.env, 10 | port = env.myapp_port || 3000, 11 | http = require('http'), 12 | server; 13 | 14 | app.use( log.requestLogger() ); 15 | 16 | // Route to handle client side log messages. 17 | // 18 | // Counter to intuition, client side logging 19 | // works best with GET requests. 20 | // 21 | // AJAX POST sends headers and body in two steps, 22 | // which slows it down. 23 | // 24 | // This route prepends the cache-control 25 | // middleware so that the browser always logs 26 | // to the server instead of fetching a useless 27 | // OK message from its cache. 28 | app.get('/log', noCache, 29 | function logHandler(req, res) { 30 | 31 | // Since all requests are automatically logged, 32 | // all you need to do is send the response: 33 | res.status(200).end(); 34 | }); 35 | 36 | // Route that triggers a sample error: 37 | app.get('/error', function createError(req, 38 | res, next) { 39 | var err = new Error('Sample error'); 40 | err.status = 500; 41 | next(err); 42 | }); 43 | 44 | 45 | // Route that triggers a sample error: 46 | app.all('/*', errorHandler.httpError(404)); 47 | 48 | // Log request errors: 49 | app.use( log.errorLogger() ); 50 | 51 | // Create the server object that we can pass 52 | // in to the error handler: 53 | server = http.createServer(app); 54 | 55 | // Respond to errors and conditionally shut 56 | // down the server. Pass in the server object 57 | // so the error handler can shut it down 58 | // gracefully: 59 | app.use( errorHandler({server: server}) ); 60 | 61 | server.listen(port, function () { 62 | log.info('Listening on port ' + port); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/restify.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var restify = require('restify'), 4 | server = restify.createServer(), 5 | errorHandler = require('../error-handler.js'), 6 | 7 | handleError = errorHandler({ 8 | server: server, 9 | 10 | // Put the errorHandler in restify error 11 | // handling mode. 12 | framework: 'restify' 13 | }), 14 | 15 | // Since restify error handlers take error 16 | // last, and process 'uncaughtError' sends 17 | // error first, you'll need another one for 18 | // process exceptions. Don't pass the 19 | // framework: 'restify' setting this time: 20 | handleProcessError = errorHandler({ 21 | server: server 22 | }), 23 | 24 | middlewareError = 25 | function middlewareError() { 26 | throw new Error('Random middleware error.'); 27 | }; 28 | 29 | // Don't do this: 30 | server.get('/brokenError', function (req, res, next) { 31 | var err = new Error('Random, possibly ' + 32 | 'unrecoverable error. Server is now running ' + 33 | 'in undefined state!'); 34 | 35 | err.status = 500; 36 | 37 | // Warning! This error will go directly to the 38 | // user, and you won't have any opportunity to 39 | // examine it and possibly shut down the system. 40 | next(err); 41 | }); 42 | 43 | // Instead, do this: 44 | server.get('/err', function (req, res) { 45 | var err = new Error('Random, possibly ' + 46 | 'unrecoverable error. Server is now running ' + 47 | 'in undefined state!'); 48 | 49 | err.status = 500; 50 | 51 | // You should invoke handleError directly in your 52 | // route instead of sending it to next() or 53 | // throwing. Note that the restify error handler 54 | // has the call signature: req, res, route, err. 55 | // Normally, route is an object. 56 | handleError(req, res, '/err', err); 57 | }); 58 | 59 | 60 | // This route demonstrates what happens when your 61 | // routes throw. Never do this on 62 | // purpose. Instead, invoke the 63 | // error handler as described above. 64 | server.get('/thrower', function () { 65 | var err = new Error('Random, possibly ' + 66 | 'unrecoverable error. Server is now running ' + 67 | 'in undefined state!'); 68 | 69 | throw err; 70 | }); 71 | 72 | // This demonstrates what happens when your 73 | // middleware throws. As with routes, never do 74 | // this on purpose. Instead, invoke the 75 | // error handler as described above. 76 | server.use(middlewareError); 77 | 78 | server.get('/middleware', function () { 79 | // Placeholder to invoke middlewareError. 80 | }); 81 | 82 | // This is called when an error is accidentally 83 | // thrown. Under the hood, it uses Node's domain 84 | // module for error handling, which limits the 85 | // scope of the domain to the request / response 86 | // cycle. Restify hooks up the domain for you, 87 | // so you don't have to worry about binding 88 | // request and response to the domain. 89 | server.on('uncaughtException', handleError); 90 | 91 | // We should still listen for process errors 92 | // and shut down if we catch them. This handler 93 | // will try to let server connections drain, first, 94 | // and invoke your custom handlers if you have 95 | // any defined. 96 | process.on('uncaughtException', handleProcessError); 97 | 98 | server.listen(3000, function () { 99 | console.log('Listening on port 3000'); 100 | }); 101 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /*global module*/ 3 | module.exports = function(grunt) { 4 | grunt.initConfig({ 5 | jshint: { 6 | all: ['./gruntfile.js', './test/*.js', 7 | './examples/*.js', './error-handler.js'], 8 | options: { 9 | curly: true, 10 | eqeqeq: true, 11 | immed: true, 12 | latedef: true, 13 | newcap: true, 14 | nonew: true, 15 | noarg: true, 16 | sub: true, 17 | undef: true, 18 | unused: true, 19 | eqnull: true, 20 | node: true, 21 | strict: true, 22 | boss: false 23 | } 24 | } 25 | 26 | }); 27 | 28 | grunt.loadNpmTasks('grunt-contrib-jshint'); 29 | 30 | grunt.registerTask('hint', ['jshint']); 31 | grunt.registerTask('default', ['jshint']); 32 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-error-handler", 3 | "version": "1.1.0", 4 | "description": "A graceful error handler for Express applications.", 5 | "main": "error-handler.js", 6 | "scripts": { 7 | "lint": "grunt hint", 8 | "pretest": "npm run -s lint", 9 | "test": "node ./test/runtests.js", 10 | "watch": "watch 'clear && npm run -s test' .", 11 | "start-example": "node examples/app.js | bunyan", 12 | "latest": "updtr" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git@github.com:ericelliott/express-error-handler.git" 17 | }, 18 | "keywords": [ 19 | "error", 20 | "errors", 21 | "handling", 22 | "handler", 23 | "express", 24 | "connect" 25 | ], 26 | "author": "Eric Elliott", 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/ericelliott/express-error-handler/issues" 30 | }, 31 | "dependencies": { 32 | "mout": "1.2.2" 33 | }, 34 | "devDependencies": { 35 | "bunyan-request-logger": "2.1.0", 36 | "connect-cache-control": "1.0.0", 37 | "express": "4.17.1", 38 | "grunt": "0.4.5", 39 | "grunt-contrib-jshint": "1.1.0", 40 | "restify": "7.7.0", 41 | "supertest": "4.0.2", 42 | "tape": "4.13.2", 43 | "through": "2.3.8", 44 | "updtr": "3.1.0", 45 | "watch": "1.0.2" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ], 5 | "automerge": true, 6 | "automergeType": "branch", 7 | "major": { 8 | "automerge": false 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /test/runtests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var test = require('tape'), 4 | createHandler = require('../error-handler.js'), 5 | through = require('through'), 6 | mixIn = require('mout/object/mixIn'), 7 | 8 | format = function format (types) { 9 | return types['text'](); 10 | }, 11 | 12 | testError = new Error('Test error'), 13 | testReq = function () { return {}; }, 14 | testRes = function (config) { 15 | return mixIn({ 16 | send: function send() {}, 17 | end: function end() {}, 18 | format: format, 19 | status: function (statusCode) { 20 | this.statusCode = statusCode; 21 | return this; 22 | } 23 | }, config); 24 | }, 25 | testNext = function () {}; 26 | 27 | 28 | test('Custom shutdown', function (t) { 29 | var shutdown = function shutdown() { 30 | t.pass('Should call custom shutdown'); 31 | t.end(); 32 | }, 33 | 34 | handler = createHandler({shutdown: shutdown}); 35 | 36 | handler( testError, testReq(), testRes(), 37 | testNext ); 38 | }); 39 | 40 | test('Custom exit status', function (t) { 41 | var status = 11, 42 | shutdown = function shutdown(options) { 43 | t.strictEqual(options.exitStatus, status, 44 | 'Should use custom exit status code'); 45 | 46 | t.end(); 47 | }, 48 | 49 | handler = createHandler({ 50 | shutdown: shutdown, 51 | exitStatus: status 52 | }); 53 | 54 | handler( testError, testReq(), testRes(), 55 | testNext ); 56 | }); 57 | 58 | test('Custom handler', function (t) { 59 | var shutdown = function shutdown() {}, 60 | e = new Error(), 61 | 62 | handler = createHandler({ 63 | shutdown: shutdown, 64 | handlers: { 65 | '404': function err404() { 66 | t.pass('Should use custom handlers ' + 67 | 'for status codes'); 68 | t.end(); 69 | } 70 | } 71 | }); 72 | 73 | e.status = 404; 74 | 75 | handler( e, testReq(), testRes(), 76 | testNext ); 77 | }); 78 | 79 | test('Missing error status', function (t) { 80 | var shutdown = function shutdown() {}, 81 | e = new Error(), 82 | 83 | handler = createHandler({ 84 | shutdown: shutdown, 85 | handlers: { 86 | '404': function err404() { 87 | t.pass('Should get status from ' + 88 | 'res.statusCode'); 89 | t.end(); 90 | } 91 | } 92 | }), 93 | res = testRes(); 94 | 95 | res.statusCode = 404; 96 | 97 | handler( e, testReq(), res, 98 | testNext ); 99 | }); 100 | 101 | test('Custom views', function (t) { 102 | var shutdown = function shutdown() {}, 103 | e = new Error(), 104 | 105 | handler = createHandler({ 106 | shutdown: shutdown, 107 | views: { 108 | '404': '404 view' 109 | } 110 | }), 111 | res = testRes({ 112 | render: function render() { 113 | t.pass('Render should be called for ' + 114 | 'custom views.'); 115 | t.end(); 116 | } 117 | }); 118 | 119 | e.status = 404; 120 | 121 | handler(e, testReq(), res, testNext); 122 | }); 123 | 124 | test('Error with status default behavior', 125 | function (t) { 126 | 127 | var shutdown = function shutdown() { 128 | t.fail('shutdown should not be called.'); 129 | }, 130 | e = new Error(), 131 | status = 404, 132 | handler = createHandler({ 133 | shutdown: shutdown 134 | }), 135 | res = testRes({ 136 | send: function send() { 137 | t.equal(res.statusCode, status, 138 | 'res.statusCode should be set to err.status'); 139 | t.end(); 140 | }, 141 | format: format 142 | }); 143 | 144 | e.status = status; 145 | 146 | handler(e, testReq(), res, testNext); 147 | }); 148 | 149 | test('Default error status for non-user error', 150 | function (t) { 151 | 152 | var shutdown = function shutdown() {}, 153 | e = new Error(), 154 | handler = createHandler({ 155 | shutdown: shutdown 156 | }), 157 | status = 511, 158 | defaultStatus = 500, 159 | res = testRes({ 160 | send: function send() { 161 | t.equal(res.statusCode, defaultStatus, 162 | 'res.statusCode should be set to default status'); 163 | t.end(); 164 | }, 165 | format: format 166 | }); 167 | 168 | e.status = status; 169 | 170 | handler(e, testReq(), res, testNext); 171 | }); 172 | 173 | test('Custom timeout', 174 | function (t) { 175 | 176 | var shutdown = function shutdown(options) { 177 | t.equal(options.timeout, 4 * 1000, 178 | 'Custom timeout should be respected.'); 179 | t.end(); 180 | }, 181 | handler = createHandler({ 182 | timeout: 4 * 1000, 183 | shutdown: shutdown 184 | }); 185 | 186 | handler(testError, testReq(), testRes(), testNext); 187 | }); 188 | 189 | test('Static file', function (t) { 190 | var 191 | buff = [], 192 | sample = 'foo', 193 | output, 194 | 195 | shutdown = function shutdown() {}, 196 | 197 | e = (function () { 198 | var err = new Error(); 199 | err.status = 505; 200 | return err; 201 | }()), 202 | 203 | handler = createHandler({ 204 | static: { 205 | '505': './test/test-static.html' 206 | }, 207 | shutdown: shutdown 208 | }), 209 | 210 | res = through(function (data) { 211 | buff.push(data); 212 | }, function () { 213 | output = Buffer.concat(buff).toString('utf8') 214 | .trim(); 215 | 216 | t.strictEqual(output, sample, 217 | 'Should send static file.'); 218 | 219 | t.end(); 220 | }); 221 | 222 | handler(e, testReq(), res, testNext); 223 | }); 224 | 225 | test('.isClientError()', function (t) { 226 | var 227 | serverPass = [399, 500].every(function (err) { 228 | return !createHandler.isClientError(err); 229 | }), 230 | clientPass = [400, 401, 499].every(function(err) { 231 | return createHandler.isClientError(err); 232 | }); 233 | 234 | t.ok(serverPass, 235 | 'Non client errors should be correctly identified.'); 236 | t.ok(clientPass, 237 | 'Client errors should be correctly identified.'); 238 | 239 | t.end(); 240 | }); 241 | 242 | test('Default static file', function (t) { 243 | var shutdown = function shutdown() {}, 244 | 245 | buff = [], 246 | sample = 'foo', 247 | output, 248 | 249 | e = (function () { 250 | var err = new Error(); 251 | err.status = 505; 252 | return err; 253 | }()), 254 | 255 | handler = createHandler({ 256 | static: { 257 | 'default': './test/test-static.html' 258 | }, 259 | shutdown: shutdown 260 | }), 261 | 262 | res = through(function (data) { 263 | buff.push(data); 264 | }, function () { 265 | output = Buffer.concat(buff).toString('utf8') 266 | .trim(); 267 | 268 | t.strictEqual(output, sample, 269 | 'Should send static file.'); 270 | 271 | t.end(); 272 | }); 273 | 274 | handler(e, testReq(), res, testNext); 275 | }); 276 | 277 | test('.restify()', function (t) { 278 | var route, 279 | shutdown = function shutdown() { 280 | t.pass('Should return restify handler.'); 281 | t.end(); 282 | }, 283 | 284 | handler = createHandler({ 285 | shutdown: shutdown, 286 | framework: 'restify' 287 | }); 288 | 289 | // Restify uses a different signature: 290 | handler(testReq(), testRes(), route, testError); 291 | }); 292 | 293 | test('.create() http error handler', function (t) { 294 | var next = function (err) { 295 | t.equal(err.status, 405, 296 | 'Status message should be set on error.'); 297 | t.equal(err.message, 'Method Not Allowed', 298 | 'Should set message correctly.'); 299 | t.end(); 300 | }, 301 | handler = createHandler.httpError(405); 302 | 303 | handler(null, null, next); 304 | }); 305 | 306 | test('JSON error format', 307 | function (t) { 308 | 309 | var shutdown = function shutdown() {}, 310 | e = new Error(), 311 | handler = createHandler({ 312 | shutdown: shutdown 313 | }), 314 | res = testRes({ 315 | send: function send(obj) { 316 | t.equal(obj.status, 500, 317 | 'res.send() should be called ' + 318 | 'with error status on response body.'); 319 | t.equal(obj.message, 'Internal Server Error', 320 | 'res.send() should be called ' + 321 | 'with error message on response body.'); 322 | t.end(); 323 | }, 324 | format: function format (types) { 325 | return types['json'](); 326 | } 327 | }); 328 | 329 | e.status = 500; 330 | 331 | handler(e, testReq(), res, testNext); 332 | }); 333 | 334 | test('JSON with custom error message', 335 | function (t) { 336 | 337 | var shutdown = function shutdown() {}, 338 | e = new Error(), 339 | handler = createHandler({ 340 | shutdown: shutdown 341 | }), 342 | res = testRes({ 343 | send: function send(obj) { 344 | t.equal(obj.message, 'half baked', 345 | 'res.send() should be called ' + 346 | 'with custom error message.'); 347 | t.end(); 348 | }, 349 | format: function format (types) { 350 | return types['json'](); 351 | } 352 | }); 353 | 354 | e.status = 420; 355 | e.message = 'half baked'; 356 | 357 | handler(e, testReq(), res, testNext); 358 | }); 359 | 360 | 361 | test('JSON with serializer', 362 | function (t) { 363 | 364 | var shutdown = function shutdown() {}, 365 | e = new Error(), 366 | handler = createHandler({ 367 | shutdown: shutdown, 368 | serializer: function (err) { 369 | return { 370 | status: err.status, 371 | message: err.message, 372 | links: [ 373 | {self: '/foo'} 374 | ] 375 | }; 376 | } 377 | }), 378 | res = testRes({ 379 | send: function send(obj) { 380 | t.equal(obj.links[0].self, '/foo', 381 | 'Should be able to define a custom ' + 382 | 'serializer for error responses.'); 383 | t.end(); 384 | }, 385 | format: function format (types) { 386 | return types['json'](); 387 | } 388 | }); 389 | 390 | e.status = 500; 391 | 392 | handler(e, testReq(), res, testNext); 393 | }); 394 | 395 | test('JSON with serializer with access to error object', 396 | function (t) { 397 | 398 | var shutdown = function shutdown() {}, 399 | e = (function () { 400 | var err = new Error(); 401 | err.status = 400; 402 | ['code', 'name', 'type', 'details'].forEach(function(prop) { err[prop] = 'foo'; }); 403 | return err; 404 | }()), 405 | handler = createHandler({ 406 | shutdown: shutdown, 407 | serializer: function(err) { return err; } 408 | }), 409 | res = testRes({ 410 | send: function send(obj) { 411 | var propertiesPass = ['code', 'name', 'type', 'details'].every(function(prop) { 412 | return obj[prop] === 'foo'; 413 | }); 414 | t.ok(propertiesPass, 415 | 'Should be able to write custom serializer with access to properties of client errors.'); 416 | t.end(); 417 | }, 418 | format: function format (types) { 419 | return types['json'](); 420 | } 421 | }); 422 | 423 | handler(e, testReq(), res, testNext); 424 | }); 425 | -------------------------------------------------------------------------------- /test/test-static.html: -------------------------------------------------------------------------------- 1 | foo --------------------------------------------------------------------------------