Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

6.0.0 Release Notes #1707

Closed
hueniverse opened this issue Jun 11, 2014 · 7 comments
Closed

6.0.0 Release Notes #1707

hueniverse opened this issue Jun 11, 2014 · 7 comments

Comments

@hueniverse
Copy link
Contributor

@hueniverse hueniverse commented Jun 11, 2014

Summary

hapi v6.0 changes how plugins, view template engines, and cache strategies are loaded. Instead of hapi trying to work around node's require() limitations (with an array of hacks and guesses), all require() activity now happens in userland code outside of hapi. This makes for slightly less pretty code but for a much more robust system and solves many edge cases that have prevented developers from using hapi effectively.

  • Upgrade time: moderate - a day or two for most users
  • Complexity: moderate - many changes but they are all easy to locate and modify
  • Risk: low to moderate - a few documented changes to authentication and initialization may cause side effects
  • Dependencies: high - requires new versions of all plugins used

Breaking Changes

  • Server
    • cache and cache.engine config no longer accept a string, only an object or function (e.g. require('catbox-redis') instead of 'catbox-redis').
    • server.view() and view config require a module, no longer supporting passing the module name string
    • app and plugins configs are no longer cloned but only shallow copied
  • Route config
    • app, plugins, and bind configs are no longer cloned but only shallow copied
    • auth config 'try' mode will not pass non-Error err responses to the client instead of treating them as errors and failing authentication. This should only affects authentication schemes using redirection response (e.g. OAuth) which would not have worked before with try. If you are using hapi-auth-cookie, the redirectTo option no longer works with try mode.
    • route auth config cannot be set to true.
    • When proxy handler passThrough is true, the 'Accept-Encoding' header is no longer removed automatically when onResponse is set. This will cause the onResponse method reading the response payload to recieve encoded data (e.g. deflate or gzip) if requested by the downstream client.
  • Pack
    • relativeTo option no longer available
    • app, plugins, and bind configs are no longer cloned but only shallow copied
    • cache and cache.engine config no longer accept a string, only an object or function (e.g. require('catbox-redis') instead of 'catbox-redis').
    • pack.list removed
    • pack.require() removed
    • pack.register() arguments changed
    • init order changed in start()
      • from: 1) after hooks, 2) servers, 3) cache
      • to: 1) cache, 2) after hooks, 3) servers
  • Plugin API
    • plugins register() function requires a new function property attributes
    • plugin.require() removed
    • plugin.register() arguments changed
    • plugin.loader() removed
    • plugin.path changed to from property to function plugin.path() which sets the path instead of reporting it
    • plugin.view() config require a module, no longer supporting passing the module name string
  • Composer
    • moved to pack.compose(manifest, options, callback)
    • uses a single step to construct a pack from a manifest
    • no longer accepts an array of packs, only a single pack
    • view template engines, catbox cache strategies, and plugins are still require()ed by compose() but relative module names are resolved against options.relativeTo
  • hapi CLI
    • all relative module names (view template engines, catbox cache strategies, and plugins) are resolved against the location of the hapi module used unless a path is provided via the -p command line option.
    • the 'manifest.json' file no longer accepts an array of packs, only a single pack

New Features

Plugin registration options

Instead of requiring every plugin to support custom options for selecting servers, prefixing route paths, or applying a virtual host settings, the modified pack.register() method supports a new registration options argument which allows setting those preferences externally. This allows loading the same plugin multiple times with different options to different subset of servers, or to customize how the plugin behaves without changing it.

For example, limiting a plugin to a specific vhost:

var server = new Hapi.Server();
server.pack.register(require('users'), { route: { vhost: 'example.com' } }, function (err) {});

Adding a path prefix to every route added by the plugin:

var server = new Hapi.Server();
server.pack.register(require('users'), { route: { prefix: '/v1' } }, function (err) {});

Selecting a subset of servers:

var pack = new Hapi.Pack();
pack.server({ labels: ['a', 'b'] });
pack.server({ labels: ['b', 'x'] });
pack.server({ labels: ['c', 'a'] });
server.pack.register(require('./users'), { select: ['a'] }, function (err) {});

Or loading the same plugin multiple times to different servers with different options:

var pack = new Hapi.Pack();
pack.server({ labels: ['a', 'b'] });
pack.server({ labels: ['b', 'x'] });
pack.server({ labels: ['c', 'a'] });

server.pack.register({
    plugin: require('./users'),
    options: { value: 4 }
}, { select: ['a'] }, function (err) {

    server.pack.register({
        plugin: require('./users'),
        options: { value: 5 }
    }, { select: ['x'] }, function (err) {});
});

