The hapi v17 release represents the next generation of node frameworks as the first enterprise-grade framework that is fully async/await end-to-end. It combines the latest technologies with a proven core that has been powering some of the world largest sites.
This release would not have been possible without the generous financial support of the following Featured Sponsors who have gone above and beyond to make this work successful and sustainable. I am extremely grateful for their support.
#

#

#

#
This release effort required a significant investment of time and resources, surpassing $40,000 (and counting). If you or your employer has benefited from my work on hapi, especially if you are reading these notes in anticipation of migrating your applications to use the new v17 release, please consider supporting my work. If you would like to have your company included in the v17 materials and promotions, please consider becoming a Featured Sponsor.

hapi v17.0.0 is a major new version of the framework. It is among the top three major rewrites of the entire factor (after v2.0.0 and v8.0.0). In many ways, it is a new framework because it make fundamental changes to how business logic is interfaced with the framework.
The main change and the motivation for this release is replacing callbacks with a fully async/await interface. This is not merely an external, cosmetic change, but a deep refactor of the entire codebase, including most of the dependencies. With a handful of exceptions, there are no callbacks or closures used within the core module.
At the heart of the v17 release is the replacement of the reply interface (e.g. the reply() method passed to handlers, extensions, and authentication methods) with the new lifecycle method concept. The other major changes are the removal of multi-connections support and domain protection.
Note: hapi v17 requires node v8 and assumes a high proficiency with recent changes to JS. These notes and the hapi documentation do not go into any details about using async/await and promises as well as other topics such as symbols, sets, default values, etc. It is critical to have a strong understanding of the new flow controls introduced by async/await and the patterns around them before attempting to migrate to this new version.
Due to the nature of this release and the scope of changes, these release notes may be missing some details. It is recommended to read the full API documentation and please post any missing information in the comments so that we can keep this up to date.
async functions:server.auth.test()server.cache.provision()server.emit()server.initialize()server.inject()server.register()server.start()server.stop()register()reply interface argumentafter argument of server.dependency()generateFunc optionautoValue option of server.state() server.connection() method is replaced with options passed directly when creating a server object.connection property is removed from all objects.select() methods and options.onPreResponse, jump to response validation first.takeover() in handler will just to response validation (before it was ignored).onCredentials request extension point and a new request.auth.isAuthorized property. If a request failed access validation, the request.auth.isAuthenticated will be true in response validation and onPreResponse (previsouly was false).reply() interface with a new lifecycle methods interface:response.hold() and response.resume().async and the required return value is the response.h) is provided with helpers (instead of the reply() decorations).async/await, most exceptions thrown are caught by the internal mechanism.async/await promises chain are no longer handled and will cause the process to exit if the application doesn't handle it explicitly.1024 bytes.compression.minBytes option.request.id to request.info.id.request.getLogs() method, replaced with direct access via request.logs.request.logs are collected only if the route log.collect is set to true (false by default).'request', 'request-internal', and 'request-error' into a single event and added channels support.event.internal flag with event.channel.event.error is provided instead of event.data.async/await and block option removed.events property:server.eventsrequest.eventsresponse.eventsfailAction argument source into the error passed instead of a separate argument.server.auth.strategy().server.auth.default().server.handler() to use server.decorate() instead.'reply' decorations now use the new 'toolkit' decorations.server.table() return value.failAction to expose the information needed.server argument from 'route' event.autoValue methods are no longer executed in parallel.config to options (config still acceptable but deprecated).route.options.pre response as well as server.inject() response instead of casting to null.Boom.create() and Boom.wrap().failAction features - all failAction options now accept functions.onCredentials extension point and the ability to change the request credentials before authorization is validated.flush() method to response streams for better Server Sent Events support.h.context in addition to this to better support arrow functions.route.options.cors.origin can be set to 'ignore' which provides a CDN-friendly mode that ignores Origin headers and always responds with 'Access-Control-Allow-Origin' set to '*'.Any function that previously accepted a callback (either via callback or next) now returns a promise instead. With the exception of methods with a reply() interface (see lifecycle methods section below), all other methods remain the same and should be called with the await keyword.
For example:
server.start((err) => {
if (err) {
console.log(err);
}
});
Is now:
try {
await server.start();
}
catch (err) {
console.log(err);
}
Checklist:
await:server.auth.test()server.cache.provision()server.emit()server.initialize()server.inject()server.register()server.start()server.stop()register()after argument of server.dependency()generateFunc optionautoValue option of server.state() The server no longer supports more than one connection. All the options previously supported by server.connection() are now merged with the server object constructor. If your server calls server.connection() more than once, you will need to create another server object.
There are no simple instructions for implementing multiple connections with v17 because the needs vary too much. In its simplest form, you can just replicate the code that creates one server and create two, using different connection information for each. If you use labels to select connections within plugins, just register the plugins you want with the matching server.
If you need to share state between the different connections, consider using a shared server.app object or using a singleton pattern between the multiple servers.
Checklist:
server.connection() call with a server instance configured with the options passed to both the server and connection.server.select() to only register it against the desired servers.labels option.connection and replace with server (e.g. request.connection).connections: false plugin option since it is no longer applicable. Plugins cannot set up connections since the connection is configured during server construction.A lifecycle method is an async function using the signature async function(request, h, [err]) and is used by pretty much every method passed to the framework to execute when processing incoming requests. This includes handlers, request extensions, failAction methods, pre-handlers, and authentication scheme methods.
With the move to async/await, the old reply() interface was no longer applicable as it was in practice a callback with a lot of special handling rules. Instead, the new lifecycle method is a much simpler interface. To set a new response, simply return that value. To set a new response and jump to response validation, use the takeover() response decorator. To continue execution without setting a response, return h.continue. The full list of options it listed in the API documentation.
For example:
// Before
const handler = function (request, reply) {
return reply('ok');
};
// After
const handler = function (request, h) {
return 'ok';
};
Checklist:
reply argument. If your code uses the same argument names as the hapi convention, searching for (request, reply) is an easy shortcut to find most of the method you need to migrate.Remove response.hold() and response.resume() and replace with an async function or return a promise.
In general:
h.response() helper to wrap a plain response in a response object to access the takeover() decorator.async for asynchronous operations.In request extensions:
h.continue instead of reply.continue() to continue without changing the response.h.response(result).takeover() to override the response and skip to validation instead of reply(result).takeover().reply.continue(result) in extension points after the handler.In authentication authenticate():
h.authenticated() or h.unauthenticated() for success and failure.If a route is configured with authentication and access rules (scope, entity) and the access validation fails, the request request.auth.isAuthenticated will be true (it was false in previous versions). This only matters if you check the flag in the onPreResponse step. If you do, check for request.auth.isAuthenticated && request.auth.isAuthorized instead for the same result.
Note that errors and takeover responses now jump to the response validation step instead of directly to onPreResponse. If you have response validation configured, ensure it can handle these error and takeover responses or the validation will fail with a 500 error.
Look for takeover() in handlers as it will now cause it to jump directly to response validation, skipping the onPostHandler step.
In order to simplify and optimize logging, the request, response, and server emitters have been moved to use the events property instead of inheritance. In the case of the request and response emitters, if you never access them, they are not initialized, saving resources.
The three request event types ( 'request', 'request-internal', and 'request-error' ) have been merged into a single 'request' event. In addition, only internal error logging are emitted and collected.
Checklist:
server.on() with server.events.on().request.on() with request.events.on().response.on() with response.events.on().onRequest and onPreResponse or the 'response' event) to manually log the information you desire.request.getLogs() and replaced them with direct access to request.logs. You will also need to configure the route to collect logs by setting the route log.collect option set to true (false by default).'request' -> { name: 'request', channels: 'app' }'request-internal' -> { name: 'request', channels: 'internal' }'request-error' -> { name: 'request', channels: 'error' } (note that the listener signature is different and that it will pass a full event object instead of the previous err which can be accessed now via event.error).event.internal argument with event.channel (and check the value is 'internal' for the same result).event.error is provided instead of event.data.block option, remove it and convert your listener to an async function.Previous versions used the now deprecated node domains feature to protect application code from throwing errors synchronously or asynchronously. This has been a great feature for a long time as it captured many developer errors that made their way to production. Instead of crashing the application, a 500 error was returned and the error logged.
The problem was, domains didn't play well with promises and could instead swallow errors or produce unexpected results. In general, when an unhandled error is thrown, the server is considered to be in an unstable state. While it is better to return a 500 error than crash the process, it wasn't a perfect solution.
v17 removed domain support. It might come back in the future when node async hooks reach a stable place and an alternative solution is provided. The good news is that many of the common errors are already covered by the asyn/await error catching flow. The framework will continue to catch errors thrown synchronously as well as many of those thrown asynchronously (as long as they are thrown as part of the proper promises chain).
This is not as extensive as the domain support. Unfortunately, there isn't much you can do other than adding some global listeners.
Checklist:
In an effort to use more conventional patterns, the plugin function with object properties style has been replaced with a plain object.
Checklist:
exports.register() and the matching exports.register.attributes with exports.plugin = { register, name, version, multiple, dependencies, once, pkg }.connections attribute.All server methods must be full synchronous, an async function, or return a promise. When a server method is cached, the result no longer changes to an envelope with the result and ttl value.
Checklist:
async function.callback method option.{ value, ttl, report }, use the catbox getDecoratedValue option.Request tails are no longer supported. Lookup calls to request.tail() and any listeners to the 'tail' event and replace them with an application specific solution. The 'tail' event can be replaced with the 'response' event. You can use the request.app object to store local request state such as promises representing tail events and then use Promise.all() in the response event to wait for them to finish processing.
Search for references torequest.id and replace them with to request.info.id.
Look for handlers or pre methods that use a string as the handler and replace them with explicit calls to the server method used.
If you use the failAction option in request input or response payload validation, the function signature has changed to a lifecycle method. The previous source argument is now available as a property of the error received.
Look for calls to server.auth.strategy() and if they third argument is true or a string, replace that with an explicit call to server.auth.default().
Replace server.handler(...) with server.decorate('handler', ...).
Replace server.decorate('reply', ...) with server.decorate('toolkit', ...).
Look for calls to server.table() and adjust the code to handle the new format which no longer returns an array or an envelope with a table property. Instead the table value is the direct return value.
Input validation errors are no longer passed directly from joi to the client. Instead, a generic 400 error is returned which simply indicates which input source failed validation (e.g. 'query'). If you want to keep the original error, set a failAction validation option such as (request, h, err) => throw err. Note that unlike previous versions, the error message is on longer HTML escaped to prevent echo attacks. You must perform the applicable error string escaping to prevent exploits. In general it is best practice to never echo back the the client anything sent that could be injected with a script or other content.
Look for listeners to the 'route' event and remove the second server argument.
Ensure your clients do not rely on receiving a 400 error code when the payload is too big. Previous versions sent a mix of 400 and 413 errors based on the payload parsing rules. This will consistently set errors to 413 when the payload is too big.
According to compression best practices, there is no reason to compress payloads under 1kb in size because the payload already fits within a single packet. In that case, compression wastes CPU resources and time for no benefit. This release changes the default compression behavior of response payloads smaller than 1kb to not compress. This should work correctly for most applications. To change this and restore the previous behavior, set the server compression.minBytes option to a smaller number or to 1.
If you use the state autoValue option, note that if there are multiple cookies set, each with an autoValue options, these methods are now called in serial, not in parallel. This matters if you are making network calls which can cause the overall response time to increase. However, it is very unusual to make network calls when processing cookies for transmission. If you must, move that logic to another spot in the request lifecycle.
When lifecycle methods returned an empty string, the response was converted to null. This was changed to retain the empty string. It will have no impact on the HTTP response payload which is still empty. It will affect the value of request.pre properties ('' instead of null) as well as the value of res.result in server.inject() which will also be ''.
While at it, replace config with options when adding routes. config will still work but will go away in the future.
Boom.create() and Boom.wrap(). Use Boom.boomify() instead. You can also use new Boom() instead of Boom.internal() for a full replacement of new Error().For all of you migrating your Hapi v16 code to v17, I've found that https://github.com/mcollina/make-promises-safe helps a lot in crashing when there is an 'unhandledRejection'. Some migration mistakes are too hard to spot otherwise.
A good resource when you don't know where to start with migrating https://futurestud.io/tutorials/hapi-v17-upgrade-guide-your-move-to-async-await 🔥
I updated one of my applications yesterday (welovecoding/website#71) and it went smoothly. Thanks for providing so detailed information on the breaking changes! 👍
I have also made a basic comparison of hapi v16 and hapi v17 which might be of help:
@hueniverse I couldn't find the info in this issue, so I was wondering when 17 will be released through NPM?
It is already. latest tag is not changed yet but you can install it.
Great thanks for quick reply!
Just finished reading through this and caught a few very small problems. I wasn't sure the best place to point them out. I hope doing it here was okay.
What about good, and good-console?
@MikeBazhenov There is a issue open and somebody seems to be working on it: https://github.com/hapijs/good/issues/568
An addition– we discovered that the default server.options.debug.request value is now ['implementation'] 👍
Did anyone have to solve the lack of security and CORS headers on non-defined routes (404) when migrating to v17? Ref: https://github.com/hapijs/hapi/issues/3792
I suggest defining a catch-all route that handles 404s however you'd like. There's a short section on this in the API docs.
const Boom = require('boom');
server.route({
method: '*',
path: '/{p*}',
options: {
cors: true,
handler() {
throw Boom.notFound();
}
}
});
@devinivy I really appreciate your response, thank you!
@devinivy this solution doesnt appear to work for me, where are you placing this route?
@bmgdev Hey Bradley, you can register this route wherever you want. Do you receive an error message?
It's important that you don't have another catch-all route, like your base handler that passes requests through to a client-side framework.
Maybe this tutorial on how to handle 404 responses helps.
There's a breaking change not noted here, and that is setting a global route validation rule for params now throws for routes that do not specify any dynamic param in their path.
const server = Hapi.server({
routes: {
validate: {
params: {} // <-- cannot be specified in v17
}
}
});
AFAIK, this isn't specified in API, either.
With regards to:
'request-internal' -> { name: 'request', channel: 'internal' }
I'm getting this error:
ValidationError: Invalid event listener options {
"name": "request",
"listener": function (request, event, tags) { ... },
"channel" [1]: "internal"
}
I understand this is a typo, as using channels (with additional 's') doesn't produce that error. However, it'd suggest to fix it in the release notes (note that API reference at https://github.com/hapijs/hapi/blob/v17/API.md#server.events.request use 'channels' )
This thread has been automatically locked due to inactivity. Please open a new issue for related bugs or questions following the new issue template instructions.
Most helpful comment
A good resource when you don't know where to start with migrating https://futurestud.io/tutorials/hapi-v17-upgrade-guide-your-move-to-async-await 🔥