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.
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:
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
- Support for Hapi.js v20
- Not backward compatible with Hapi.js < v18
- 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
Error handling in 3 easy steps:
plugin from NPM:
1. Install thenpm install hapi-error --save
2. Include the plugin in your Hapi project
Include the plugin when you register
your server
:
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 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 usingVision
(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
statusCodes
config object to transform messages or redirect for certain status codes
4. Optional Add 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.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
That's it!
Want more...? ask!
request.handleError
Custom Error Messages using 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:
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 therequest
?
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.
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: 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.
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
- Writing useful / friendly error messages: https://medium.com/@thomasfuchs/how-to-write-an-error-message-883718173322