/hapi-error

:umbrella: Intercept errors in your Hapi Web App/API and send a *useful* message to the client OR redirect to the desired endpoint.

Primary LanguageJavaScript

hapi-error

Intercept errors in your Hapi web app/api and send a useful message to the client.

Build Status Test Coverage JavaScript Style Guide Code Climate Dependency Status devDependencies Status contributions welcome npm package version

dilbert-404-error

Why?

Seeing an (unhelpful/unfriendly) error message is by far the most frustrating part of the "User Experience" (UX) of your web app/site.

Most non-technical people ("average" web users) have no clue what a 401 error is. And if you/we the developer(s) do not communicate with them, it can quickly lead to confusion and abandonment! If instead of simply displaying 401 we inform people: "Please login to see that page." we instantly improve the UX and thus make that person's day/life better. ❤️

The "Number 1 Rule" is to make sure your error messages sound like they’ve been written for/by humans. ~ The Four H's of Writing Error Messages

What?

By default, Hapi does not give people friendly error messages.

hapi-error is a plugin that lets your Hapi app display consistent, human-friendly & useful error messages so the people using your app don't panic.

Try it: http://hapi-error.herokuapp.com/panacea

Under the hood, Hapi uses Boom to handle errors. These errors are returned as JSON. e.g:

If a URL/Endpoint does not exist a 404 error is returned:
hapi-login-404-error

When a person/client attempts to access a "restricted" endpoint without the proper authentication/authorisation a 401 error is shown:

hapi-login-401-error

And if an unknown error occurs on the server, a 500 error is thrown:

localhost-500-error

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:

hapi-error-screens

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!

Note: if the client expects a JSON response simply define that in the headers.accept and it will still receive the JSON error messages.

v2.0.0 Changes

  1. Support for Hapi.js v17
  2. Not backward compatible with Hapi.js < v17
  3. Requires NodeJS v8 and above

How?

Note: If you (or anyone on your team) are unfamiliar with Hapi.js we have a quick guide/tutorial to help get you started: https://github.com/dwyl/learn-hapi

Error handling in 3 easy steps:

1. Install the plugin from NPM:

npm install hapi-error --save

2. Include the plugin in your Hapi project

Include the plugin when you register your server:

var Hapi = require('hapi');
var Path = require('path');
var server = new Hapi.Server({ port: process.env.PORT || 8000 });

server.route([
  {
    method: 'GET',
    path: '/',
    config: {
      handler: function (request, reply) {
        reply('hello world');
      }
    }
  },
  {
    method: 'GET',
    path: '/error',
    config: {
      handler: function (request, reply) {
        reply(new Error('500'));
      }
    }
  }
]);

// this is where we include the hapi-error plugin:
module.exports = async () => {
  try {
    await server.register(require('hapi-error'));
    await server.register(require('vision'));
    server.views({
      engines: {
        html: require('handlebars') // or Jade or Riot or React etc.
      },
      path: Path.resolve(__dirname, '/your/view/directory')
    });
    await server.start();
    return server;
  } catch (e) {
    throw e;
  }
};

See: /example/server_example.js for simple example

3. Create an Error View Template

The default template name is error_template and is expected to exist, but can be configured in the options:

const config = {
  templateName: 'my-error-template'
};

Note: hapi-error plugin expects you are using Vision (the standard view rendering library for Hapi apps) which allows you to use Handlebars, Jade, Riot, React, etc. for your templates.

Your templateName (or error_template.ext error_template.tag error_template.jsx) should make use of the 3 variables it will be passed:

  • errorTitle - the error tile generated by Hapi
  • statusCode - *HTTP statusCode sent to the client e.g: 404 (not found)
  • errorMessage - the human-friendly error message

for an example see: /example/error_template.html

4. Optional Add statusCodes config object to transform messages or redirect for certain status codes

Each status code can be given two properties message and redirect.

The default config object for status codes:

const config = {
  statusCodes: {
    401: { message: 'Please Login to view that page' },
    400: { message: 'Sorry, we do not have that page.' },
    404: { message: 'Sorry, that page is not available.' }
  }
};

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.

Any of the above can be overwritten and new status codes can be added.

message Parse/replace the error message

This parameter can be of the form function(message, request) or just simply a 'string' to replace the message.

An example of a use case would be handling errors form joi validation.

Or erroring in different languages.

const config = {
  statusCodes: {
    "401": {
      "message": function(msg, req) {
        var lang = findLang(req);

        return translate(lang, message);
      }
    }
  }
};

Or providing nice error messages like in the default config above.

redirect Redirecting to another endpoint

Sometimes you don't want to show an error page; instead you want to re-direct to another page. For example, when your route/page requires the person to be authenticated (logged in), but they have not supplied a valid session/token to view the route/page.

In this situation the default Hapi behaviour is to return a 401 (unauthorized) error, however this is not very useful to the person using your application.

Redirecting to a specific url is easy with hapi-error:

const config = {
  statusCodes: {
    "401": { // if the statusCode is 401
      "redirect": "/login" // redirect to /login page/endpoint
    },
    "403": { // if the statusCode is 403
      "redirect": function (request) {
        return "/login?redirect=" + request.url.path
      }
    }
  }
}
(async () => {
  await server.register({
      plugin: require('hapi-error'),
      options: config // pass in your redirect configuration in options
    });
  await server.register(require('vision'));
})();

This in both cases will redirect the client/browser to the /login endpoint and will append a query parameter with the url the person was trying to visit. With the use of function instead of simple string you can further manipulate the resulted url. Should the parameter be a function and return false it will be ignored.

