HTTP/2 Today with Phoenix

As you may know, the IETF’s HTTP Working Group has released a new version of the HTTP standard, HTTP/2. The new standard is binary, fully multiplexed, and supports server push. The standard was approved in February of 2015, and now almost all modern browsers support it, so you should be able to use it for new projects that don’t target IE versions lower than 11. Unfortunately outside of NGINX, and some CDNs server side support has been lagging in many language ecosystems. However, the master branches of Cowboy2 and Plug have supported the standard since November of 2017. It requires a bit of effort, but you can get started with HTTP/2 in a new Phoenix app today.

Back in December Maarten Van Vliet posted an nice article describing how to do the minimal setup for a new app. I’ll be expanding on that here and explaining how to use Webpack to split you assets to take advantage of HTTP/2 multiplexing.

Getting Started

Let’s quickly start a new phoenix project.

  mix phx.new --no-brunch --no-ecto http_2_today

We’ll be omitting ecto for simplicity, and brunch so that we can add Webpack. We’ll be using Webpack, because Brunch doesn’t do code splitting, which is useful for creating a number of small files which can be pushed to the client in parallel. Webpack also allows async loading, which can be useful for grabbing assets as you need them and can be combined with HTTP/2 in interesting ways.

Next, lets update our mix.exs file to use the versions of Cowboy, Phoenix, and Plug that support HTTP/2.

  defp deps do
    [
      {:phoenix, git: "https://github.com/phoenixframework/phoenix", branch: "master", override: true},
       {:plug, "1.5.0-rc.1", override: true},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 2.1", override: true},
    ]
  end

Plug 1.5 should be out soon, and for the moment you can use 1.5.0-rc.1. Cowboy 2.1 is stable and simple require overriding the Phoenix default as with Plug. Phoenix is targeting support with updated defaults in 1.4 and there is no release candidate at the time of writing this post, so you’ll need to target the master branch for now.

Run mix deps.get and then check to make sure that mix phx.server works. Everything should be running and ok at this point.

Now let’s quickly set up webpack and add some simple JavaScript and CSS. I’ll assume you have yarn installed, and are loosely familiar with it, and if not check their site for details.

First create an assets/ folder at the top level of your project. Then move to that directory and begin adding Webpack.

  mkdir assets
  cd assets
  yarn add webpack webpack-dev-server --dev
  yarn add phoenix

This will create a package.json file, a yarn.lock file, and a node_modules/ directory. Now let’s add a few more dependencies relates to ES6 transformation and handling Sass.

  yarn add babel-core babel-loader babel-preset-env css-loader extract-text-webpack-plugin node-sass sass-loader style-loader --dev

If you’re coming form brunch, of Phoenix without a front-end build tool, this looks like a lot of impenetrable stuff, but it all boils down to turning new JavaScript features and Sass into something the majority of browsers can handle.

Webpack Config & Assets

Now let’s create a simple(ish) webpack config file that will get us started.

  touch webpack.config.js

Next we’ll work on a config that will process top level files in /js and /css as well as splitting out phoenix_html lib into a vendor bundle. Vendoring is a great way to take advantage of caching and our case multiplexing. I won’t dwell on this too much as Webpack 4, which is in RC changes vendoring a bit and removes the CommonsChunkPlugin. The following setup also assumes you’ll be using some sort of jsx files, but you could easily use .vue or something else.

  const webpack = require("webpack");
  const ExtractTextPlugin = require('extract-text-webpack-plugin')
  const path = require('path');

  module.exports = {
    entry: {
      'app': ['./js/app.js', './css/app.scss'],
      'vendor': [
        'phoenix'
      ]
    },
    output: {
      path: path.resolve(__dirname, '../priv/static/js'),
      filename: '[name].js'
    },
    devtool: 'source-map',
    resolve: {
      extensions: ['.js', '.jsx']
    },
    module: {
      rules: [
        {
          test: /\.(sass|scss)$/,
          include: /css/,
          use: ExtractTextPlugin.extract({
            fallback: 'style-loader',
            use: [
              {loader: 'css-loader'},
              {loader: "sass-loader"},
            ]
          })
        },
        {
          test: /\.(js|jsx)$/,
          exclude: /node_modules/,
          use: [
            'babel-loader',
          ],
        },
      ],
    },
    resolve: {
      extensions: ['.js', '.jsx'],
    },
    plugins: [
      new ExtractTextPlugin('css/app.css'),
      new webpack.optimize.CommonsChunkPlugin({name: 'vendor'})
    ],
  };

Babel

Next let’s add a .babelrc file, so that we can use babel and store it’s config seperately from webpack.

  touch .babelrc

Then set it to use the env preset, which should be adequate for most users.

  {
    "presets": ["env"]
  }

The Javascript

For now, let’s just create a folder and a simple entry point.

  mkdir js
  touch js/app.js

Now in the js file, let’s import the Phoenix sockets code.

  import { Socket } from 'phoenix';

This is fine to start with, and will let us see if things are working as intended.

Some Sass/Scss

Similar to the above JavaScript, we’ll create an entry point file and I’ll be using Scss syntax for Sass.

  mkdir css
  touch css/app.scss
  touch css/normalize.css

