Server-Side HTTP/2

Enable HTTP/2 support

HTTP/2 is enabled through configuration:

pekko.http.server.enable-http2 = on

Since Pekko HTTP 2.0.0, this defaults to on.

Use newServerAt(...).bind() and HTTPS

HTTP/2 is primarily used over a secure HTTPS connection which takes care of protocol negotiation and falling back to HTTP/1.1 over TLS when the client does not support HTTP/2. See the HTTPS section for how to set up HTTPS.

You can use Http().newServerAt(...).bind()Http().get(system).newServerAt(...).bind() as long as you followed the above steps:

Scala
sourceimport scala.concurrent.Future

import org.apache.pekko
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.HttpsConnectionContext
Http().newServerAt(interface = "localhost", port = 8443).enableHttps(httpsServerContext).bind(asyncHandler)
Java
sourceimport org.apache.pekko.http.javadsl.Http;

Http.get(system)
    .newServerAt("127.0.0.1", 8443)
    .enableHttps(httpsConnectionContext)
    .bind(asyncHandler);

Note that currently only newServerAt(...).bind and newServerAt(...).bindSync support HTTP/2 but not bindFlow or connectionSource(): Source.

HTTP/2 over TLS uses Application-Layer Protocol Negotiation (ALPN) to negotiate whether both client and server support HTTP/2. ALPN support is included in the Java versions supported by Apache Pekko HTTP and does not require a separate provider or boot classpath configuration.

HTTP/2 without HTTPS

HTTP/2 without TLS should only be used on a trusted network or when TLS is terminated by a trusted proxy. Most public clients use HTTP/2 over TLS.

Apache Pekko HTTP supports starting a cleartext HTTP/2 connection with Prior Knowledge. For compatibility with older clients, it also supports the HTTP/1.1 Upgrade mechanism from the obsolete RFC 7540 specification.

Both approaches are supported transparently on the same port and are automatically enabled when HTTP/2 is enabled:

Scala
sourceimport org.apache.pekko
import pekko.http.scaladsl.Http
import pekko.http.scaladsl.HttpConnectionContext

Http().newServerAt("localhost", 8080).bind(handler)
Java
sourceimport org.apache.pekko.http.javadsl.Http;

Http.get(system).newServerAt("127.0.0.1", 8443).bind(asyncHandler);

h2c Upgrade

Warning

The HTTP/1.1 Upgrade mechanism and the h2c upgrade token are obsolete in RFC 9113. Use this mode only when compatibility with an older client requires it. New cleartext HTTP/2 clients should use Prior Knowledge.

The legacy Upgrade mechanism allows HTTP/1.1 and HTTP/2 clients to connect to the same port without knowing beforehand which protocol the server supports.

The disadvantage is that relatively few clients support switching to HTTP/2 in this way. Additionally, HTTP/2 communication cannot start until the first request has been completely sent. This means if your first request may be large, it might be worth it to start with an empty OPTIONS request to switch to HTTP/2 before sending your first ‘real’ request, at the cost of a roundtrip.

Cleartext HTTP/2 with Prior Knowledge

The other option is to connect and start communicating in HTTP/2 immediately. The downside of this approach is the client must know beforehand that the server supports HTTP/2. For this reason the approach is known as HTTP/2 with Prior Knowledge.

Limiting the lifetime of connections

Long-lived HTTP/2 connections, for example those used by gRPC, can lead to an uneven load distribution across server instances: clients stay connected to the instances they found at connect time, and instances added later (after a scale-out or a rolling deploy) receive no share of the existing traffic.

To rebalance connections regularly, set a maximum connection age:

pekko.http.server.http2.max-connection-age = 120s

When the age of a connection exceeds the configured value, the server sends a GOAWAY frame, lets requests that are already in flight complete, and then closes the connection. Streams that the peer opens after the GOAWAY frame was sent are refused, upon which well-behaved clients (for example, grpc-java) transparently retry them on a new connection.

The setting applies to HTTP/2 connections only: HTTP/1.1 connections accepted on the same port are not age-limited.

If the connection is already being terminated when its age expires, for example because the server binding is being terminated, the expiry has no effect and the termination in progress keeps its own deadline. Conversely, terminating the server binding with an earlier deadline shortens a drain that the age started.

Requests that are still in flight when the age expires are given a grace period to complete, after which the connection is closed even if they have not completed:

pekko.http.server.http2.max-connection-age-grace = 30s

The default grace period is 30 seconds. Set it to infinite to wait for all requests in flight to complete, however long they take.

A jitter is applied to the configured value for each connection (by default +/- 10%, configurable via pekko.http.server.http2.max-connection-age-jitter), so that connections that were opened together are not all closed at the same time.

Trailing headers

Like in the HTTP/1.1 ‘Chunked’ transfer encoding, HTTP/2 supports a trailer part containing headers after the body. Apache Pekko HTTP currently doesn’t expose the trailing headers of the request. For the response, you can either model the trailing headers as the HttpEntity.LastChunklast chunk of a HttpEntity.Chunkedchunked response entity, or use the trailertrailer attribute:

Scala
sourceimport org.apache.pekko
import pekko.http.scaladsl.model.ContentTypes
import pekko.http.scaladsl.model.HttpEntity
import pekko.http.scaladsl.model.Trailer
import pekko.http.scaladsl.model.AttributeKeys.trailer
import pekko.http.scaladsl.model.headers.RawHeader
import pekko.util.ByteString

HttpResponse(StatusCodes.OK, entity = HttpEntity.Strict(ContentTypes.`text/plain(UTF-8)`, ByteString("Tralala")))
  .addAttribute(trailer, Trailer(RawHeader("name", "value")))
Java
sourceimport org.apache.pekko.http.javadsl.model.Trailer;
import org.apache.pekko.http.javadsl.model.headers.RawHeader;
import static org.apache.pekko.http.javadsl.model.AttributeKeys.trailer;

HttpResponse.create()
    .withStatus(200)
    .addAttribute(trailer, Trailer.create().addHeader(RawHeader.create("name", "value")));

Having both a trailingHeaders attribute and a LastChunk element is not supported.

Testing with cURL

At this point you should be able to connect, but HTTP/2 may still not be available.

You’ll need a recent version of cURL compiled with HTTP/2 support (for OSX see this article). You can check whether your version supports HTTP2 with curl --version, look for the nghttp2 extension and the HTTP2 feature:

curl 7.52.1 (x86_64-pc-linux-gnu) libcurl/7.52.1 OpenSSL/1.0.2l zlib/1.2.8 libidn2/0.16 libpsl/0.17.0 (+libidn2/0.16) libssh2/1.8.0 nghttp2/1.23.1 librtmp/2.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp
Features: AsynchDNS IDN IPv6 Largefile GSS-API Kerberos SPNEGO NTLM NTLM_WB SSL libz TLS-SRP HTTP2 UnixSockets HTTPS-proxy PSL

When you connect to your service you may now see something like:

$ curl -k -v https://localhost:8443
(...)
* ALPN, offering h2
* ALPN, offering http/1.1
(...)
* ALPN, server accepted to use h2
(...)
> GET / HTTP/1.1
(...)
< HTTP/2 200
(...)

If your curl output looks like above, you have successfully configured HTTP/2.