Description
Summary
hapi v21.0.0 is a medium-sized release focused on modernization and miscellaneous API improvements. All modules in the hapi.js ecosystem have been updated to officially support Node.js v18, be compatible with ESM projects, and drop support for Node.js v12. Plugins in the hapi.js ecosystem now support hapi v20+.
- Upgrade time: medium - none to a couple of hours for many users, but could be more impactful for those running Node.js v12.
- Complexity: low - requires following the list of changes to verify their impact.
- Risk: low - small number of changes with simple ways to retain previous behavior in most cases.
- Dependencies: medium - most existing plugins will work as-is, but upgrading from hapi v19 or Node.js v12 may require care.
Breaking Changes
- Only support Node.js v14 LTS or newer (Drop node v12 support #4346, Upgrade production deps for hapi v21 and node v18 #4361, Drop node v12 support #4279).
- Remove support for JSONP via the
route.options.jsonp
option (Remove JSONP support #4271, Remove JSONP support #4270). - Only support native Node.js
Stream.Readable
interfaces for stream responses (Only support native ReadableStream streams #4293, Enforce that response streams are always Stream.Readable #4349). - Ignore return value from
prepare()
method provided torequest.generateResponse()
(Ignore return value from generateResponse() prepare method #4348, Remove return value from generateResponse() prepare method #4299). - Change the default CORS preflight status code from
204
to200
(Change default CORS preflight status code w/ configuration #4351, Proposal to add route option for CORS preflight status code #4165). - Change the
x-xss-protection
header default to'0'
when security headers are enabled (Change XSS header default to '0' #4352, x-xss-protection should default to 0 (not 1; mode=block) #4327). - Update the default server host to be IPv6-friendly (
'::'
if IPv6 is available, otherwise'0.0.0.0'
), inheriting from Node.js defaults (Change default host to be IPv6-friendly #4357). - No longer wait for server
'start'
and'stop'
event listeners to complete asynchronously during server start and stop (Inconsistent event emits #4184, Upgrade production deps for hapi v21 and node v18 #4361). - Server events interfaces (
server.events
) have been updated in line with changes in podium v5, notably the removal ofregisterPodium()
, no longer waiting for async listeners onemit()
, and the addition ofgauge()
to wait for async listeners (Upgrade production deps for hapi v21 and node v18 #4361, Inconsistent event emits #4184). - Allow matching prereleases when validating plugin version requirements (Allow matching prereleases when validating plugin version requirements #4366).
Ecosystem-wide
- Plugins throughout the ecosystem (inert, vision, h2o2, jwt, log, cookie, scooter, etc.) now support hapi v20+, dropping support for hapi v19 and below.
- Catbox caching engines (catbox-memory, catbox-redis, catbox-memcached, catbox-object) now export the engine as a property
Engine
rather than as the default export, in order to work well for ESM users. E.g.require('catbox-redis')
becomesrequire('catbox-redis').Engine
.
New Features
- Support Node.js v18 (Upgrade production deps for hapi v21 and node v18 #4361).
- Make default error available in validation
failAction
function (Make default error available in validation failAction #4350, failAction: detailedError to log, defaultError to response #4040, failAction: detailedError to log, defaultError to response #4229). - Allow customizing the CORS preflight status code independently from
emptyStatusCode
usingroute.options.cors.preflightStatusCode
(Change default CORS preflight status code w/ configuration #4351, Proposal to add route option for CORS preflight status code #4165). - Track event loop utilization on
server.load
(Add documentation for server.options.load.maxEventLoopUtilization #4387, Add event loop utilization support heavy#48).
Ecosystem-wide
- All modules in the ecosystem support Node.js v18.
- All modules in the ecosystem can be used in ESM projects.
- In lab v25 you may write your tests in ESM.
- In glue v9 you may specify paths to ESM plugins or cache engines.
Bug fixes
- Misuse of
prepare()
method provided torequest.generateResponse()
, causingclose()
not to be called, is no longer possible (Ignore return value from generateResponse() prepare method #4348, Remove return value from generateResponse() prepare method #4299).
Migration Checklist
TLDR
While a more extensive migration guide exists below, the vast majority of hapi users should be looking for the following:
- Ensure you're using Node.js v14 LTS or above.
- If you use a hapi catbox engine such as catbox-redis, you may optionally choose to update it to the latest version. If you do so, update any references to it to reflect that the engine is now exported as
Engine
. E.g.require('@hapi/catbox-redis')
becomesrequire('@hapi/catbox-redis').Engine
. - If you use
route.options.jsonp
, remove this option and instead setroute.options.cors
totrue
. You should update any clients that utilize JSONP to instead make a regular AJAX/fetch()
requests. - Ensure that any stream responses that you serve are an instance of Node.js's
Stream.Readable
. You can easily check by exercising the route, and seeing if hapi serves a 500 with a message indicating there's an issue. A common fix is to pipe your non-standard stream through Node.js'sStream.PassThrough
, e.g.nonStandardStream.pipe(new Stream.PassThrough())
. - If you use
emit()
asynchronously, e.g.await server.events.emit()
, then switch it to use the newgauge()
method, e.g.await server.events.gauge()
with the same arguments. - If you have any listeners to the server's
'start'
or'stop'
events that are asynchronous, switch these event listeners to a server lifecycle extension,server.ext('onPreStart', ...)
orserver.ext('onPostStop', ...)
respectively.
Node.js version
Make sure your Node.js version is v14 LTS or newer. This release uses language features that are only available in Node.js v14.15.0 or higher and will not start on older versions. Node.js v18 is the current LTS and is the recommended version.
Default server host aligned with Node.js defaults
When creating a server with Hapi.server()
, if you do not specify server.options.host
then hapi used to default to 0.0.0.0
, i.e. all available IPv4 network interfaces. This did not jibe with IPv6 or dual-stack hosts, particularly since as of Node.js v17, the dns module would resolve a request to localhost
to IPv6 network interfaces when available. So hapi now defaults server.options.host
to ::1
on IPv6-compatible machines, and otherwise to 0.0.0.0
, in alignment with Node.js's bare HTTP server.
Checklist:
- To restore the previous behavior, set
server.options.host
to0.0.0.0
, e.g.Hapi.server({ host: '0.0.0.0' })
.
Catbox engines are now exported as Engine
In order to be more ESM-friendly, the catbox engines (catbox-memory, catbox-redis, catbox-memcached, catbox-object) now export the engine as a property Engine
rather than as the default export. There have also been changes to catbox-memcached's engine options.
Checklist:
- Optionally update to the latest version of your catbox engine.
- If you do so, update any references to catbox engines to reflect the new
Engine
export. For example:const CatboxRedis = require('@hapi/catbox-redis'); -const engine = new CatboxRedis(); +const engine = new CatboxRedis.Engine(); const server = Hapi.server({ cache: { engine } });
- If you use catbox-memcached, note that the engine's
location
option has been renamed toserver
, and it takes a new format following from memcache-client's configuration.
Remove support for JSONP
JSONP (or, "JSON with Padding") was an approach to circumvent the browser same-origin policy when making AJAX requests for data. With the advent and broad client support for CORS, JSONP no longer belongs in hapi core, so the route.options.jsonp
option has been removed.
Checklist:
- Identify any routes using
route.options.jsonp
and remove the option. - You may consider replacing
route.options.jsonp
withroute.options.cors
. Settingroute.options.cors
totrue
on a route will allow cross-origin requests from any domain, similar to JSONP. You would followup by updating clients to use a standard AJAX request rather than the JSONP approach of appending a script tag to the page.
Support only standard Stream.Readable
implementations for responses
Over Node.js's lifetime, we have seen several implementations of streams, and it used to be more common for libraries to implement their own non-standard stream interfaces. The Node.js ecosystem has settled down in this regard as of Node.js v14, and this has allowed hapi to remove code catering to non-standard implementations.
Checklist:
- Identify any route handlers that respond with a stream. Lifecycle handlers that takeover the response with a stream are also affected.
- Ensure that the stream object used for the response is an instance of Node.js's
Stream.Readable
. - You may check this by exercising the route. If the response is not a stream, the route will serve a 500 response, and the internal message will read:
Cannot reply with a stream-like object that is not an instance of Stream.Readable
. If the server serves the stream successfully, then you do not need to make any changes. - In any cases where you are responding with a stream that is not a
Stream.Readable
, determine the source of the non-standard stream implementation that is being used. For example, the stream may come from an outdated version of a dependency that works with an old version of streams. - If you can upgrade the dependency producing the non-standard stream implementation, consider doing so.
- As a last resort, often you can convert a non-standard readable stream into a
Stream.Readable
by piping it through Node.js'sStream.PassThrough
:-return h.response(nonStandardStream); +const standardStream = nonStandardStream.pipe(new Stream.PassThrough()) +return h.response(standardStream);
Updated server.events
interface
The server.events
interface comes from the podium package, which has been upgrade to v5 with some breaking changes. Notably, async usage of emit()
should be replaced with await gauge()
.
Checklist:
- Identify any asynchronous usage of emit, e.g.
await server.events.emit()
, and replace it with the new gauge method:await server.events.gauge()
. Synchronous usage of emit, e.g.server.events.emit()
, does not need to be updated. - Any usage of
server.events.registerPodium()
should be removed. The events of any podiums registered toserver.events
should instead be registered directly toserver.events
usingserver.event()
.
Server lifecycle no longer waits on 'start'
and 'stop'
event handlers
When calling await server.start()
and await server.stop()
, hapi previously waited for all asynchronous event handlers for 'start'
and 'stop'
to complete. These event handlers no longer delay server start and stop.
Checklist:
- Identify any asynchronous event handlers used with
server.events.on('start', ...)
andserver.events.on('stop', ...)
. - If
await server.start()
should wait on any of the async'start'
event handlers, switch the event handler to a server lifecycle extension viaserver.ext('onPreStart', ...)
orserver.ext('onPostStart', ...)
. - If
await server.stop()
should wait on any of the async'stop'
event handlers, switch the event handler to a server lifecycle extension viaserver.ext('onPreStop', ...)
orserver.ext('onPostStop', ...)
.
Change default x-xss-protection
header to 0
The route.options.security.xss
option governs the x-xss-protection
security header. In the past this option has been true
or false
, defaulting to true
which resulted in a header value of 1; mode=block
. This is no longer standard practice, and is recommended against by OWASP. The option now has been expanded to take values 'disable'
, 'enable'
, or false
. When set to the default value of 'disable'
, it results in a header value of 0
.
Checklist:
- To restore the previous default behavior, you may set
route.options.security.xss
to'enable'
. - If you explicitly set
route.options.security.xss
totrue
, instead set it to'disable'
or'enable'
. It's recommended to switch to the new default of'disable'
.
Default status code for CORS preflight requests has changed
In past versions of hapi, successful CORS preflight requests have been served with the status code determined by route.options.response.emptyStatusCode
, which is 204
by default. Despite the fact that these responses are indeed empty, it turns out to be compatible with a wider variety of clients to serve a 200
, so in the this version of hapi we have switched the status code of CORS preflights to 200
.
Checklist:
- To restore the previous default behavior, you may set the new option
route.options.cors.preflightStatusCode
to200
.
Custom response varieties ignore return value of prepare()
If you create a custom response variety using request.generateResponse()
, it has been an implicit requirement that prepare(response)
return response
. If you returned a different response object, then response
would be orphaned and it would not be closed, resulting in potential resource leaks. If you returned nothing, it would lead to a runtime error. In order to avoid this, hapi now ignores the return value of prepare()
.
Checklist:
- If you use
request.generateResponse()
withprepare()
, then it's suggested that your implementation ofprepare()
no longer return any value. Instead, simply mutate theresponse
passed into the method.