Note that the new registration options are not passed to the plugin but modify internally what the plugin has access to or how it behaves.

The same features are also now available in the enhanced composer manifest format:

var manifest = {
    servers: [
        { options: { labels: ['a', 'b'] } },
        { options: { labels: ['b', 'x'] } },
        { options: { labels: ['c', 'a'] } }
    ],
    plugins: {
        './users': [
            {
                select: 'a',
                route: {
                    vhost: 'example.com'
                },
                options: {
                    value: 4
                }
            },
            {
                select: 'x',
                route: {
                    prefix: '/v1'
                },
                options: {
                    value: 5
                }
            }
        ]
    }
};

By default, plugins cannot be loaded twice into the same pack to protect the application from plugins with shared memory or other singleton state that is not safe to overlap. To enalbe loading a plugin multiple times, the plugin must set the attributes.multiple flag to true.

Third-party login plugin

Over the past year we've offered the travelogue plugin for adding third-party login capabilities (e.g. Twitter, Facebook) to a hapi application. travelogue uses the excellent passport.js login middleware for express. As you can imagine, making a comlpex express middle work with hapi was not trivial and while it works, produced a plugin that is hard to maintain and that does not enjoy the clean simplicity standards we strive for. This is in no way criticism of passport.js, only of travelogue due to the significant differences between the frameworks.

With v6.0, we are also shipping a new module called bell for third-party login. bell ships with built-in support for Facebook, GitHub, Google, Twitter, Yahoo, and Windows Live. It also supports any compliant OAuth 1.0a and 2.0 based login services with a simple configuration object.

For example, adding Twitter login to your application is as simple as:

var Hapi = require('hapi');
var server = new Hapi.Server(8000);

// Register bell with the server
server.pack.register(require('bell'), function (err) {

    server.auth.strategy('twitter', 'bell', {
        provider: 'twitter',
        password: 'cookie_encryption_password',
        clientId: 'my_twitter_client_id',
        clientSecret: 'my_twitter_client_secret'
    });

    server.route({
        method: ['GET', 'POST'],
        path: '/login',
        config: {
            auth: 'twitter',
            handler: function (request, reply) {

                // <-- Application session management logic here -->
                return reply.redirect('/home');
            }
        }
    });

    server.start();
});

Selectable plugin methods

The following methods were previously limited to the entire pack but are now available after calling plugin.select():

  • plugin.events
  • plugin.register()
  • plugin.dependency()

Additional features

  • server.location() exposes the internal logic of setting the HTTP 'Location' header. This is useful when you need access to the outgoing value of the header for other uses. For example, the OAuth protocol requires specifying the callback URI in the payload which must match the redirection instructions given by the server. This allows the bell plugin to set the correct value without replicating the internal header logic.
  • Allow setting state-specific options for failAction, strictHeader, and clearInvalid instead of just the server global config.
  • Add reply.redirect() back. Same as reply().redirect() but prettier.
  • server.auth.test() allows testing a request against a configured authentication strategy without requiring authentication or when multiple strategies are required together (each route can only be configured to successfully authenticate against one strategy, even when multiple are listed).

Migration Checklist

Server

  • If you are setting the server cache or cache.engine options to a catbox strategy name (e.g. catbox-memory, catbox-redis), you must replace that with an external require(). For example, require('catbox-redis') instead of 'catbox-redis').
  • Look for server.view() and server view config and replace the template engine module name string with require(). For example, { html: require('handlebars') } instead of { html: 'handlebars' }.
  • Look for server app or plugins configs, as well as where you use server.app and server.plugins and ensure that your application does not expect these values to be a deep copy of the provided configuration. These values were cloned (deep copy) in previous versions which was a bug. v6.0 fixes that by performing a shallow copy of the object reference only.
  • Look for server.auth.strategy() setting a default authentication mode try and verify that the scheme used will not be affected by the change where non-Error err responses are now passed back to the client instead of treating them as errors and failing authentication. This affects the 'hapi-auth-cookie' scheme as well as some 'travelogue' schemes. If you are using 'hapi-auth-cookie' with try, remove the redirectTo option or use the new route override to turn it off for individual routes using try.
  • The order in which pack components are initialized during server.start() has changed by moving cache start to the beginning of the process instead of the end. This solves a potential race condition when the server is started by the cache is not yet ready. It should not have any impact on existing code but you should review how you are using the cache at startup to confirm. For example, your code might rely on the fact that the cache is not available during plugin.after() hooks or server start event.

