├── CONTRIBUTING.md ├── .travis.yml ├── test ├── good_options.js ├── api_server.js ├── message_server_example.js ├── uncaught_exception_server.js ├── redirect_server_example.js ├── jwt_server_example.js └── index.test.js ├── .github └── workflows │ ├── npm-test.yml │ └── npm-publish.yml ├── index.d.ts ├── example ├── server_example.js ├── error_template.html └── server.js ├── .gitignore ├── LICENSE ├── package.json ├── lib └── index.js └── README.md /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Please view our [contribution guidelines](https://github.com/dwyl/contributing) 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | - "node" 5 | sudo: false 6 | after_success: 7 | - bash <(curl -s https://codecov.io/bash) -------------------------------------------------------------------------------- /test/good_options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ops: { 3 | interval: 30000 // reporting interval (30 seconds) 4 | }, 5 | reporters: { 6 | myConsoleReporter: [{ 7 | module: 'good-squeeze', // https://github.com/hapijs/good-squeeze 8 | name: 'Squeeze', 9 | args: [{ log: '*', error: '*', response: '*', request: '*', ops: '*' }] 10 | }, { 11 | module: 'good-console' 12 | }, 'stdout'] 13 | } 14 | } -------------------------------------------------------------------------------- /.github/workflows/npm-test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [14.x, 15.x, 16.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install && npm test -------------------------------------------------------------------------------- /test/api_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // this mini server is for: https://github.com/dwyl/hapi-error/issues/49 4 | var Hapi = require('@hapi/hapi'); 5 | var Hoek = require('@hapi/hoek'); 6 | 7 | var server = new Hapi.Server(); 8 | 9 | // no server.routes required as we are *trying* to test for an error! 10 | 11 | module.exports = async () => { 12 | try { 13 | await server.register(require('../lib/index.js')); 14 | Hoek.assert('no errors registering plugins'); 15 | return server; 16 | } catch (e) { 17 | throw e; 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | 2 | /// 3 | 4 | import { 5 | Plugin, 6 | Request, 7 | ResponseObject, 8 | } from '@hapi/hapi'; 9 | 10 | declare namespace hapiError{ 11 | interface Options { 12 | statusCodes?: {}; 13 | } 14 | type handleError = (error: Error, errorMessage: string) => boolean; 15 | } 16 | 17 | 18 | declare const hapiError: Plugin; 19 | 20 | export = hapiError; 21 | 22 | declare module 'hapi' { 23 | interface Request { 24 | handleError: hapiError.handleError; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/server_example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var server = require('./server.js'); 4 | var Hoek = require('@hapi/hoek'); 5 | 6 | module.exports = async () => { 7 | try { 8 | await server.register(require('@hapi/vision')); 9 | await server.register(require('../lib/index.js')); 10 | server.views({ 11 | engines: { 12 | html: require('handlebars') 13 | }, 14 | path: require('path').resolve(__dirname, './') 15 | }); 16 | await server.start(); 17 | server.log('info', 'Visit: ' + server.info.uri); 18 | Hoek.assert('no errors starting server'); 19 | return server; 20 | } catch(e) { 21 | throw e; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | coverage.lcov 17 | 18 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 19 | .grunt 20 | 21 | # node-waf configuration 22 | .lock-wscript 23 | 24 | # Compiled binary addons (https://nodejs.org/api/addons.html) 25 | build/Release 26 | 27 | # Dependency directory 28 | node_modules 29 | 30 | # Optional npm cache directory 31 | .npm 32 | 33 | # Optional REPL history 34 | .node_repl_history 35 | .DS_Store 36 | 37 | // currently required for Goodparts 38 | .eslintrc.js 39 | 40 | .nyc_output 41 | package-lock.json 42 | yarn.lock -------------------------------------------------------------------------------- /test/message_server_example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('decache')('../example/server.js'); 4 | // ensure we have a fresh module 5 | var server = require('../example/server.js'); 6 | var Hoek = require('@hapi/hoek'); 7 | var Path = require('path'); 8 | var Handlebars = require('handlebars'); 9 | 10 | var config = { 11 | 404: { // if the statusCode is 401 redirect to /login page/endpoint 12 | message: function () { 13 | return 'robots in disguise'; 14 | } 15 | }, 16 | 500: { 17 | message: function (msg, request) { 18 | return 'User agent: ' + request.headers['user-agent']; 19 | } 20 | } 21 | }; 22 | 23 | module.exports = async () => { 24 | try { 25 | await server.register({ plugin: require('../lib/index.js'), options: config }); 26 | Hoek.assert('no errors registering plugins'); 27 | return server; 28 | } catch (e){ 29 | throw e; 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /example/error_template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{errorTitle}} 6 | 7 | 29 | 30 | 31 |
32 |

◕ ︵ ◕

33 |

{{errorMessage}}

34 |

{{statusCode}}

35 |

{{email}}

36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: npm-publish 2 | on: 3 | push: 4 | branches: 5 | - master # Change this to your default branch 6 | jobs: 7 | npm-publish: 8 | name: npm-publish 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | - name: Publish if version has been updated 14 | uses: pascalgn/npm-publish-action@1.3.9 15 | with: # All of theses inputs are optional 16 | tag_name: "v%s" 17 | tag_message: "v%s" 18 | create_tag: "true" 19 | commit_pattern: "^Release (\\S+)" 20 | workspace: "." 21 | publish_command: "yarn" 22 | publish_args: "--non-interactive" 23 | env: # More info about the environment variables in the README 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Leave this as is, it's automatically generated 25 | NPM_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} # You need to set this in your repo settings 26 | -------------------------------------------------------------------------------- /test/uncaught_exception_server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hapi = require('@hapi/hapi'); 4 | var Hoek = require('@hapi/hoek'); 5 | 6 | var server = new Hapi.Server(); 7 | 8 | module.exports = async () => { 9 | try { 10 | await server.register({ 11 | plugin: require('@hapi/good'), 12 | options: require('./good_options'), 13 | }); 14 | await server.register(require('../lib/index.js')); 15 | await server.register(require('@hapi/vision')); 16 | await server.views({ 17 | engines: { 18 | html: require('handlebars') 19 | }, 20 | path: require('path').resolve(__dirname, '../example') 21 | }); 22 | server.route([ 23 | { 24 | method: 'GET', 25 | path: '/throw', 26 | handler: function (request, reply) { 27 | throw new Error('AAAAA!'); 28 | } 29 | } 30 | ]); 31 | Hoek.assert('no errors registering plugins'); 32 | return server; 33 | } catch (e) { 34 | throw e; 35 | } 36 | }; 37 | ; 38 | -------------------------------------------------------------------------------- /test/redirect_server_example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('decache')('../example/server.js'); // ensure we have a fresh module 4 | var server = require('../example/server.js'); 5 | var Hoek = require('@hapi/hoek'); 6 | 7 | var config = { 8 | "401": { // if the statusCode is 401 redirect to /login page/endpoint 9 | "redirect": "/login" 10 | }, 11 | "403": { 12 | "redirect": function (request) { 13 | var redirectString = request.url.pathname + request.url.search; 14 | return "/login?redirect=" + redirectString 15 | } 16 | } 17 | }; 18 | 19 | module.exports = async () => { 20 | try { 21 | await server.register(require('@hapi/vision')); 22 | await server.register({ 23 | plugin: require('../lib/index.js'), 24 | options: config // pass in your redirect configuration in options 25 | }); 26 | await server.views({ 27 | engines: { 28 | html: require('handlebars') 29 | }, 30 | path: require('path').resolve(__dirname, '../example') 31 | }); 32 | Hoek.assert('no errors registering plugins'); 33 | return server; 34 | } catch (e) { 35 | throw e; 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Dwyl and Contributors 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hapi = require('@hapi/hapi'); 4 | var Boom = require('@hapi/boom'); 5 | var Hoek = require('@hapi/hoek'); 6 | var Joi = require('@hapi/validate'); 7 | 8 | var server = new Hapi.Server({ port: process.env.PORT }); 9 | 10 | server.route([ 11 | { 12 | method: 'GET', 13 | path: '/', 14 | config: { 15 | handler: function (request, reply) { 16 | var err = null; 17 | request.handleError(err); 18 | return 'hello'; 19 | } 20 | } 21 | }, 22 | { 23 | method: 'GET', 24 | path: '/error', 25 | config: { 26 | handler: function (request, reply) { 27 | throw new Error('500'); 28 | } 29 | } 30 | }, 31 | { 32 | method: 'GET', 33 | path: '/admin', 34 | config: { 35 | handler: function (request, reply) { 36 | throw Boom.unauthorized('Anauthorised'); 37 | } 38 | } 39 | }, 40 | { 41 | method: 'GET', 42 | path: '/management', 43 | config: { 44 | handler: function (request, reply) { 45 | throw Boom.forbidden('forbidden'); 46 | } 47 | } 48 | }, 49 | { 50 | method: 'GET', 51 | path: '/register/{param*}', 52 | config: { 53 | validate: { 54 | params: Joi.object({ param: Joi.string().min(4).max(160).alphanum() }), 55 | }, 56 | handler: function (request, reply) { 57 | if(request.params.param.indexOf('script') > -1) { // more validation 58 | throw Boom.notFound('hapi-error intercepts this'); 59 | } else { 60 | return 'Hello ' + request.params.param + '!'; 61 | } 62 | } 63 | } 64 | }, 65 | { 66 | method: 'GET', 67 | path: '/login', 68 | config: { 69 | handler: function (request, reply) { 70 | return 'please login'; 71 | } 72 | } 73 | } 74 | ]); 75 | 76 | module.exports = server; 77 | -------------------------------------------------------------------------------- /test/jwt_server_example.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.JWT_SECRET = 'supersecret'; // github.com/dwyl/hapi-auth-jwt2#generating-your-secret-key 4 | var Hapi = require('@hapi/hapi'); 5 | var path = require('path'); 6 | var Hoek = require('@hapi/hoek'); 7 | var assert = require('assert'); 8 | var server = new Hapi.Server({ port: 8765, debug: false }); 9 | 10 | 11 | var db = { 12 | '123': { allowed: true, name: 'Charlie', email: 'charlie@mail.co' }, 13 | '321': { allowed: false, name: 'Old Gregg'} 14 | }; 15 | 16 | // for a more real-world validate function, see: https://git.io/vPZmr 17 | var validate = function (decoded, request, callback) { 18 | if (db[decoded.id].allowed) { 19 | return callback(null, true); 20 | } 21 | else { 22 | return callback(null, false); 23 | } 24 | }; 25 | 26 | // server.start(function (err) { 27 | // assert(!err); 28 | // server.log('info', 'Visit: ' + server.info.uri); 29 | // }); 30 | 31 | module.exports = async () => { 32 | try { 33 | await server.register(require('../lib/index.js')); 34 | await server.register(require('@hapi/vision')); 35 | await server.register(require('hapi-auth-jwt2')); 36 | await server.views({ 37 | engines: { 38 | html: require('handlebars') 39 | }, 40 | path: path.resolve(__dirname, '../example') 41 | }); 42 | await server.auth.strategy('jwt', 'jwt', { 43 | key: process.env.JWT_SECRET, 44 | validate: validate 45 | }); 46 | await server.route([ 47 | { method: 'GET', path: '/throwerror', config: { auth: 'jwt' }, 48 | handler: function throwerror (request, reply) { 49 | var err = true; // deliberately throw an error for https://git.io/vPZ4A 50 | return request.handleError(err, { errorMessage: 'Sorry, we haz fail.'}); 51 | } 52 | }]); 53 | Hoek.assert('no errors registering plugins'); 54 | return server; 55 | } catch (e) { 56 | throw e; 57 | } 58 | }; 59 | ; 60 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hapi-error", 3 | "version": "3.0.0", 4 | "description": "catch errors in your hapi application and display the appropriate error message/page", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "nyc tape ./test/*.test.js | tap-nyc", 8 | "nocov": "tape ./test/*.test.js", 9 | "dev": "PORT=8000 ./node_modules/.bin/nodemon example/server_example.js", 10 | "start": "node example/server_example.js", 11 | "check-coverage": "npm run test && nyc check-coverage --statements 100 --functions 100 --lines 100 --branches 100", 12 | "lint": "node_modules/.bin/goodparts ./lib" 13 | }, 14 | "engines": { 15 | "node": ">=14.0.0" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/dwyl/hapi-error.git" 20 | }, 21 | "license": "BSD-3-Clause", 22 | "bugs": { 23 | "url": "https://github.com/dwyl/hapi-error/issues" 24 | }, 25 | "homepage": "https://github.com/dwyl/hapi-error#readme", 26 | "dependencies": { 27 | "@hapi/hoek": "^10.0.0" 28 | }, 29 | "devDependencies": { 30 | "@hapi/boom": "^10.0.0", 31 | "@hapi/good": "^9.0.1", 32 | "@hapi/hapi": "^20.2.2", 33 | "@hapi/validate": "^2.0.0", 34 | "@hapi/vision": "^6.1.0", 35 | "@types/hapi": "^18.0.7", 36 | "decache": "^4.6.1", 37 | "handlebars": "^4.7.7", 38 | "hapi-auth-jwt2": "^10.2.0", 39 | "jsonwebtoken": "^8.5.1", 40 | "nodemon": "^2.0.18", 41 | "nyc": "15.1.0", 42 | "pre-commit": "^1.2.2", 43 | "tap-nyc": "1.0.3", 44 | "tape": "5.5.3", 45 | "tape-async": "2.3.0" 46 | }, 47 | "pre-commit": [ 48 | "check-coverage" 49 | ], 50 | "keywords": [ 51 | "custom", 52 | "customise", 53 | "error", 54 | "friendly", 55 | "hapi", 56 | "hapijs", 57 | "hapi.js", 58 | "helpful", 59 | "html", 60 | "human", 61 | "json", 62 | "message", 63 | "page", 64 | "useful", 65 | "user friendly", 66 | "UX" 67 | ], 68 | "author": "dwyl & co", 69 | "nyc": { 70 | "exclude": [ 71 | "example/*.js", 72 | "coverage/*", 73 | "test/*.js" 74 | ], 75 | "report-dir": "./coverage", 76 | "reporter": [ 77 | "lcov", 78 | "text" 79 | ], 80 | "cache": false, 81 | "all": true 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Hoek = require('@hapi/hoek'); 4 | var pkg = require('../package.json'); // require package.json for attributes 5 | 6 | /** 7 | * isFunction checks if a given value is a function. 8 | * @param {Object} functionToCheck - the object we want to confirm is a function 9 | * @returns {Boolean} true|false 10 | */ 11 | function isFunction(functionToCheck) { 12 | const toString = Object.prototype.toString; 13 | return functionToCheck 14 | && toString.call(functionToCheck) === '[object Function]' 15 | || toString.call(functionToCheck) === '[object AsyncFunction]'; 16 | } 17 | 18 | /** 19 | * Merges in custom options to teh default config for each status code 20 | * @param {Object} config - the custom option object with status codes as keys 21 | * and objects with settings as values 22 | * @returns {Object} config to be used in plugin with defaults overwritten 23 | * and or added to 24 | */ 25 | function createConfig (config) { 26 | var mergedConfig = { 27 | templateName: 'error_template', 28 | statusCodes: { 29 | 401: { message: 'Please Login to view that page' }, 30 | 400: { message: 'Sorry, we do not have that page.' }, 31 | 404: { message: 'Sorry, that page is not available.' } 32 | } 33 | }; 34 | 35 | // Target status code configuration objects. 36 | var statusCodes = config.statusCodes || config; // Backwards compatibility. 37 | 38 | // Configure error template name. 39 | mergedConfig.templateName = config.templateName || mergedConfig.templateName; 40 | 41 | Object.keys(statusCodes).forEach(function (statusCode) { 42 | if (!mergedConfig.statusCodes[statusCode]) { 43 | mergedConfig.statusCodes[statusCode] = {}; 44 | } 45 | // Configure status code settings. 46 | Object.keys(statusCodes[statusCode]).forEach(function (setting) { 47 | mergedConfig.statusCodes[statusCode][setting] 48 | = statusCodes[statusCode][setting]; 49 | }); 50 | }); 51 | 52 | return mergedConfig; 53 | }; 54 | 55 | 56 | /** 57 | * Takes an error Object and Message and throws Hoek Error if not null 58 | * @param {String} error - error Object or null 59 | * @param {String|Object} [errorMessage] - Optional error message String/Object 60 | * @returns {Boolean} false. 61 | */ 62 | function handleError (error, errorMessage) { 63 | if (errorMessage) { 64 | return Hoek.assert(!error, errorMessage); 65 | } 66 | 67 | return Hoek.assert(!error, error); 68 | }; 69 | // export for use in files that do not have access to the request object 70 | exports.handleError = handleError; // e.g. database-specific getters/setters 71 | 72 | /** 73 | * register defines our errorHandler plugin following the standard hapi plugin 74 | * @param {Object} server - the server instance where the plugin is being used 75 | * @param {Object} options - any configuration options passed into the plugin 76 | * @returns {Function} reply.continue is called when the plugin is finished 77 | */ 78 | exports.plugin = { 79 | pkg: pkg, 80 | register: async function (server, options) { 81 | // creates config for handler to be used in 'onPreResponse' function 82 | var config = createConfig(options); 83 | 84 | // make handleError available on request 85 | server.ext('onRequest', function (request, reply) { 86 | request.handleError = handleError; // github.com/dwyl/hapi-error/issues/23 87 | 88 | return reply.continue; 89 | }); 90 | 91 | // onPreResponse intercepts ALL errors 92 | server.ext('onPreResponse', function (request, reply) { 93 | var res = request.response; 94 | var req = request.raw.req; 95 | var msg = 'Sorry, something went wrong, please retrace your steps.'; 96 | var statusCode = 200; // default to "success" 97 | var accept = request.raw.req.headers.accept; 98 | var debug; // defined here to keep JSLint Happy. 99 | 100 | if (res.isBoom) { 101 | statusCode = res.output.payload.statusCode; 102 | 103 | debug = { 104 | method: req.method, // e.g GET/POST 105 | url: request.url.path, // the path the person requested 106 | headers: request.raw.req.headers, // all HTTP Headers 107 | info: request.info, // all additional request info (useful to debug) 108 | auth: request.auth, // any authentication details e.g. decoded JWT 109 | payload: request.payload, // the complete request payload received 110 | response: res.output.payload, // response before error intercepted 111 | stackTrace: res.stack // the stack trace of the error 112 | }; 113 | // ALWAYS Log the error 114 | server.log('error', debug); // github.com/dwyl/hapi-error/issues/22 115 | 116 | // Header check, should take priority 117 | if (accept && accept.match(/json/)) { // support REST/JSON requests 118 | return reply.response(res.output.payload).code(statusCode); 119 | } 120 | // custom redirect https://github.com/dwyl/hapi-error/issues/5 121 | var currentCodeConfig = config.statusCodes[statusCode]; 122 | if (currentCodeConfig && currentCodeConfig.redirect) { 123 | // if redirect is function invoke it with the request object 124 | if (isFunction(currentCodeConfig.redirect)) { 125 | const url = currentCodeConfig.redirect(request) 126 | return reply.redirect(url); 127 | } 128 | else { 129 | // if parameter is string, append redirect query 130 | var redirectString = request.url.pathname + request.url.search; 131 | return reply.redirect(currentCodeConfig.redirect + '?redirect=' + redirectString); 132 | } 133 | } 134 | 135 | if (currentCodeConfig && currentCodeConfig.message) { 136 | msg = isFunction(currentCodeConfig.message) 137 | ? currentCodeConfig.message(msg, request) 138 | : currentCodeConfig.message 139 | ; 140 | } 141 | 142 | res = Object.assign(debug, { 143 | errorTitle: res.output.payload.error, 144 | statusCode: statusCode, 145 | errorMessage: msg 146 | }); 147 | 148 | // next avoids TypeError if view rendering is not used in app e.g API! 149 | // see: https://github.com/dwyl/hapi-error/issues/49 150 | if (!reply.view) { 151 | return reply.response(res).code(statusCode); 152 | } 153 | 154 | return reply.view(config.templateName, res).code(statusCode); // e.g 401 155 | }; // end if (res.isBoom) 156 | return reply.continue; // continue processing the request 157 | }); 158 | } 159 | }; 160 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const test = require('tape-async'); 4 | const JWT = require('jsonwebtoken'); 5 | const decache = require('decache'); 6 | 7 | /************************* handleError method test ***************************/ 8 | 9 | const handleError = require('../lib').handleError; 10 | 11 | test("handleError no error is thrown when error = null", function (t) { 12 | const error = null; 13 | t.equal(handleError(error), undefined, 'No error thrown'); 14 | t.end(); 15 | }); 16 | 17 | test("handleError don't throw error even if errorMessage is set", function (t) { 18 | const error = null; 19 | t.equal(handleError(error, 'this will not throw!'), undefined, 'No error thrown'); 20 | t.end(); 21 | }); 22 | 23 | /************************* REDIRECT TEST ***************************/ 24 | const redirectServerExampleLocation = './redirect_server_example'; 25 | 26 | test("GET /admin?hello=world should re-direct to /login?redirect=/admin?hello=world", async function (t) { 27 | decache(redirectServerExampleLocation); 28 | const redirectServer = await require(redirectServerExampleLocation)(); 29 | 30 | const urlWithQuery = '/admin?hello=world'; 31 | const combinedUrl = '/login?redirect=/admin?hello=world'; 32 | 33 | const options = { 34 | method: 'GET', 35 | url: urlWithQuery // this will re-direct to /login 36 | }; 37 | 38 | const res = await redirectServer.inject(options); 39 | t.equal(res.statusCode, 302, 'statusCode: + ' + res.statusCode + ' (as expected)'); 40 | t.equal(res.headers.location, combinedUrl, 'Successfully redirected to: ' + combinedUrl); 41 | t.end( await redirectServer.stop() ); 42 | }); 43 | 44 | test("GET /management?hello=world should re-direct to /login?redirect=/management?hello=world", async function (t) { 45 | decache(redirectServerExampleLocation); 46 | const redirectServer = await require(redirectServerExampleLocation)(); 47 | 48 | const urlWithQuery = '/management?hello=world'; 49 | const combinedUrl = '/login?redirect=/management?hello=world'; 50 | 51 | const options = { 52 | method: 'GET', 53 | url: urlWithQuery // this will re-direct to /login 54 | }; 55 | 56 | const res = await redirectServer.inject(options); 57 | t.equal(res.statusCode, 302, 'statusCode: + ' + res.statusCode + ' (as expected)'); 58 | t.equal(res.headers.location, combinedUrl, 'Successfully redirected to: ' + combinedUrl); 59 | t.end( await redirectServer.stop() ); 60 | }); 61 | 62 | /************************* Message TEST ***************************/ 63 | test('Initializing message_server_example', async function (t) { 64 | try { 65 | decache('../example/server.js'); 66 | const messageServer = await require('./message_server_example')(); 67 | test('example of overriding the', async function (t) { 68 | const options = { 69 | method: 'GET', 70 | url: '/notfound' 71 | }; 72 | 73 | const res = await messageServer.inject(options); 74 | t.ok(res.payload.includes('robots in disguise'), '404 gets transformed'); 75 | t.equal(res.statusCode, 404, 'statusCode give back ok'); 76 | t.end(); 77 | }); 78 | 79 | test('example of adding a new message transform which uses req',async function (t) { 80 | const options = { 81 | method: 'GET', 82 | url: '/error' 83 | }; 84 | 85 | const res = await messageServer.inject(options); 86 | t.ok(res.payload.includes('User agent: shot'), 'Internal Server Error'); 87 | t.equal(res.statusCode, 500, 'statusCode 500'); 88 | t.end(); 89 | }); 90 | 91 | test('close messageServer', async function (t) { 92 | await messageServer.stop(); 93 | t.end() 94 | }); 95 | } catch (e) { 96 | throw e; 97 | } 98 | }); 99 | 100 | // /************************* Regular TESTS ***************************/ 101 | test('Initializing server_example', async function (t) { 102 | decache('../example/server.js'); 103 | const server = await require('../example/server_example')(); 104 | 105 | test("GET / returns 200",async function (t) { 106 | const options = { 107 | method: 'GET', 108 | url: '/', 109 | headers: { accept: 'application/json' } 110 | }; 111 | 112 | const res = await server.inject(options); 113 | t.ok(res.payload.includes('hello'), 'No Errors'); 114 | t.equal(res.statusCode, 200, 'statusCode 200'); 115 | t.end(); 116 | }); 117 | 118 | test("GET /login ",async function (t) { 119 | const options = { 120 | method: 'GET', 121 | url: '/login', 122 | headers: { accept: 'application/json' } 123 | }; 124 | const res = await server.inject(options); 125 | t.equal(res.statusCode, 200, 'statusCode 200'); 126 | t.ok(res.payload.includes('please login'), 'Please Login'); 127 | t.end(); 128 | }); 129 | 130 | test("GET /notfound returns 404",async function (t) { 131 | const options = { 132 | method: 'GET', 133 | url: '/notfound' 134 | }; 135 | const res = await server.inject(options); 136 | t.ok(res.payload.includes('not available'), 'page not available'); 137 | t.equal(res.statusCode, 404, 'statusCode 404'); 138 | t.end(); 139 | }); 140 | 141 | test("GET /admin expect to see 401 unauthorized error",async function (t) { 142 | const options = { 143 | method: 'GET', 144 | url: '/admin' 145 | }; 146 | const res = await server.inject(options); 147 | t.ok(res.payload.includes('Please Login'), 'Please login to see /admin'); 148 | t.equal(res.statusCode, 401, 'statusCode 401'); 149 | t.end(); 150 | }); 151 | 152 | test("GET /error returns 500 Error HTML Page",async function (t) { 153 | const options = { 154 | method: 'GET', 155 | url: '/error' 156 | }; 157 | const res = await server.inject(options); 158 | t.ok(res.payload.includes('500'), 'Internal Server Error'); 159 | t.equal(res.statusCode, 500, 'statusCode 500'); 160 | t.end(); 161 | }); 162 | 163 | test("GET /error returns JSON when headers.accept 'application/json'",async function (t) { 164 | const options = { 165 | method: 'GET', 166 | url: '/error', 167 | headers: { accept: 'application/json' } 168 | }; 169 | const res = await server.inject(options); 170 | t.ok(res.payload.includes('Internal Server Error'), '500 Server Error'); 171 | t.equal(res.statusCode, 500, 'Got statusCode 500 (as expected)'); 172 | t.end(); 173 | }); 174 | 175 | test("GET /register/username passes validation",async function (t) { 176 | const options = { 177 | method: 'GET', 178 | url: '/register/username' 179 | }; 180 | const res = await server.inject(options); 181 | t.ok(res.payload.includes('Hello username'), 'Passes validation'); 182 | t.equal(res.statusCode, 200, 'statusCode 200'); 183 | t.end(); 184 | }); 185 | 186 | test("GET /register/22%3A%5B%22black%22%5D%7D%22%3E%3C%7%203cript fails Joi validation",async function (t) { 187 | const options = { 188 | method: 'GET', 189 | url: '/register/22%3A%5B%22black%22%5D%7D%22%3E%3C%7%203cript%3Ealert%281%29%3C%2fscript%3E' 190 | }; 191 | const res = await server.inject(options); 192 | t.ok(res.payload.includes('Sorry'), 'Fails Joi validation'); 193 | t.equal(res.statusCode, 400, 'intercepted error > 400'); 194 | t.end(); 195 | }); 196 | 197 | test("GET /register/myscript fails additional (CUSTOM) validation",async function (t) { 198 | const options = { 199 | method: 'GET', 200 | url: '/register/myscript?hello=world' 201 | }; 202 | const res = await server.inject(options); 203 | t.ok(res.payload.includes('Sorry, that page is not available.'), 'Got Friendly 404 Page'); 204 | t.equal(res.statusCode, 404, 'Got 404'); 205 | t.end(); 206 | }); 207 | 208 | server.stop() 209 | }); 210 | 211 | // /************************* 'email' prop Available in Error Template/View ***************/ 212 | test('Initializing server', async function (t) { 213 | decache('./jwt_server_example'); 214 | const jwtserver = await require('./jwt_server_example')(); 215 | 216 | test("GET /error should display an error page containing the current person's email address",async function (t) { 217 | decache('../lib/index.js'); // ensure we have a fresh module 218 | const person = { id: 123, email: 'charlie@mail.me' } 219 | const token = JWT.sign(person, process.env.JWT_SECRET); 220 | 221 | const options = { 222 | method: 'GET', 223 | url: '/throwerror', 224 | headers: { authorization: "Bearer " + token } 225 | }; 226 | 227 | const res = await jwtserver.inject(options); 228 | t.equal(res.statusCode, 500, 'statusCode: + ' + res.statusCode + ' (as expected)'); 229 | jwtserver.stop(); 230 | t.end(); 231 | }); 232 | }); 233 | 234 | // /************************* API (no vision) Tests ***************************/ 235 | test('Initializing api_server', async function (t) { 236 | decache('./api_server.js'); 237 | const apiServer = await require('./api_server.js')(); 238 | 239 | test('regression test for #49 (when no vison views configured)', async function (t) { 240 | const options = { url: '/error' }; 241 | 242 | const res = await apiServer.inject(options); 243 | t.equal(res.statusCode, 404, 'statusCode give back ok'); 244 | apiServer.stop(); 245 | t.end(); 246 | }); 247 | }); 248 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # `hapi-error` 4 | 5 | Intercept errors in your Hapi web app/api 6 | and send a *useful* message to the client. 7 | 8 | ![dilbert-404-error](https://cloud.githubusercontent.com/assets/194400/17856406/53feeee4-6875-11e6-8480-d493906f6aa1.png) 9 | 10 | [![Known Vulnerabilities](https://snyk.io/test/github/dwyl/hapi-error/badge.svg?targetFile=package.json&style=flat-square)](https://snyk.io/test/github/dwyl/hapi-error?targetFile=package.json) 11 | [![Build Status](https://img.shields.io/travis/dwyl/hapi-error/master.svg?style=flat-square)](https://travis-ci.org/dwyl/hapi-error) 12 | [![codecov.io](https://img.shields.io/codecov/c/github/dwyl/hapi-error/master.svg?style=flat-square)](https://codecov.io/github/dwyl/hapi-error?branch=master) 13 | [![HAPI 20.2.x](https://img.shields.io/badge/hapi-18.4.0-brightgreen.svg?style=flat-square "Latest Hapi.js")](https://hapijs.com) 14 | [![Node.js Version](https://img.shields.io/node/v/hapi-error.svg?style=flat-square "Node.js 14 and above supported")](https://nodejs.org/download/) 15 | [![Dependencies Status](https://david-dm.org/dwyl/hapi-error/status.svg?style=flat-square)](https://david-dm.org/dwyl/hapi-error) 16 | [![devDependencies Status](https://david-dm.org/dwyl/hapi-error/dev-status.svg?style=flat-square)](https://david-dm.org/dwyl/hapi-error?type=dev) 17 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/hapi-error/issues) 18 | [![HitCount](https://hits.dwyl.io/dwyl/hapi-error.svg)](https://hits.dwyl.io/dwyl/hapi-error) 19 | [![npm package version](https://img.shields.io/npm/v/hapi-error.svg?style=flat-square)](https://www.npmjs.com/package/hapi-error) 20 | 21 | 22 |
23 | 24 | ## *Why*? 25 | 26 | > #### Seeing an (_unhelpful/unfriendly_) error message is _by far_ the _most frustrating_ part of the "**User _Experience_**" (**UX**) of your web app/site. 27 | 28 | Most _non-technical_ people (_"average" web users_) have _no clue_ 29 | what a `401` error is. And if you/we the developer(s) do not _communicate_ with them, it can quickly lead to confusion and 30 | [_abandonment_](https://en.wikipedia.org/wiki/Abandonment_rate)! 31 | If instead of simply displaying **`401`** we _inform_ people: 32 | `"Please login to see that page."` we _**instantly improve**_ 33 | the **UX** and thus make that person's day/life better. :heart: 34 | 35 | > _The "**Number 1 Rule**" is to make sure your **error messages** 36 | sound like they’ve been **written for/by humans**_. 37 | [~ _The **Four H**'s of Writing Error Messages_](https://uxmas.com/2012/the-4-hs-of-writing-error-messages) 38 | 39 | ## *What*? 40 | 41 | By `default`, `Hapi` does _not_ give people *friendly* error messages. 42 | 43 | `hapi-error` is a plugin that lets your Hapi app display _consistent_, _**human-friendly**_ & *useful* 44 | error messages so the _people_ using your app 45 | [_don't panic_](https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#Don.27t_Panic). 46 | 47 | > Try it: https://hapi-error.herokuapp.com/panacea 48 | 49 | 50 | Under the hood, Hapi uses 51 | [`Boom`](https://github.com/dwyl/learn-hapi#error-handling-with-boom) 52 | to handle errors. These errors are returned as `JSON`. e.g: 53 | 54 | If a URL/Endpoint does not exist a `404` error is returned: 55 | ![hapi-login-404-error](https://cloud.githubusercontent.com/assets/194400/14770263/06bdc6dc-0a65-11e6-9f9b-80944711a4f1.png) 56 | 57 | When a person/client attempts to access a "*restricted*" endpoint without 58 | the proper authentication/authorisation a `401` error is shown: 59 | 60 | ![hapi-login-401-error](https://cloud.githubusercontent.com/assets/194400/14770276/57022f20-0a65-11e6-86de-d9b8e456b344.png) 61 | 62 | And if an *unknown* error occurs on the server, a `500` error is *thrown*: 63 | 64 | ![localhost-500-error](https://cloud.githubusercontent.com/assets/194400/14770517/98a4b6d6-0a6b-11e6-8448-4b66e3df9a9a.png) 65 | 66 | The `hapi-error` plugin *re-purposes* the `Boom` errors (*both the standard Hapi errors and your custom ones*) and instead display human-friendly error *page*: 67 | 68 | ![hapi-error-screens](https://cloud.githubusercontent.com/assets/194400/15275274/ef9e5402-1abe-11e6-9313-71b11c61f032.png) 69 | 70 | > ***Note***: *super basic error page example is just what we came up with in a few minutes, you have full control over what your error page looks like, so use your imagination*! 71 | 72 | > ***Note***: if the client expects a JSON response simply define 73 | that in the `headers.accept` and it will still receive the JSON error messages. 74 | 75 | ## *v3.0.0 Changes* 76 | 1. Support for Hapi.js v20 77 | 2. Not backward compatible with Hapi.js < v18 78 | 3. Requires NodeJS v14 and above 79 | 80 | ## *How*? 81 | 82 | > **Note**: If you (_or anyone on your team_) are _unfamiliar_ with **Hapi.js** we have a 83 | quick guide/tutorial to help get you started: [https://github.com/dwyl/**learn-hapi**](https://github.com/dwyl/learn-hapi) 84 | 85 | Error handling in 3 *easy* steps: 86 | 87 | ### 1. Install the [plugin](https://www.npmjs.com/package/hapi-error) from NPM: 88 | 89 | ```sh 90 | npm install hapi-error --save 91 | ``` 92 | 93 | ### 2. Include the plugin in your Hapi project 94 | 95 | Include the plugin when you `register` your `server`: 96 | 97 | ```js 98 | var Hapi = require('@hapi/hapi'); 99 | var Path = require('path'); 100 | var server = new Hapi.Server({ port: process.env.PORT || 8000 }); 101 | 102 | server.route([ 103 | { 104 | method: 'GET', 105 | path: '/', 106 | config: { 107 | handler: function (request, reply) { 108 | reply('hello world'); 109 | } 110 | } 111 | }, 112 | { 113 | method: 'GET', 114 | path: '/error', 115 | config: { 116 | handler: function (request, reply) { 117 | reply(new Error('500')); 118 | } 119 | } 120 | } 121 | ]); 122 | 123 | // this is where we include the hapi-error plugin: 124 | module.exports = async () => { 125 | try { 126 | await server.register(require('hapi-error')); 127 | await server.register(require('vision')); 128 | server.views({ 129 | engines: { 130 | html: require('handlebars') // or Jade or Riot or React etc. 131 | }, 132 | path: Path.resolve(__dirname, '/your/view/directory') 133 | }); 134 | await server.start(); 135 | return server; 136 | } catch (e) { 137 | throw e; 138 | } 139 | }; 140 | ``` 141 | 142 | > See: [/example/server_example.js](https://github.com/dwyl/hapi-error/blob/master/example/server_example.js) for simple example 143 | 144 | ### 3. Create an Error View Template 145 | 146 | The default template name is `error_template` and is expected to exist, but can be configured in the options: 147 | 148 | ```js 149 | const config = { 150 | templateName: 'my-error-template' 151 | }; 152 | ``` 153 | 154 | > Note: `hapi-error` plugin *expects* you are using [`Vision`](https://github.com/hapijs/vision) (*the standard view rendering library for Hapi apps*) 155 | which allows you to use Handlebars, Jade, [**Riot**](https://github.com/dwyl/hapi-riot), React, etc. for your templates. 156 | 157 | Your `templateName` (*or `error_template.ext` `error_template.tag` `error_template.jsx`*) should make use of the 3 variables it will be passed: 158 | 159 | + `errorTitle` - *the error tile generated by Hapi* 160 | + `statusCode` - *HTTP statusCode sent to the client *e.g: `404`* (*not found*) 161 | + `errorMessage` - the *human-friendly error message* 162 | 163 | > for an example see: [`/example/error_template.html`](https://github.com/dwyl/hapi-error/blob/master/example/error_template.html) 164 | 165 | ### 4. *Optional* Add `statusCodes` config object to transform messages or redirect for certain status codes 166 | 167 | Each status code can be given two properties `message` and `redirect`. 168 | 169 | The default config object for status codes: 170 | ``` 171 | const config = { 172 | statusCodes: { 173 | 401: { message: 'Please Login to view that page' }, 174 | 400: { message: 'Sorry, we do not have that page.' }, 175 | 404: { message: 'Sorry, that page is not available.' } 176 | } 177 | }; 178 | ``` 179 | We want to provide useful error messages that are pleasant for the user. If you think there are better defaults for messages or other codes then do let us know via [issue](https://github.com/dwyl/hapi-error/issues). 180 | 181 | Any of the above can be overwritten and new status codes can be added. 182 | 183 | #### `message` Parse/replace the error message 184 | 185 | This parameter can be of the form `function(message, request)` or just simply a `'string'` to replace the message. 186 | 187 | An example of a use case would be handling errors form joi validation. 188 | 189 | Or erroring in different languages. 190 | ```js 191 | const config = { 192 | statusCodes: { 193 | "401": { 194 | "message": function(msg, req) { 195 | var lang = findLang(req); 196 | 197 | return translate(lang, message); 198 | } 199 | } 200 | } 201 | }; 202 | ``` 203 | 204 | Or providing nice error messages like in the default config above. 205 | 206 | #### `redirect` *Redirecting* to another endpoint 207 | 208 | Sometimes you don't _want_ to show an error page; 209 | _instead_ you want to re-direct to another page. 210 | For example, when your route/page requires the person 211 | to be authenticated (_logged in_), but they have 212 | not supplied a valid session/token to view the route/page. 213 | 214 | In this situation the default Hapi behaviour is to return a `401` (_unauthorized_) error, 215 | however this is not very _useful_ to the _person_ using your application. 216 | 217 | Redirecting to a specific url is _easy_ with `hapi-error`: 218 | 219 | ```js 220 | const config = { 221 | statusCodes: { 222 | "401": { // if the statusCode is 401 223 | "redirect": "/login" // redirect to /login page/endpoint 224 | }, 225 | "403": { // if the statusCode is 403 226 | "redirect": function (request) { 227 | return "/login?redirect=" + request.url.pathname 228 | } 229 | } 230 | } 231 | } 232 | (async () => { 233 | await server.register({ 234 | plugin: require('hapi-error'), 235 | options: config // pass in your redirect configuration in options 236 | }); 237 | await server.register(require('vision')); 238 | })(); 239 | ``` 240 | 241 | This in both cases will `redirect` the client/browser to the `/login` endpoint 242 | and will append a query parameter with the url the person was _trying_ to visit. 243 | With the use of function instead of simple string you can further manipulate the resulted url. 244 | Should the parameter be a function and return false it will be ignored. 245 | 246 | e.g: GET /admin --> 401 unauthorized --> redirect to /login?redirect=/admin 247 | 248 | > Redirect Example: [/redirect_server_example.js](https://github.com/dwyl/hapi-error/blob/master/test/redirect_server_example.js) 249 | 250 | 251 | ## *That's it*! 252 | 253 | *Want more...?* [*ask*!](https://github.com/dwyl/hapi-error/issues) 254 | 255 | ## *Custom* Error Messages using `request.handleError` 256 | 257 | When you `register` the `hapi-error` plugin a _useful_ `handleError` method 258 | becomes available in every request handler which allows you to (_safely_) 259 | "handle" any "*thrown*" errors using just one line of code. 260 | 261 | Consider the following Hapi route handler code that is fetching data from a generic Database: 262 | 263 | ```js 264 | function handler (request, reply) { 265 | db.get('yourkey', function (err, data) { 266 | if (err) { 267 | return reply('error_template', { msg: 'A database error occurred'}); 268 | } else { 269 | return reply('amazing_app_view', {data: data}); 270 | } 271 | }); 272 | } 273 | ``` 274 | This can be re-written (*simplified*) using `request.handleError` method: 275 | 276 | ```js 277 | function handler (request, reply) { 278 | db.get('yourkey', function (err, data) { // much simpler, right? 279 | request.handleError(err, 'A database error occurred'); 280 | return reply('amazing_app_view', {data: data}); 281 | }); // this has *exactly* the same effect in much less code. 282 | } 283 | ``` 284 | Output: 285 | 286 | ![hapi-error-a-database-error-occured](https://cloud.githubusercontent.com/assets/194400/19078231/590d2d80-8a47-11e6-82e2-742d193b43b9.png) 287 | 288 | #### Explanation: 289 | 290 | Under the hood, `request.handleError` is using `Hoek.assert` which 291 | will `assert` that there is ***no error*** e.g: 292 | 293 | `Hoek.assert(!err, 'A database error occurred');` 294 | 295 | Which means that if there *is* an error, it will be "*thrown*" 296 | with the message you define in the *second argument*. 297 | 298 |
299 | 300 | ### `handleError` _everywhere_ 301 | 302 | > Need to call `handleError` _outside_ of the context of the `request` ? 303 | 304 | Sometimes we create handlers that perform a task _outside_ of the context of 305 | a route/handler (_e.g accessing a database or API_) in this context 306 | we still want to use `handleError` to simplify error handling. 307 | 308 | This is easy with `hapi-error`, here's an example: 309 | 310 | ```js 311 | var handleError = require('hapi-error').handleError; 312 | 313 | db.get(key, function (error, result) { 314 | handleError(error, 'Error retrieving ' + key + ' from DB :-( '); 315 | return callback(err, result); 316 | }); 317 | ``` 318 | or in a file operation (_uploading a file to AWS S3_): 319 | 320 | ```js 321 | var handleError = require('hapi-error').handleError; 322 | 323 | s3Bucket.upload(params, function (err, data) { 324 | handleError(error, 'Error retrieving ' + key + ' from DB :-( '); 325 | return callback(err, result); 326 | } 327 | ``` 328 | 329 | Provided the `handleError` is called from a function/helper 330 | that is being _run_ by a Hapi server any errors will be _intercepted_ 331 | and _logged_ and displayed (_nicely_) to people using your app. 332 | 333 | ### _custom_ data in error pages 334 | 335 | > Want/need to pass some more/custom data to display in your `error_template` view? 336 | 337 | All you have to do is pass an object to `request.handleError` with an 338 | errorMessage property and any other template properties you want! 339 | 340 | For example: 341 | ```js 342 | request.handleError(!error, {errorMessage: 'Oops - there has been an error', 343 | email: 'example@mail.co', color:'blue'}); 344 | ``` 345 | You will then be able to use {{email}} and {{color}} in your `error_template.html` 346 | 347 | ### logging 348 | 349 | As with _all_ hapi apps/APIs the recommended approach to logging 350 | is to use [`good`](https://github.com/dwyl/learn-hapi#logging-with-good) 351 | 352 | `hapi-error` logs all errors using `server.log` (_the standard way of logging in Hapi apps_) so once you enable `good` in your app you will _see_ any errors in your logs. 353 | 354 | e.g: 355 | ![hapi-error-log](https://cloud.githubusercontent.com/assets/194400/19013932/f2471060-87d6-11e6-980a-d7210c9fea7e.png) 356 | 357 | ### Debugging 358 | 359 | If you need more debugging in your error template, `hapi-error` exposes _several_ 360 | useful properties which you can use. 361 | 362 | ```js 363 | { 364 | "method":"GET", 365 | "url":"/your-endpoint", 366 | "headers":{ 367 | "authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzLCJlbWFpbCI6ImhhaUBtYWlsLm1lIiwiaWF0IjoxNDc1Njc0MDQ2fQ.Xc6nCPQW4ZSf9jnIIs8wYsM4bGtvpe8peAxp6rq4y0g", 368 | "user-agent":"shot", 369 | "host":"http://yourserver:3001" 370 | }, 371 | "info":{ 372 | "received":1475674046045, 373 | "responded":0, 374 | "remoteAddress":"127.0.0.1", 375 | "remotePort":"", 376 | "referrer":"", 377 | "host":"http://yourserver:3001", 378 | "acceptEncoding":"identity", 379 | "hostname":"http://yourserver:3001" 380 | }, 381 | "auth":{ 382 | "isAuthenticated":true, 383 | "credentials":{ 384 | "id":123, 385 | "email":"hai@mail.me", 386 | "iat":1475674046 387 | }, 388 | "strategy":"jwt", 389 | "mode":"required", 390 | "error":null, 391 | "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzLCJlbWFpbCI6ImhhaUBtYWlsLm1lIiwiaWF0IjoxNDc1Njc0MDQ2fQ.Xc6nCPQW4ZSf9jnIIs8wYsM4bGtvpe8peAxp6rq4y0g" 392 | }, 393 | "email":"hai@mail.me", 394 | "payload":null, 395 | "response":{ 396 | "statusCode":500, 397 | "error":"Internal Server Error", 398 | "message":"An internal server error occurred" 399 | } 400 | } 401 | ``` 402 | 403 | All the properties which are logged by `hapi-error` are available in 404 | your error template. 405 | 406 | ### Are Query Parameters Preserved? 407 | 408 | ***Yes***! e.g: if the original url is `/admin?sort=desc` 409 | the redirect url will be: `/login?redirect=/admin?sort=desc` 410 | Such that after the person has logged in they will be re-directed 411 | back to to `/admin?sort=desc` _as desired_. 412 | 413 | And it's valid to have multiple question marks in the URL see: 414 | https://stackoverflow.com/questions/2924160/is-it-valid-to-have-more-than-one-question-mark-in-a-url 415 | so the query is preserved and can be used to send the person 416 | to the _exact_ url they requested _after_ they have successfully logged in. 417 | 418 |
419 | 420 | ### Under the Hood (_Implementation Detail_): 421 | 422 | When there is an error in the request/response cycle, 423 | the Hapi `request` Object has *useful* error object we can use. 424 | 425 | Try logging the `request.response` in one of your Hapi route handlers: 426 | 427 | ```js 428 | console.log(request.response); 429 | ``` 430 | A typical `Boom` error has the format: 431 | ```js 432 | { [Error: 500] 433 | isBoom: true, 434 | isServer: true, 435 | data: null, 436 | output: 437 | { statusCode: 500, 438 | payload: 439 | { statusCode: 500, 440 | error: 'Internal Server Error', 441 | message: 'An internal server error occurred' }, 442 | headers: {} }, 443 | reformat: [Function] } 444 | ``` 445 | 446 | The way to *intercept* this error is with a plugin that gets invoked 447 | *before* the response is returned to the client. 448 | 449 | See: [lib/index.js](https://github.com/dwyl/hapi-error/blob/master/lib/index.js) 450 | for details on how the plugin is implemented. 451 | 452 | If you have _any_ questions, just [*ask*!](https://github.com/dwyl/hapi-error/issues) 453 | 454 | 455 | ## Background Reading & Research 456 | 457 | + Writing *useful* / *friendly* error messages: 458 | https://medium.com/@thomasfuchs/how-to-write-an-error-message-883718173322 459 | --------------------------------------------------------------------------------