Migrating from Hapi v14 to v18
About Hapi
Hapi (pronounced “happy”) or HapiJS is a framework for building web applications, APIs and services. It’s extremely simple to get started with, and extremely powerful at the same time. We use Hapi in our kubernetes cluster for our api-gateway microservice and are quite satisfied with it. It’s elegant and more extensive than node.js’ express, and similar to python’s flask.
The drive to upgrade Hapi
Security fixes, lack of proper documentation for Hapi v14 (the v18 documentation is not usable, because APIs were changed) and general need to use latest modules & libraries, which are not compatible with Hapi v14 made upgrading Hapi attractive and needed.
Unfortunately Hapi framework broke its backward compatibility somewhere between versions 14 and versions 17 and we could not simply upgrade Hapi’s version or anywhere that simple. This article will help those who want to follow the upgrade path, without being experts in all the changes in between.
This is a technical article, intended to help developers facing same task of upgrading from ancient version of Hapi into modern versions
Getting started
Update your companion libraries
First off, we updated the ‘require’ statements and package.json version numbers. Bottom lines:
You should upgrade to using Joi 15.1.1; (Joi 16.x.x causes errors, see here: https://github.com/hapijs/joi/issues/2107). If you get an error for using example() function on arrays, you might need to change “.example([…])” to “.example([[…]])”
We also executed “npm audit fix” to update libraries with security issues
Libraries have been renamed. This code is for Hapi v18 and related libraries:
// Renamed:
const Hapi = require('@hapi/hapi')
const Inert = require('@hapi/inert')
const Vision = require('@hapi/vision')
const H2o2 = require('@hapi/h2o2')
const Joi = require('@hapi/joi')
const Boom = require('@hapi/boom')// hapi-swagger, however, were not renamed
const HapiSwagger = require('hapi-swagger')
Authentication with JWT
We use JWT tokens for authentication and authorization in our api-gateway, and with Hapi 14 we implemented our own JWT validation code, integrated with scopes validation (comparing the required scopes with the scopes allowed to the requesting user)
With Hapi v18, there’s a JWT validator plugin called hapi-auth-jwt2 which does the work for you (well most of it…). The JWT token validation is performed by the plugin, but the scopes permission system is entirely our own. Luckily the hapi-auth-jwt2 has a callback where you can implement your custom validation code. That’s where we put our scopes validation code. Hapi can validate scopes for you, if you make sure to put the user’s permitted scopes (listed in the JWT token) into the credentials section, like so:
const validate = async function (decoded, request) {
let permittedScopes = decoded.scopes || decoded.scope.split(' ')
if (!Array.isArray(permittedScopes)) return { isValid: false, errorMessage: 'Invalid scopes in token' }
console.log(
'decoded:', decoded,
'permitted:', permittedScopes
)
return {
isValid: true,
credentials: {
scope: permittedScopes, // this will cause hapi to validate scopes
correlationId: Genid(),
session: {
user_id: decoded.user_id,
username: decoded.username
}
}
}
}“isValid: false” means authorization fails, but “isValid: true” is not an authorization success; rather, it means ‘proceed with other authorization code’ (such as validating scopes)
request.auth.artifacts vs request.auth.credentials
Hapi authentication schemes aren’t stateful but they can store the important authentication data in request.auth so it can be accessed by other functions at a later time. On request.auth you have access to the following properties:
- credentials — Things that identify or represent the unique user
- artifacts — Optional authentication-related data that isn’t credentials
However hapi-auth-jwt2 owns the ‘request.auth.artifacts’, where we used to put our authentication-driven information, such as user ID, or session info. With hapi-auth-jwt2 ‘request.auth.artifacts’ always contains exactly the JWT token. Our solution was to use ‘request.auth.credentials’ as can be seen from the code snippet above.
The route ‘auth’ key
The ‘auth’ key of the routes can no longer contain custom structures. it contains only a string indicating which authentication scheme to use, or false (public API). In Hapi v14 we had additional authorization information added in that key:
{
auth: {
scope: [ 'Admin', 'Self' ] // requires user has either 'Admin' or 'Self'
},
config: ...
}Hapi 18 defines the ‘auth’ object structure slightly different. The equivalent would be:
{
auth: {
access: {
scope: [ 'Admin', 'Self' ] // requires user has either 'Admin' or 'Self'
}
},
options: ... // 'config' was renamed
}The route ‘config’ key
This was is renamed to ‘options’, but ‘config’ and ‘options’ can still be used in Hapi 18
Migrating the handler function
The route’s handler function must return a value
Hapijs 18 expects a return value. Where in Hapi v14 it is enough to call reply(…), but handler return value was ignored, in Hapi v18 the handler’s return value is expected and used as the basis to generate the HTTP response. This value can be a constant, or a promise generated via h.response(). A third option exists: throw an exception usually generated via Boom.xxx functions.
If using h.response() it’s not enough just to call it. Its return value must be passed on as the return value of the entire handler function.
If you executed reply() from a callback, you’ll have to use Promises. For example this Hapi v14 code:
handler: function(request, reply) { setTimeout(function () { reply("something") }, 3000) }Should be converted to a Promise which resolves to h.response(…):
handler: function(request, reply) { return new Promise( (resolve, reject) => { setTimeout(function () { resolve(h.response("something")) }, 3000) } } }Deeper on second parameter of the handler function: ‘h’ vs. ‘reply’
Handler function in Hapi v14: function(request, reply), while in Hapi v18 it’s: function(request, h). The difference in second parameter is not just the name, but the value of that parameter is different. While the function reply was renamed to ‘h.response’, its not enough. Inside the handler’s code, you will need to do ‘let reply = x => h.response(x)’ in some cases.
The h.response(…) function of Hapi v18 expects ‘this’ to be ‘h’ (second parameter of handler). If you are passing reply to callbacks or other subordinate functions, it’s NOT equivalent just to pass on ‘h.response’ now. Here’s why:
// Load node.js:
$ node
> > h = { req: {}, response: function() { console.log(this) } } // simulate 'h' and 'h.response'
function { req: {}, response: [Function: response] } > h.response() { req: {}, response: [Function: response] } // this points to h itself - correct!
undefined > function execute(reply, req) { return reply('error') }
> HttpClient = { execute: execute } // simulate HttpClient.execute
> HttpClient.execute( h.response, 'some' ) Object [global] { ... /*lot of stuff printed here */ } // This is the result of console.log(this) from h.response()
undefined
// Observe: while calling 'response', 'this' points to the global scope - wrong! > HttpClient.execute( x => h.response(x), 'some' ) { req: {}, response: [Function: response] } // This is the result of console.log(this) from h.response()
undefined // Observe: while calling 'response', 'this' points to 'h' - correct!!
More information on ‘this’ here: Javascript ‘this’ Keyword, How it works?
Accessing the request body when using validation
Whereas in Hapi v14 you’d simply do ‘request.payload’ to get the payload, in Hapi v18 it depends on how you define the validation:
If you define as one big object:
validate: Joi.object({ param1: ..., param2: ... })Then `request.payload.value` should be used inside the handler function rather than just ‘request.payload’. ‘payload.error will contain an error if payload cannot be parsed as a JSON object. On the other hand, if your validation is a simple object
validate: { param1: ..., param2: ... }Then you should continue to use ‘request.payload’ in that handler function
Hapi v14 would put the parameters in request.payload in both cases, but Hapi v18 would treat these differently
Summary
Hapi is fun and stable, and generally recommended if you are building an api-gateway. The documentation on Hapi v14 is lacking, probably because it’s ancient… I hope you find this article useful in helping to keep your Hapi up to date
References
- Hapi 18 documentation: https://hapi.dev/api/?v=18.4.0
- Hapi 14 documentation: I could not find it… More incentive to move forward…
- Getting started with Hapi 17
Originally published at https://www.linkedin.com.