e.g: GET /admin --> 401 unauthorized --> redirect to /login?redirect=/admin

Redirect Example: /redirect_server_example.js

That's it!

Want more...? ask!

Custom Error Messages using request.handleError

When you register the hapi-error plugin a useful handleError method becomes available in every request handler which allows you to (safely) "handle" any "thrown" errors using just one line of code.

Consider the following Hapi route handler code that is fetching data from a generic Database:

function handler (request, reply) {
  db.get('yourkey', function (err, data) {
    if (err) {
      return reply('error_template', { msg: 'A database error occurred'});
    } else {
      return reply('amazing_app_view', {data: data});
    }
  });
}

This can be re-written (simplified) using request.handleError method:

function handler (request, reply) {
  db.get('yourkey', function (err, data) { // much simpler, right?
    request.handleError(err, 'A database error occurred');
    return reply('amazing_app_view', {data: data});
  }); // this has *exactly* the same effect in much less code.
}

Output:

hapi-error-a-database-error-occured

Explanation:

Under the hood, request.handleError is using Hoek.assert which will assert that there is no error e.g:

Hoek.assert(!err, 'A database error occurred');

Which means that if there is an error, it will be "thrown" with the message you define in the second argument.


handleError everywhere

Need to call handleError outside of the context of the request ?

Sometimes we create handlers that perform a task outside of the context of a route/handler (e.g accessing a database or API) in this context we still want to use handleError to simplify error handling.

This is easy with hapi-error, here's an example:

var handleError = require('hapi-error').handleError;

db.get(key, function (error, result) {
  handleError(error, 'Error retrieving ' + key + ' from DB :-( ');
  return callback(err, result);
});

or in a file operation (uploading a file to AWS S3):

var handleError = require('hapi-error').handleError;

s3Bucket.upload(params, function (err, data) {
  handleError(error, 'Error retrieving ' + key + ' from DB :-( ');
  return callback(err, result);
}

Provided the handleError is called from a function/helper that is being run by a Hapi server any errors will be intercepted and logged and displayed (nicely) to people using your app.

custom data in error pages

Want/need to pass some more/custom data to display in your error_template view?

All you have to do is pass an object to request.handleError with an errorMessage property and any other template properties you want!

For example:

request.handleError(!error, {errorMessage: 'Oops - there has been an error',
email: 'example@mail.co', color:'blue'});

You will then be able to use {{email}} and {{color}} in your error_template.html

logging

As with all hapi apps/APIs the recommended approach to logging is to use good

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.

e.g:
hapi-error-log

Debugging

If you need more debugging in your error template, hapi-error exposes several useful properties which you can use.

{
  "method":"GET",
  "url":"/your-endpoint",
  "headers":{
    "authorization":"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzLCJlbWFpbCI6ImhhaUBtYWlsLm1lIiwiaWF0IjoxNDc1Njc0MDQ2fQ.Xc6nCPQW4ZSf9jnIIs8wYsM4bGtvpe8peAxp6rq4y0g",
    "user-agent":"shot",
    "host":"http://yourserver:3001"
  },
  "info":{
    "received":1475674046045,
    "responded":0,
    "remoteAddress":"127.0.0.1",
    "remotePort":"",
    "referrer":"",
    "host":"http://yourserver:3001",
    "acceptEncoding":"identity",
    "hostname":"http://yourserver:3001"
  },
  "auth":{
    "isAuthenticated":true,
    "credentials":{
       "id":123,
       "email":"hai@mail.me",
       "iat":1475674046
    },
    "strategy":"jwt",
    "mode":"required",
    "error":null,
    "token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTIzLCJlbWFpbCI6ImhhaUBtYWlsLm1lIiwiaWF0IjoxNDc1Njc0MDQ2fQ.Xc6nCPQW4ZSf9jnIIs8wYsM4bGtvpe8peAxp6rq4y0g"
  },
  "email":"hai@mail.me",
  "payload":null,
  "response":{
    "statusCode":500,
    "error":"Internal Server Error",
    "message":"An internal server error occurred"
  }
}

All the properties which are logged by hapi-error are available in your error template.

Are Query Parameters Preserved?

Yes! e.g: if the original url is /admin?sort=desc the redirect url will be: /login?redirect=/admin?sort=desc Such that after the person has logged in they will be re-directed back to to /admin?sort=desc as desired.

And it's valid to have multiple question marks in the URL see: http://stackoverflow.com/questions/2924160/is-it-valid-to-have-more-than-one-question-mark-in-a-url so the query is preserved and can be used to send the person to the exact url they requested after they have successfully logged in.


Under the Hood (Implementation Detail):

When there is an error in the request/response cycle, the Hapi request Object has useful error object we can use.

Try logging the request.response in one of your Hapi route handlers:

console.log(request.response);

A typical Boom error has the format:

{ [Error: 500]
  isBoom: true,
  isServer: true,
  data: null,
  output:
   { statusCode: 500,
     payload:
      { statusCode: 500,
        error: 'Internal Server Error',
        message: 'An internal server error occurred' },
     headers: {} },
  reformat: [Function] }

The way to intercept this error is with a plugin that gets invoked before the response is returned to the client.

See: lib/index.js for details on how the plugin is implemented.

If you have any questions, just ask!

Background Reading & Research

https://nodei.co/npm/hapi-error.png?downloads=true&downloadRank=true&stars=true HitCount