├── index.js ├── .editorconfig ├── .eslintrc.yml ├── .travis.yml ├── LICENSE ├── package.json ├── lib ├── middleware.js └── defaults.js ├── README.md ├── .gitignore └── test └── test.js /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Error handler for pure-JSON Koa 2.0.0+ apps. 4 | * 5 | *``` 6 | * const Koa = require('koa'); 7 | * const error = require('koa-json-error') 8 | * 9 | * let app = new Koa(); 10 | * app.use(error()); 11 | *``` 12 | * 13 | * @module koa-json-error 14 | */ 15 | module.exports = require('./lib/middleware'); 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | # Support ES2016 features 2 | parser: babel-eslint 3 | 4 | extends: standard 5 | 6 | plugins: [ 7 | "babel" 8 | ] 9 | 10 | env: 11 | node: true 12 | mocha: true 13 | 14 | rules: 15 | arrow-parens: [2, "as-needed"] 16 | 17 | eqeqeq: 0 18 | no-return-assign: 0 # fails for arrow functions 19 | no-var: 2 20 | semi: [2, always] 21 | space-before-function-paren: [2, never] 22 | yoda: 0 23 | arrow-spacing: 2 24 | dot-location: [2, "property"] 25 | prefer-arrow-callback: 2 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | node_js: 2 | - '6' 3 | - '7' 4 | sudo: false 5 | language: node_js 6 | branches: 7 | only: 8 | - master 9 | - develop 10 | - /^v?(\d\.){2}\d$/ 11 | notifications: 12 | email: 13 | - nfantone@gmail.com 14 | cache: 15 | bundler: true 16 | directories: 17 | - node_modules 18 | before_install: npm i -g npm 19 | script: 20 | - npm run ci 21 | deploy: 22 | provider: npm 23 | email: nfantone@gmail.com 24 | api_key: 25 | secure: gI4rGcA+YExiQk7JTg/EDlb6nD+W34bp/Eit0YYwafBstmj49YeDrZDkmq0aii5hcXeiJTvkbB78R09LPTeCKGacUJRpuXs3yhGEMYoNCWlwSMI7WIUTZ1ZmDm59sAPDxyIbrnvxclPR7foeC5+yZV2ob6pcOrZ85bctyJgTCSU= 26 | on: 27 | tags: true 28 | repo: koajs/json-error 29 | node_js: '7' 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 Jonathan Ong me@jongleberry.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koa-json-error", 3 | "version": "3.1.2", 4 | "description": "Error handler for pure-JSON Koa apps", 5 | "keywords": [ 6 | "koa", 7 | "json", 8 | "error", 9 | "api", 10 | "handler", 11 | "middleware" 12 | ], 13 | "bugs": { 14 | "url": "https://github.com/koajs/json-error/issues" 15 | }, 16 | "license": "MIT", 17 | "author": { 18 | "name": "Nicolás Fantone", 19 | "email": "nfantone@gmail.com", 20 | "url": "https://github.com/nfantone", 21 | "twitter": "https://twitter.com/nfantone" 22 | }, 23 | "contributors": [ 24 | "Jonathan Ong (http://jongleberry.com)" 25 | ], 26 | "files": [ 27 | "index.js", 28 | "lib/" 29 | ], 30 | "main": "index.js", 31 | "repository": "koajs/json-error", 32 | "scripts": { 33 | "lint": "eslint *.js test/", 34 | "test": "NODE_ENV=test mocha --reporter spec", 35 | "coverage": "nyc npm run test", 36 | "report-coverage": "nyc report --reporter=lcov > coverage.lcov && codecov", 37 | "validate": "npm-run-all lint coverage", 38 | "ci": "npm-run-all validate report-coverage" 39 | }, 40 | "dependencies": { 41 | "lodash.compact": "^3.0.1", 42 | "lodash.curry": "^4.1.1" 43 | }, 44 | "devDependencies": { 45 | "babel-eslint": "^7.1.1", 46 | "codecov": "^2.0.1", 47 | "eslint": "^3.17.1", 48 | "eslint-config-standard": "^7.0.1", 49 | "eslint-plugin-babel": "^4.1.1", 50 | "eslint-plugin-promise": "^3.5.0", 51 | "eslint-plugin-standard": "^2.1.1", 52 | "koa": "^2.2.0", 53 | "mocha": "3.2.0", 54 | "npm-run-all": "^4.0.2", 55 | "nyc": "^10.1.2", 56 | "pre-commit": "^1.2.2", 57 | "supertest": "3.0.0" 58 | }, 59 | "engines": { 60 | "node": ">=6.0.0" 61 | }, 62 | "pre-commit": "test" 63 | } 64 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Main json-error middleware factory function. 4 | * @module lib/middleware 5 | */ 6 | const compact = require('lodash.compact'); 7 | const curry = require('lodash.curry'); 8 | const { DEFAULT_FORMATTING_PIPELINE } = require('./defaults'); 9 | 10 | module.exports = function createJsonErrorMiddleware(options) { 11 | if (typeof options === 'function') { 12 | // If a function is passed as an argument, treat it 13 | // like a `format` function, with no `preFormat`. 14 | options = { 15 | preFormat: null, 16 | format: options 17 | }; 18 | } 19 | 20 | // Extend options with default values 21 | const formatters = Object.assign({}, DEFAULT_FORMATTING_PIPELINE, options); 22 | 23 | const formattingPipeline = compact([ 24 | formatters.preFormat, 25 | formatters.format, 26 | formatters.postFormat 27 | ]); 28 | 29 | const applyFormat = curry((err, acum, formatter) => formatter(err, acum)); 30 | 31 | /** 32 | * Apply all ordered formatting functions to original error. 33 | * @param {Error} err The thrown error. 34 | * @return {Object} The JSON serializable formatted object. 35 | */ 36 | const formatError = err => { 37 | return formattingPipeline.reduce(applyFormat(err), {}); 38 | }; 39 | 40 | const shouldThrow404 = (status, body) => { 41 | return !status || (status === 404 && body == null); 42 | }; 43 | 44 | const shouldEmitError = (err, status) => { 45 | return !err.expose && status >= 500; 46 | }; 47 | 48 | return function jsonError(ctx, next) { 49 | return next() 50 | .then(() => { 51 | // future proof status 52 | shouldThrow404(ctx.status, ctx.body) && ctx.throw(404); 53 | }) 54 | .catch(err => { 55 | // Format and set body 56 | ctx.body = formatError(err) || {}; 57 | // Set status 58 | ctx.status = err.status || err.statusCode || 500; 59 | // Emit the error if we really care 60 | shouldEmitError(err, ctx.status) && ctx.app.emit('error', err, ctx); 61 | }); 62 | }; 63 | }; 64 | -------------------------------------------------------------------------------- /lib/defaults.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | /** 3 | * Declare and export default values for middleware options. 4 | * @module lib/defaults 5 | */ 6 | const curry = require('lodash.curry'); 7 | 8 | /** 9 | * Name of default attributes shown on errors. 10 | * This attributes can be customized by setting a 11 | * `format` function during middleware declaration. 12 | * @type {Array} 13 | */ 14 | const DEFAULT_PROPERTIES = [ 15 | 'name', 16 | 'message', 17 | 'stack', 18 | 'type' 19 | ]; 20 | 21 | /** 22 | * A pure curried reducer iteratee that builds a new object with properties 23 | * named after the elements of the collection being reduced only if that 24 | * property is also present in `err`. 25 | * @param {Object} err The original raised error being handled. 26 | * @param {Object} acum The reducer's acumulator. 27 | * @param {String} propertyName Name of the new property to add to the acumulator. 28 | * @return {Object} A new object with all of properties in `acum` as well 29 | * as `err[propertyName]`, if `propertyName` is an 30 | * enumerable property of `err`. 31 | */ 32 | const toErrorObject = curry((err, acum, propertyName) => { 33 | return err[propertyName] ? Object.assign({}, acum, { 34 | [propertyName]: err[propertyName] 35 | }) : acum; 36 | }); 37 | 38 | /** 39 | * Default middleware configuration values. 40 | * @type {Object} 41 | */ 42 | const DEFAULT_FORMATTING_PIPELINE = Object.freeze({ 43 | // Set all enumerable properties of error onto the object 44 | preFormat: err => Object.assign({}, err), 45 | // Add default custom properties to final error object 46 | format: function(err, preFormattedError) { 47 | const formattedError = DEFAULT_PROPERTIES.reduce(toErrorObject(err), {}); 48 | return Object.assign({}, preFormattedError, formattedError, { 49 | status: err.status || err.statusCode || 500 50 | }); 51 | }, 52 | // Final transformation after `options.format` (defaults to no op) 53 | postFormat: null 54 | }); 55 | 56 | // Module API 57 | module.exports = { 58 | DEFAULT_PROPERTIES, 59 | DEFAULT_FORMATTING_PIPELINE 60 | }; 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Koa JSON Error 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![Build status][travis-image]][travis-url] 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![Dependency Status][david-image]][david-url] 7 | [![License][license-image]][license-url] 8 | [![Downloads][downloads-image]][downloads-url] 9 | 10 | Error handler for pure [Koa](https://koajs.com) `>=2.0.0` JSON apps where showing the stack trace is _cool!_ 11 | 12 | ```sh 13 | npm install --save koa-json-error 14 | ``` 15 | 16 | > Versions `>=3.0.0` support Koa `^2.0.0`. For earlier versions of Koa, _please use previous releases_. 17 | 18 | ## Requirements 19 | - node `>=6.0.0` 20 | - koa `>=2.2.0` 21 | 22 | > Starting from `3.2.0`, this package supports node `>=6.0.0` to match [Koa requirements][koa-requirements]. 23 | 24 | 25 | ## API 26 | 27 | ```js 28 | 'use strict'; 29 | const koa = require('koa'); 30 | const error = require('koa-json-error') 31 | 32 | let app = new Koa(); 33 | app.use(error()) 34 | ``` 35 | 36 | If you don't really feel that showing the stack trace is _that_ cool, you can customize the way errors are shown on responses. There's a **basic** and more **advanced**, granular approach to this. 37 | 38 | ### Basic usage 39 | You can provide a _single formatter function_ as an argument on middleware initialization. It receives the original raised error and it is expected to return a formatted response. 40 | 41 | Here's a simple example: 42 | 43 | ```js 44 | 'use strict'; 45 | const koa = require('koa'); 46 | const error = require('koa-json-error') 47 | 48 | function formatError(err) { 49 | return { 50 | // Copy some attributes from 51 | // the original error 52 | status: err.status, 53 | message: err.message, 54 | 55 | // ...or add some custom ones 56 | success: false, 57 | reason: 'Unexpected' 58 | } 59 | } 60 | 61 | let app = new Koa(); 62 | app.use(error(formatError)); 63 | ``` 64 | 65 | This basic configuration is essentially the same (and serves as a shorthand for) the following: 66 | 67 | ```js 68 | 'use strict'; 69 | let app = new Koa(); 70 | app.use(error({ 71 | preFormat: null, 72 | format: formatError 73 | })); 74 | ``` 75 | 76 | See section below. 77 | 78 | ### Advanced usage 79 | You can also customize errors on responses through a series of _three formatter functions_, specified in an `options` object. They receive the raw error object and return a formatted response. This gives you fine-grained control over the final output and allows for different formats on various environments. 80 | 81 | You may pass in the `options` object as argument to the middleware. These are the available settings. 82 | 83 | #### `options.preFormat (Function)` 84 | Perform some task before calling `options.format`. Must be a function with the original `err` as its only argument. 85 | 86 | Defaults to: 87 | 88 | ```js 89 | (err) => Object.assign({}, err) 90 | ``` 91 | 92 | Which sets all enumerable properties of `err` onto the formatted object. 93 | 94 | #### `options.format (Function)` 95 | Runs inmediatly after `options.preFormat`. It receives two arguments: the original `err` and the output of `options.preFormat`. It should `return` a newly formatted error. 96 | 97 | Defaults to adding the following non-enumerable properties to the output: 98 | 99 | ```js 100 | const DEFAULT_PROPERTIES = [ 101 | 'name', 102 | 'message', 103 | 'stack', 104 | 'type' 105 | ]; 106 | ``` 107 | 108 | It also defines a `status` property like so: 109 | 110 | ```js 111 | obj.status = err.status || err.statusCode || 500; 112 | ``` 113 | 114 | #### `options.postFormat (Function)` 115 | Runs inmediatly after `options.format`. It receives two arguments: the original `err` and the output of `options.format`. It should `return` a newly formatted error. 116 | 117 | The default is a no-op (final output is defined by `options.format`). 118 | 119 | This option is useful when you want to preserve the default functionality and extend it in some way. 120 | 121 | For example, 122 | ```js 123 | 'use strict'; 124 | const _ = require('lodash'); 125 | const koa = require('koa'); 126 | const error = require('koa-json-error') 127 | 128 | let options = { 129 | // Avoid showing the stacktrace in 'production' env 130 | postFormat: (e, obj) => process.env.NODE_ENV === 'production' ? _.omit(obj, 'stack') : obj 131 | }; 132 | let app = new Koa(); 133 | app.use(error(options)); 134 | ``` 135 | 136 | > Modifying the error inside the `*format` functions will mutate the original object. Be aware of that if any other Koa middleware runs after this one. 137 | 138 | [npm-image]: https://img.shields.io/npm/v/koa-json-error.svg?style=flat-square 139 | [npm-url]: https://npmjs.org/package/koa-json-error 140 | [travis-image]: https://img.shields.io/travis/koajs/json-error/master.svg?style=flat-square 141 | [travis-url]: https://travis-ci.org/koajs/json-error 142 | [codecov-image]: https://img.shields.io/codecov/c/github/koajs/json-error/master.svg?style=flat-square 143 | [codecov-url]: https://codecov.io/github/koajs/json-error 144 | [david-image]: http://img.shields.io/david/koajs/json-error.svg?style=flat-square 145 | [david-url]: https://david-dm.org/koajs/json-error 146 | [license-image]: http://img.shields.io/npm/l/koa-json-error.svg?style=flat-square 147 | [license-url]: LICENSE 148 | [downloads-image]: http://img.shields.io/npm/dm/koa-json-error.svg?style=flat-square 149 | [downloads-url]: https://npmjs.org/package/koa-json-error 150 | [koa-requirements]: https://github.com/koajs/koa/blob/master/package.json#L61 151 | 152 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### TextMate ### 2 | *.tmproj 3 | *.tmproject 4 | tmtags 5 | 6 | 7 | ### grunt ### 8 | # Grunt usually compiles files inside this directory 9 | dist/ 10 | 11 | # Grunt usually preprocesses files such as coffeescript, compass... inside the .tmp directory 12 | .tmp/ 13 | 14 | 15 | ### Eclipse ### 16 | 17 | .metadata 18 | bin/ 19 | tmp/ 20 | *.tmp 21 | *.bak 22 | *.swp 23 | *~.nib 24 | local.properties 25 | .settings/ 26 | .loadpath 27 | .recommenders 28 | 29 | # Eclipse Core 30 | .project 31 | 32 | # External tool builders 33 | .externalToolBuilders/ 34 | 35 | # Locally stored "Eclipse launch configurations" 36 | *.launch 37 | 38 | # PyDev specific (Python IDE for Eclipse) 39 | *.pydevproject 40 | 41 | # CDT-specific (C/C++ Development Tooling) 42 | .cproject 43 | 44 | # JDT-specific (Eclipse Java Development Tools) 45 | .classpath 46 | 47 | # Java annotation processor (APT) 48 | .factorypath 49 | 50 | # PDT-specific (PHP Development Tools) 51 | .buildpath 52 | 53 | # sbteclipse plugin 54 | .target 55 | 56 | # Tern plugin 57 | .tern-project 58 | 59 | # TeXlipse plugin 60 | .texlipse 61 | 62 | # STS (Spring Tool Suite) 63 | .springBeans 64 | 65 | # Code Recommenders 66 | .recommenders/ 67 | 68 | 69 | ### Linux ### 70 | *~ 71 | 72 | # temporary files which can be created if a process still has a handle open of a deleted file 73 | .fuse_hidden* 74 | 75 | # KDE directory preferences 76 | .directory 77 | 78 | # Linux trash folder which might appear on any partition or disk 79 | .Trash-* 80 | 81 | 82 | ### SublimeText ### 83 | # cache files for sublime text 84 | *.tmlanguage.cache 85 | *.tmPreferences.cache 86 | *.stTheme.cache 87 | 88 | # workspace files are user-specific 89 | *.sublime-workspace 90 | 91 | # project files should be checked into the repository, unless a significant 92 | # proportion of contributors will probably not be using SublimeText 93 | # *.sublime-project 94 | 95 | # sftp configuration file 96 | sftp-config.json 97 | 98 | # Package control specific files 99 | Package Control.last-run 100 | Package Control.ca-list 101 | Package Control.ca-bundle 102 | Package Control.system-ca-bundle 103 | Package Control.cache/ 104 | Package Control.ca-certs/ 105 | bh_unicode_properties.cache 106 | 107 | # Sublime-github package stores a github token in this file 108 | # https://packagecontrol.io/packages/sublime-github 109 | GitHub.sublime-settings 110 | 111 | 112 | ### JetBrains ### 113 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 114 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 115 | 116 | # User-specific stuff: 117 | .idea/workspace.xml 118 | .idea/tasks.xml 119 | .idea/dictionaries 120 | .idea/vcs.xml 121 | .idea/jsLibraryMappings.xml 122 | 123 | # Sensitive or high-churn files: 124 | .idea/dataSources.ids 125 | .idea/dataSources.xml 126 | .idea/dataSources.local.xml 127 | .idea/sqlDataSources.xml 128 | .idea/dynamic.xml 129 | .idea/uiDesigner.xml 130 | 131 | # Gradle: 132 | .idea/gradle.xml 133 | .idea/libraries 134 | 135 | # Mongo Explorer plugin: 136 | .idea/mongoSettings.xml 137 | 138 | ## File-based project format: 139 | *.iws 140 | 141 | ## Plugin-specific files: 142 | 143 | # IntelliJ 144 | /out/ 145 | 146 | # mpeltonen/sbt-idea plugin 147 | .idea_modules/ 148 | 149 | # JIRA plugin 150 | atlassian-ide-plugin.xml 151 | 152 | # Crashlytics plugin (for Android Studio and IntelliJ) 153 | com_crashlytics_export_strings.xml 154 | crashlytics.properties 155 | crashlytics-build.properties 156 | fabric.properties 157 | 158 | ### JetBrains Patch ### 159 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 160 | 161 | # *.iml 162 | # modules.xml 163 | # .idea/misc.xml 164 | # *.ipr 165 | 166 | 167 | ### Node ### 168 | # Logs 169 | logs 170 | *.log 171 | npm-debug.log* 172 | 173 | # Runtime data 174 | pids 175 | *.pid 176 | *.seed 177 | *.pid.lock 178 | 179 | # Directory for instrumented libs generated by jscoverage/JSCover 180 | lib-cov 181 | 182 | # Coverage directory used by tools like istanbul 183 | coverage 184 | 185 | # nyc test coverage 186 | .nyc_output 187 | 188 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 189 | .grunt 190 | 191 | # node-waf configuration 192 | .lock-wscript 193 | 194 | # Compiled binary addons (http://nodejs.org/api/addons.html) 195 | build/Release 196 | 197 | # Dependency directories 198 | node_modules 199 | jspm_packages 200 | 201 | # Optional npm cache directory 202 | .npm 203 | 204 | # Optional REPL history 205 | .node_repl_history 206 | 207 | 208 | ### OSX ### 209 | *.DS_Store 210 | .AppleDouble 211 | .LSOverride 212 | 213 | # Icon must end with two \r 214 | Icon 215 | 216 | 217 | # Thumbnails 218 | ._* 219 | 220 | # Files that might appear in the root of a volume 221 | .DocumentRevisions-V100 222 | .fseventsd 223 | .Spotlight-V100 224 | .TemporaryItems 225 | .Trashes 226 | .VolumeIcon.icns 227 | .com.apple.timemachine.donotpresent 228 | 229 | # Directories potentially created on remote AFP share 230 | .AppleDB 231 | .AppleDesktop 232 | Network Trash Folder 233 | Temporary Items 234 | .apdisk 235 | 236 | 237 | ### Windows ### 238 | # Windows image file caches 239 | Thumbs.db 240 | ehthumbs.db 241 | 242 | # Folder config file 243 | Desktop.ini 244 | 245 | # Recycle Bin used on file shares 246 | $RECYCLE.BIN/ 247 | 248 | # Windows Installer files 249 | *.cab 250 | *.msi 251 | *.msm 252 | *.msp 253 | 254 | # Windows shortcuts 255 | *.lnk 256 | 257 | 258 | ### Bower ### 259 | bower_components 260 | .bower-cache 261 | .bower-registry 262 | .bower-tmp 263 | 264 | 265 | ### VisualStudioCode ### 266 | .vscode 267 | 268 | 269 | ### Typings ### 270 | ## Ignore downloaded typings 271 | typings 272 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const request = require('supertest'); 3 | const assert = require('assert'); 4 | const Koa = require('koa'); 5 | 6 | const error = require('..'); 7 | 8 | describe('with default options', () => { 9 | it('should show the stack', done => { 10 | let app = new Koa(); 11 | app.use(error()); 12 | app.use(() => { 13 | throw new Error(); 14 | }); 15 | request(app.listen()) 16 | .get('/') 17 | .expect(500, (err, res) => { 18 | assert.ifError(err); 19 | assert(res.body); 20 | return done(); 21 | }); 22 | }); 23 | 24 | it('should show the name', done => { 25 | let app = new Koa(); 26 | app.use(error()); 27 | app.use(() => { 28 | throw new Error(); 29 | }); 30 | request(app.listen()) 31 | .get('/') 32 | .expect(500, (err, res) => { 33 | assert.ifError(err); 34 | assert.equal('Error', res.body.name); 35 | return done(); 36 | }); 37 | }); 38 | 39 | it('should show the message', done => { 40 | let app = new Koa(); 41 | app.use(error()); 42 | app.use(() => { 43 | throw new Error('boom'); 44 | }); 45 | request(app.listen()) 46 | .get('/') 47 | .expect(500, (err, res) => { 48 | assert.ifError(err); 49 | assert.equal('boom', res.body.message); 50 | return done(); 51 | }); 52 | }); 53 | 54 | it('should show the status', done => { 55 | let app = new Koa(); 56 | app.use(error()); 57 | app.use(ctx => { 58 | ctx.throw(404); 59 | }); 60 | request(app.listen()) 61 | .get('/') 62 | .expect(404, (err, res) => { 63 | assert.ifError(err); 64 | assert.equal('Not Found', res.body.message); 65 | assert.equal(404, res.body.status); 66 | return done(); 67 | }); 68 | }); 69 | 70 | it('should check for err.statusCode', done => { 71 | let app = new Koa(); 72 | app.use(error()); 73 | app.use(() => { 74 | let err = new Error('boom'); 75 | err.statusCode = 501; 76 | throw err; 77 | }); 78 | 79 | request(app.listen()) 80 | .get('/') 81 | .expect(501, done); 82 | }); 83 | 84 | it('should emit errors', done => { 85 | let app = new Koa(); 86 | app.use(error()); 87 | app.use(() => { 88 | throw new Error('boom'); 89 | }); 90 | 91 | app.once('error', (err, ctx) => { 92 | assert.equal('boom', err.message); 93 | assert.equal(500, ctx.status); 94 | return done(); 95 | }); 96 | 97 | request(app.listen()) 98 | .get('/') 99 | .expect(500, () => {}); 100 | }); 101 | 102 | it('should throw 404 if no route matches', done => { 103 | let app = new Koa(); 104 | app.use(error()); 105 | 106 | request(app.listen()) 107 | .get('/') 108 | .expect(404, (err, res) => { 109 | assert.equal('Not Found', res.body.message); 110 | assert.equal(404, res.body.status); 111 | return done(err); 112 | }); 113 | }); 114 | 115 | it('should throw 404 if status is set explicitly but response body is left empty', done => { 116 | let app = new Koa(); 117 | app.use(error()); 118 | 119 | app.use(ctx => { 120 | ctx.status = 404; 121 | }); 122 | 123 | request(app.listen()) 124 | .get('/') 125 | .expect(404, (err, res) => { 126 | assert.equal('Not Found', res.body.message); 127 | assert.equal(404, res.body.status); 128 | return done(err); 129 | }); 130 | }); 131 | }); 132 | 133 | describe('with custom options', () => { 134 | it('should allow defining a pre, post and format functions', () => { 135 | let options = { 136 | preFormat: Function.prototype, 137 | format: Function.prototype, 138 | postFormat: Function.prototype 139 | }; 140 | 141 | let app = new Koa(); 142 | assert.doesNotThrow(() => { 143 | app.use(error(options)); 144 | }); 145 | }); 146 | 147 | it('should respect custom format while preserving status', done => { 148 | let options = { 149 | format: () => { 150 | return { 151 | status: 200, 152 | message: 'OK' 153 | }; 154 | } 155 | }; 156 | 157 | let app = new Koa(); 158 | app.use(error(options)); 159 | app.use(() => { 160 | let err = new Error('boom'); 161 | err.statusCode = 501; 162 | err.customField = 'fatal'; 163 | throw err; 164 | }); 165 | 166 | request(app.listen()) 167 | .get('/') 168 | .expect(501, (err, res) => { 169 | assert.ifError(err); 170 | assert.equal('OK', res.body.message); 171 | assert.equal(200, res.body.status); 172 | return done(); 173 | }); 174 | }); 175 | 176 | it('should allow overriding of preFormat function', done => { 177 | let options = { 178 | preFormat: () => { 179 | return { 180 | status: -100, 181 | message: 'ERROR' 182 | }; 183 | } 184 | }; 185 | 186 | let app = new Koa(); 187 | app.use(error(options)); 188 | app.use(() => { 189 | let err = new Error('boom'); 190 | err.statusCode = 422; 191 | err.customEnumerableField = 'fatal'; 192 | throw err; 193 | }); 194 | 195 | request(app.listen()) 196 | .get('/') 197 | .expect(422, (err, res) => { 198 | assert.ifError(err); 199 | assert.equal(undefined, res.body.customEnumerableField); 200 | assert.equal(422, res.body.status); 201 | return done(); 202 | }); 203 | }); 204 | }); 205 | 206 | describe('with a format function as options', () => { 207 | it('should allow passing a function as argument', done => { 208 | let app = new Koa(); 209 | assert.doesNotThrow(() => { 210 | app.use(error(Function.prototype)); 211 | return done(); 212 | }); 213 | }); 214 | 215 | it('should behave as if preFormat and postFormat were no-ops', done => { 216 | let options = err => { 217 | return { 218 | why: err.reason, 219 | status: err.status || -200 220 | }; 221 | }; 222 | 223 | let app = new Koa(); 224 | app.use(error(options)); 225 | app.use(() => { 226 | let err = new Error('boom'); 227 | err.reason = 'Not 42'; 228 | err.customEnumerableField = 'fail'; 229 | throw err; 230 | }); 231 | 232 | request(app.listen()) 233 | .get('/') 234 | .expect(500, (err, res) => { 235 | assert.ifError(err); 236 | assert.deepEqual({ 237 | status: -200, 238 | why: 'Not 42' 239 | }, res.body); 240 | assert.equal(undefined, res.body.customEnumerableField); 241 | return done(); 242 | }); 243 | }); 244 | }); 245 | --------------------------------------------------------------------------------