<div align="center">

# `hapi-error`

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

![dilbert-404-error](https://cloud.githubusercontent.com/assets/194400/17856406/53feeee4-6875-11e6-8480-d493906f6aa1.png)

[![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)
[![Build Status](https://img.shields.io/travis/dwyl/hapi-error/master.svg?style=flat-square)](https://travis-ci.org/dwyl/hapi-error)
[![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)
[![HAPI 20.2.x](https://img.shields.io/badge/hapi-18.4.0-brightgreen.svg?style=flat-square "Latest Hapi.js")](https://hapijs.com)
[![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/)
[![Dependencies Status](https://david-dm.org/dwyl/hapi-error/status.svg?style=flat-square)](https://david-dm.org/dwyl/hapi-error)
[![devDependencies Status](https://david-dm.org/dwyl/hapi-error/dev-status.svg?style=flat-square)](https://david-dm.org/dwyl/hapi-error?type=dev)
[![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat-square)](https://github.com/dwyl/hapi-error/issues)
[![HitCount](https://hits.dwyl.io/dwyl/hapi-error.svg)](https://hits.dwyl.io/dwyl/hapi-error)
[![npm package version](https://img.shields.io/npm/v/hapi-error.svg?style=flat-square)](https://www.npmjs.com/package/hapi-error)


</div>

## *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_](https://en.wikipedia.org/wiki/Abandonment_rate)!
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. :heart:

> _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_](https://uxmas.com/2012/the-4-hs-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_](https://en.wikipedia.org/wiki/Phrases_from_The_Hitchhiker%27s_Guide_to_the_Galaxy#Don.27t_Panic).

> Try it: https://hapi-error.herokuapp.com/panacea


Under the hood, Hapi uses
[`Boom`](https://github.com/dwyl/learn-hapi#error-handling-with-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](https://cloud.githubusercontent.com/assets/194400/14770263/06bdc6dc-0a65-11e6-9f9b-80944711a4f1.png)

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

![hapi-login-401-error](https://cloud.githubusercontent.com/assets/194400/14770276/57022f20-0a65-11e6-86de-d9b8e456b344.png)

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

![localhost-500-error](https://cloud.githubusercontent.com/assets/194400/14770517/98a4b6d6-0a6b-11e6-8448-4b66e3df9a9a.png)

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](https://cloud.githubusercontent.com/assets/194400/15275274/ef9e5402-1abe-11e6-9313-71b11c61f032.png)

> ***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.

## *v3.0.0 Changes*
1. Support for Hapi.js v20
2. Not backward compatible with Hapi.js < v18
3. Requires NodeJS v14 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**](https://github.com/dwyl/learn-hapi)

Error handling in 3 *easy* steps:

### 1. Install the [plugin](https://www.npmjs.com/package/hapi-error) from NPM:

```sh
npm install hapi-error --save
```

### 2. Include the plugin in your Hapi project

Include the plugin when you `register` your `server`:

```js
var Hapi = require('@hapi/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](https://github.com/dwyl/hapi-error/blob/master/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:

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

> Note: `hapi-error` plugin *expects* you are using [`Vision`](https://github.com/hapijs/vision) (*the standard view rendering library for Hapi apps*)
which allows you to use Handlebars, Jade, [**Riot**](https://github.com/dwyl/hapi-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`](https://github.com/dwyl/hapi-error/blob/master/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](https://github.com/dwyl/hapi-error/issues).

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.
```js
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`:

```js
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.pathname
      }
    }
  }
}
(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](https://github.com/dwyl/hapi-error/blob/master/test/redirect_server_example.js)


## *That's it*!

*Want more...?* [*ask*!](https://github.com/dwyl/hapi-error/issues)

## *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:

```js
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:

```js
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](https://cloud.githubusercontent.com/assets/194400/19078231/590d2d80-8a47-11e6-82e2-742d193b43b9.png)

#### 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*.

<br />

### `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:

```js
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_):

```js
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:  
```js
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`](https://github.com/dwyl/learn-hapi#logging-with-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](https://cloud.githubusercontent.com/assets/194400/19013932/f2471060-87d6-11e6-980a-d7210c9fea7e.png)

### Debugging

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

```js
{
  "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:
https://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.

<br />

### 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:

```js
console.log(request.response);
```
A typical `Boom` error has the format:
```js
{ [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](https://github.com/dwyl/hapi-error/blob/master/lib/index.js)
for details on how the plugin is implemented.

If you have _any_ questions, just [*ask*!](https://github.com/dwyl/hapi-error/issues)


## Background Reading & Research

+ Writing *useful* / *friendly* error messages:
https://medium.com/@thomasfuchs/how-to-write-an-error-message-883718173322