Route config

  • Look for routes using the app, plugins, or bind configs, as well as where you use request.route.app, request.route.plugins, and this in request handlers, and ensure that your application does not expect these values to be a deep copy of the provided configuration. These values were cloned (deep copy) in previous versions which was a bug. v6.0 fixes that by performing a shallow copy of the object reference only.
  • Look for routes using authentication mode try and verify that the scheme used will not be affected by the change where non-Error err responses are now passed back to the client instead of treating them as errors and failing authentication. This affects the 'hapi-auth-cookie' scheme as well as some 'travelogue' schemes. However, it is rare to use those schemes together with try mode as it used to fail in many cases.
  • Replace route configs with { auth: true } with { auth: 'strategy_name' } (where 'strategy_name' is the strategy you want to use).
  • Look for handlers using the proxy handler. If passThrough is true, onResponse is set, and the onResponse function reads the response payload - set the new proxy acceptEncoding config to false to remove any incoming value from the client. Otherwise, onResponse may recieve encoded data (e.g. deflate or gzip) if requested by the downstream client.

Pack

  • Look for pack configs using the app or plugins options, as well as where you use pack.app, pack.plugins, plugin.app, or plugin.plugins, and ensure that your application does not expect these values to be a deep copy of the provided configuration. These values were cloned (deep copy) in previous versions which was a bug. v6.0 fixes that by performing a shallow copy of the object reference only.
  • If you are setting the pack cache or cache.engine options to a catbox strategy name (e.g. catbox-memory, catbox-redis), you must replace that with an external require(). For example, require('catbox-redis') instead of 'catbox-redis').
  • Look for pack.list and remove it. This feature is no longer available. If you depend on it, please open an issue with your use case.
  • The order in which pack components are initialized during pack.start() has changed by moving cache start to the beginning of the process instead of the end. This solves a potential race condition when the server is started by the cache is not yet ready. It should not have any impact on existing code but you should review how you are using the cache at startup to confirm. For example, your code might rely on the fact that the cache is not available during plugin.after() hooks or server start event.
  • Remove the relativePath configuration option as it is no longer available.

Registration

Look for pack.register() calls using the second optional options argument and move that into the first plugin argument using the options key:

// Previous versions
var plugin = {
    name: 'test',
    register: function (plugin, options, next) { }
};

var options = { some: 'value' };

pack.register(plugin, options, callback)`

// v6.0
var plugin = {
    name: 'test',
    register: function (plugin, options, next) { },
    options: { some: 'value' }
};

pack.register(plugin, callback)`

Note that the second optional argument in plugin.register() is for new registration options, not for plugin options.

The pack.require() interface is no longer available and must be replaced with pack.register():

Without options:

// Previous versions
plugin.require('name', function (err) {});

// v6.0
plugin.register(require('name'), function (err) {});

With options:

// Previous versions
plugin.require('name', { some: 'options' }, function (err) {});

// v6.0
plugin.register({
    plugin: require('name'),
    options: { some: 'options' }
}, function (err) {});

If you load multiple plugins using a single pack.require() call, passing in an array of plugin name strings, you need to require() each string in the array:

// Previous versions
plugin.require(['name1', 'name2'], function (err) {});

// v6.0
plugin.register([require('name1'), require('name2')], function (err) {});

If you load multiple plugins using an object with name-options pairs you need to convert it to an array or single plugin structures:

// Previous versions
plugin.require({
    name1: { some: 'value' },
    name2: { another: 'value' }
}, function (err) {});

// v6.0
plugin.register([
    {
        plugin: require('name1'),
        options: { some: 'value' }
    },
    {
        plugin: require('name2'),
        options: { another: 'value' }
    }
], function (err) {});

Plugins

In most cases, plugins can be easily upgraded to work with hapi v6.0 while maintaining backwards compatiblity with older versions.

Export package.json

Backwards compatible change

In your plugin module, add the attributes property to the exports.register() function:

exports.register = function (plugin, options, next) {
    plugin.route({
        method: 'GET',
        path: '/',
        handler: function (request, reply) { reply('ok'); }
    });

    next()
};

exports.register.attributes = {
    pkg: require('../package.json')
};

This will export the plugin's name and version to the registration engine.

Require view engine modules directly

Backwards compatible change when not using helpers/partials

When configuring view engines, call node's require() and pass the object in the configuration instead of the module name. For example, the value { html: 'handlebars' } in previous versions must now be replaced with { html: require('handlebars') }. This change does not always work with some template engines and may cause older versions of hapi to be incompatible. You must test your plugin code against both v6.0 and v5.x to verify and may need to restrict the new plugin version to v6.x or above.

