hapi-error
Version:
catch errors in your hapi application and display the appropriate error message/page
459 lines (347 loc) • 16.3 kB
Markdown
<div align="center">
# `hapi-error`
Intercept errors in your Hapi web app/api
and send a *useful* message to the client.

[](https://snyk.io/test/github/dwyl/hapi-error?targetFile=package.json)
[](https://travis-ci.org/dwyl/hapi-error)
[](https://codecov.io/github/dwyl/hapi-error?branch=master)
[](https://hapijs.com)
[](https://nodejs.org/download/)
[](https://david-dm.org/dwyl/hapi-error)
[](https://david-dm.org/dwyl/hapi-error?type=dev)
[](https://github.com/dwyl/hapi-error/issues)
[](https://hits.dwyl.io/dwyl/hapi-error)
[](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:

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

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

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

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

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

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