Go to the Normalize github page and copy the latest version and paste it into your new normalize.css file. Then include that in you app.scss file.

  @import "normalize.css";

This will bundle up normalize, and you can use the same pattern for your own css/scss/sass files.

Configuring the Start Script

This is a good time to configure a simple start script for webpack-dev-server. Open you package.json and add the following snippet.

  "scripts": {
    "start": "webpack-dev-server --https --color --compress"
  },

Now when you run yarn start inside of assets/ you will spin up a dev server that will serve asstes over https which is necessary for HTTP/2, it will colorize it’s output, and gzip everything. If yarn start works, then you’re ready to jump back to the Phoenix portion of the app and configure things there.

Phoenix Configuration

First, we need to generate a private key and self signed certificate so that Cowboy and Phoenix can serve you application over https locally. The following is taken directly the section in config/dev.exs about SSL.

  openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/C=US/ST=Denial/L=Springfield/O=Dis/CN=www.example.com" -keyout priv/server.key -out priv/server.pem

You’ll want to add the following to your .gitignore, as you should never store sensitive information or credentials in you source control.

  priv/server.key
  priv/server.pem

Now let’s configure our endpoint to server our app over https and run a watcher for webpack-dev-server. Add the following to your config/dev.exs.

  config :http_2_today, Http2TodayWeb.Endpoint,
    debug_errors: true,
    code_reloader: true,
    check_origin: false,
    watchers: [
      node: [
        "node_modules/.bin/webpack-dev-server",
        "--https",
        "--color",
        "--inline",
        "--hot",
        "--stdin",
        "--host", "localhost",
        "--port", "8080",
        "--public", "localhost:8080",
        "--config", "webpack.config.js",
        cd: Path.expand("../assets", __DIR__)
      ]
    ],
    https: [port: 4000, keyfile: "priv/server.key", certfilee: "priv/server.pem"]

You should now be able to run mix phx.server from you main directory and see Webpack output in the console.

At this point, we need to make sure we can include our assets in out html templates.

View Functions

Add the following functions to lib\http_2_today_web\views\layout_view.

  defmodule Http2TodayWeb.LayoutView do
    use Http2TodayWeb, :view
    def js_script_tag do
      if env() == :prod do
        # In production we'll just reference the file
        """
          <script src="<%= static_path(@conn, "/js/vendor.js") %>"></script>
          <script src="<%= static_path(@conn, "/js/app.js") %>"></script>
        """
      else
        # In development mode we'll load it from our webpack dev server
        """
          <script src="https://localhost:8080/vendor.js"></script>
          <script src="https://localhost:8080/app.js"></script>
        """
      end
    end

    # Ditto for the css
    def css_link_tag do
      if env() == :prod do
        "<link rel=\"stylesheet\" href=\"<%= static_path(@conn, \"/css/app.css\") %>" 
      else
        "<link rel=\"stylesheet\" type=\"text/css\" href=\"https://localhost:8080/css/app.css\" />"
      end
    end

    defp env do
      unquote(Mix.env())
    end
  end

Note, this is update from the original version of this post to use unquote/1 to evaluate the environment at compile time since Mix isn’t available in a running production app. Thanks to OvermindDL1 for the feedback!

This will load assets from the dev server in dev mode and server the bundled files in production. Now we can use these function in lib/http_2_today_web/templates/layout/app.html.eex.

  <!DOCTYPE html>
  <html lang="en">
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width, initial-scale=1">
      <meta name="description" content="">
      <meta name="author" content="">

      <title>Hello Http2Today!</title>
      <%= {:safe, css_link_tag()} %>
    </head>

    <body>
      <div class="container">
        <header class="header">
          <nav role="navigation">
            <ul class="nav nav-pills pull-right">
              <li><a href="http://www.phoenixframework.org/docs">Get Started</a></li>
            </ul>
          </nav>
          <span class="logo"></span>
        </header>

        <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
        <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>

        <main role="main">
          <%= render @view_module, @view_template, assigns %>
        </main>

      </div> <!-- /container -->
      <%= {:safe, js_script_tag()} %>
    </body>
  </html>

Trying It Out

Now, let’s put it all together and check it out in the browser. Run mix phx.server and visit https://localhost:4000 in your favorite browser. You’ll probably have to tell the browser at this point to trust your self-signed cert, and you’ll need to visit https://localhost:8080/ and do the same for the assets host. If that worked you should be able to open the inspector, switch to the network tab, and see that everything is loading over HTTP/2.

inspector view

Notice under protocol, all of the assets are marked h2, which is shorthand for HTTP/2.

Wrap Up

This should give you enough to start working with HTTP/2 and actual assets. I’ll leave it as an exercise for readers to explore pushing multiple js files to the client and combining Webpack’s lazy loading to push files on demand. In production, you will need to generate real certs and configure prod.exs, but that’s out of the scope of this post. As always, if you have any questions, feel free to reach out and ask me.

You can find the full source code here.


If you enjoyed this post, follow me on twitter @ChaseGilliam, sometimes I'm funny. You can also find me on Github.