Adjust static resources paths

Backwards compatible when using absolute paths

In previous versions, hapi figured out where the plugin was loaded from and prefixed all relative paths (file, directory, and views) with that location. It also provided that location via the plugin.path property. In v6.0, all plugin paths must be absolute, relative to process.cwd(), or relative to a manually configured path using the new plugin.path(path) method. Note that since plugin.path() is not backwards compatible, if you want to keep the plugin working with previous versions, switch your path to absolute using __dirname instead of setting the path with plugin.path().

If you plugin uses views, file handlers, or directory handlers, review your configuration for relative paths. In general, the following will work for most plugins:

exports.register = function (plugin, options, next) {

    plugin.path(Path.join(__dirname, '..'));

    var views = {
        engines: { 'html': require('handlebars') },
        path: './templates'
    };

    plugin.views(views);

    plugin.route({
        path: '/file', method: 'GET', handler: { file: './static/test.html' }
    }]);

    return next();
};

exports.register.attributes = {
    pkg: require('../package.json')
};

Remove plugin.loader()

Backwards compatible when loading view engines directly

The plugin.loader() method is no longer needed because view engines and other plugins are required outside of the framework. Simply remove this unused code.

Replace plugin.require() with plugin.register()

Not backwards compatible

If your plugin requires another plugin, that code has to change to use the new plugin.register() method.

Without options:

// Previous versions
plugin.require('name', function (err) {});

// v6.0
plugin.register(require('name'), function (err) {});

With options:

// Previous versions
plugin.require('name', { some: 'options' }, function (err) {});

// v6.0
plugin.register({
    plugin: require('name'),
    options: { some: 'options' }
}, function (err) {});

Note that the second optional argument in plugin.register() is for registration options, not for plugin options.

Composer

The Hapi.Composer functionality has been moved to a simpler interface under Hapi.Pack. The same manifest format still applies with some new powerful features. The only exception is that you can no longer include multiple packs within a single manifest.

Instead of:

var composer = new Hapi.Composer(manifest);
composer.compose(function (err) {

    composer.start();
});

Use:

Hapi.Pack.compose(manifest, function (err, pack) {

    pack.start();
});

Since Pack no longer supports the relativeTo config option, the compose() function takes an optional second options argument which supports the relativeTo options. If set, all relative module names (view template engines, catbox cache strategies, and plugins) are resolved against it. Otherwise, they are resolved against the location of the hapi module used.

hapi CLI

The hapi command line interface is a simple wrapper around the Pack.compose() method and has the same migration items listed above. The -p command line option is used to set the value of the relativeTo config option.

@hueniverse hueniverse added this to the 6.0.0 milestone Jun 11, 2014
@hueniverse hueniverse self-assigned this Jun 11, 2014
@hueniverse hueniverse closed this Jun 12, 2014
@alexgorbatchev
Copy link
Contributor

@alexgorbatchev alexgorbatchev commented Jun 12, 2014

Awesome! When will this be in npm?

@alexgorbatchev
Copy link
Contributor

@alexgorbatchev alexgorbatchev commented Jun 12, 2014

The notes refer to .register(plugin, {route: {path: ...}}) but actual current implementation is .register(plugin, {route: {prefix: ...}})

@hueniverse
Copy link
Contributor Author

@hueniverse hueniverse commented Jun 12, 2014

@alexgorbatchev good catch. Spent 5 hours on a plane writing it so not surprised...

@garthk
Copy link

@garthk garthk commented Jun 18, 2014

Plugin modules MUST export an object. Exporting a function with a register property previously worked, perhaps unintentionally (#1728).

@garthk
Copy link

@garthk garthk commented Jun 18, 2014

postResponse has disappeared without notice… at least, if you're upgrading all the way from 2.6.

The last full documentation for postResponse was in the v2.1.1 reference. It was superseded by onResponse in v2.2.0, and its documentation removed. A reference to postResponse in the next paragraph, describing ttl, survived until v5.1.0.

@dypsilon
Copy link
Contributor

@dypsilon dypsilon commented Jul 4, 2014

Thanks again for all the great work documenting the breaking changes.

@tcoopman
Copy link

@tcoopman tcoopman commented Sep 29, 2014

The url to passport is: http://passportjs.org/ not .com

@lock lock bot locked as resolved and limited conversation to collaborators Jan 12, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
5 participants