Switch your Phoenix app to HTTP/2 in just a Few Minutes
The core of Nots.io is built on top of Elixir Phoenix framework. It’s stable, well designed, modern, pretty extensible with lots of great libs for essential web development needs. The production version of it is all about handling HTTP/1.1 requests. But it turns out it’s surprisingly easy to add HTTP/2 support into existing Phoenix application. This is a post in “Less is more” series on little nifty tech things which pop up while we’re working on Nots.

So HTTP/2 is the next generation of good-old HTTP protocol, derived from Google’s experimental  SPDY protocol. It’s history, design goals and implementation details are well described in a great post by Ilya Grigorik. Turns out that HTTP/2 protocol is well supported by modern browsers.

The Phoenix community is heavily working on bringing HTTP/2 support into the framework. Few days ago they announced that the Plug library, which is one of the building blocks underneath the framework, got 1.5.0 RC-1 update. It brings all required plumbing for HTTP/2 and is stable enough to start testing it on a development setup. Another core part of any Phoenix application is Cowboy HTTP server. Thanks to awesome Loïc Hoguin who develops it, the 2nd version of Cowboy fully supports HTTP/2. 

The update path is pretty straightforward. 

1. Adjust `mix.exs` existing dependencies so that they now point to the newest version of libs:

defp deps do
    [{:phoenix,git: "https://github.com/phoenixframework/phoenix", branch: "master", override: true},
    {:plug, "1.5.0-rc.1", override: true},
    {:cowboy, "~> 2.1", override: true}
...
   ]
  end

2. Run mix deps.get to fetch new dependencies.

3. Since HTTP/2 works only with TLS encryption, we have to generate self-signed certificate:

openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -keyout priv/server.key -out priv/server.pem

4. Switch handler and turn on HTTPS support in config/dev.exs:

config :yourapp, YourAppWeb.Endpoint,
  handler: Phoenix.Endpoint.Cowboy2Handler,
  https: [port: 4001, keyfile: "priv/server.key", certfile: "priv/server.pem"]

5.  That’s it.  Now restart phx.server and visit secure https://localhost:4001 URL to check if it works properly. The first time you enter it the browser will tell you that connection is not secure and the certificate is not issued by a certificate authority. Just add this URL to exception list and off we go. You’ll see H2 protocol in DevTools:

 

6.  In addition that would be great, when someone visits conventional phoenix’s 4000 port on localhost via HTTP, to automatically switch protocol to secure HTTPS. To do that change endpoint configuration this way:

config :yourapp, YourAppWeb.Endpoint,
  http: [port: 4000],
  url: [scheme: "https", host: "localhost:4001"],
  debug_errors: true,
  code_reloader: true,
  check_origin: false,
  handler: Phoenix.Endpoint.Cowboy2Handler,
  https: [compress: true,
          port: 4001,
          keyfile: "priv/server.key",
          certfile: "priv/server.pem"],
  force_ssl: [hsts: true]

Cowboy server will listen not secure requests on 4000 port and when HTTP request comes in it’ll force a browser to redirect to HTTPS protocol, to 4001 port with HTTP Strict Transport Security (HSTS) turned on.

 


 

One thing which appears partially broken in Nots after this update is Phoenix.ConnTest. This module is made up for testing controllers like this:

build_conn()
|> put_req_header("accept", "application/json")
|> get("/")

Under the hood, the get method delegates connection creation to Plug.Adapters.Test.Conn which uses http scheme by default when URI like “/“ with no scheme and host comes in (as in the example above).

In order to force Plug to use correct HTTPS protocol and right port, the scheme and port should be provided in a URI, e.g. https://localhost:4001/.

URI module from Elixir standard distribution is made just for that. The make_full_url helper function adds scheme and host according to the configuration file setup:

defp make_full_url(url) do
  URI.parse(url)
  |> Map.put(:scheme, YourAppWeb.Endpoint.config(:url)[:scheme])
  |> Map.put(:host, YourAppWeb.Endpoint.config(:url)[:host])
  |> URI.to_string
end

build_conn()
|> put_req_header("accept", "application/json")
|> get(make_full_url("/"))

Now everything works smoothly, and a little bit faster due to HTTP/2 stream multiplexing.