Running Ring web application on HTTP2 with rj9a

Ring-jetty9-adapter(rj9a) just received an update, the 0.9, with Jetty 9.3 adoption. The most important feature in this release is support for HTTP2. That means, you can run your Ring application on the new HTTP2 protocol.

In case you still have no idea about HTTP2, it's the biggest update to HTTP, the protocol we use everyday and everywhere. In short, HTTP2 introduces connection multiplex to reuse connection for several request/response simultaneously. Also the persisted connection makes server push possible, and that's part of HTTP2. HTTP2 uses TLS by default. In order to keep most servers backward compatible, we will run HTTP2 and HTTP1.1 on the same server and port. Modern client will detect server configuration on SSL handshake, via a TLS extension called ALPN. The server will list supported application layer protocols in SERVER HELLO and let client to choose what it understands.

The basic part of HTTP2 is fully compatible for 1.1, so you won't have to modify your application code to use it. In rj9a, just add option :h2? true to enable HTTP2. And :h2c? true to enable its variance on plain socket.

(defn dummy-app [req] {:body "It works" :status 200})
(jetty/run-jetty dummy-app {:port 5000
                            :h2c? true
                            :h2? true
                            :ssl? true
                            :ssl-port 5443
                            :keystore "..."
                            :key-password "..."})

To test HTTP2 interface, you will need to install nghttp. It's pretty similar to curl:

$ nghttp -v https://localhost:5443
[  0.000] Connected
The negotiated protocol: h2-14
[  0.031] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
         (niv=2)
         [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
         [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.031] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
         (dep_stream_id=0, weight=201, exclusive=0)
[  0.031] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
         (dep_stream_id=0, weight=101, exclusive=0)
[  0.031] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
         (dep_stream_id=0, weight=1, exclusive=0)
[  0.031] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
         (dep_stream_id=7, weight=1, exclusive=0)
[  0.031] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
         (dep_stream_id=3, weight=1, exclusive=0)
[  0.031] send HEADERS frame <length=37, flags=0x25, stream_id=13>
         ; END_STREAM | END_HEADERS | PRIORITY
         (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
         ; Open new stream
         :method: GET
         :path: /
         :scheme: https
         :authority: localhost:5443
         accept: */*
         accept-encoding: gzip, deflate
         user-agent: nghttp2/1.0.1
[  0.032] recv SETTINGS frame <length=12, flags=0x00, stream_id=0>
         (niv=2)
         [SETTINGS_HEADER_TABLE_SIZE(0x01):4096]
         [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.032] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
         ; ACK
         (niv=0)
[  0.032] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
         ; ACK
         (niv=0)
[  0.033] recv (stream_id=13) :status: 200
[  0.033] recv (stream_id=13) server: Jetty(9.3.1.v20150714)
[  0.033] recv HEADERS frame <length=20, flags=0x04, stream_id=13>
         ; END_HEADERS
         (padlen=0)
         ; First response header
It works[  0.033] recv DATA frame <length=8, flags=0x01, stream_id=13>
         ; END_STREAM
[  0.033] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
         (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

The verbose output shows us every detail about request and response in HTTP2.

Note that in order to run HTTP2, you will need JDK 8 / OpenJDK 1.8 and put alpn-boot jar in your bootclasspath. I have created a leiningen plugin to manage bootclasspath in clojure project.

The complete example is available in github repository.