HTTP/2 server in Deno
Introduction
From Wiki: HTTP/2 is a major revision of the HTTP protocol used by the World Wide Web. HTTP/2 is the first new version of HTTP since HTTP/1.1, which was standardized in RFC 2068 in 1997. The standardization effort was supported by Chrome, Opera, Firefox, Internet Explorer 11, Safari, Amazon Silk, and Edge browsers. Most major browsers had added HTTP/2 support by the end of 2015. About 97% of web browsers have the capability of getting content over HTTP/2. As of October 2021, 47% of the top 10 million websites supported HTTP/2.
HTTP/2 enables a more efficient use of network resources and a reduced perception of latency by introducing header field compression and allowing multiple concurrent exchanges on the same connection. It also introduces unsolicited push of representations from servers to clients. HTTP/2 also enables more efficient processing of messages through use of binary message framing.
HTTP/2 is specified in RFC 7540.
In this article, we’ll learn how to use HTTP/2 for making HTTP requests and serving HTTP. We’ll look at secure HTTP/2 only (https://), as Deno doesn’t support serving HTTP/2 over unencrypted channel.
Negotiation
The HTTP/2 specification doesn’t limit HTTP/2 to run over secure connections only. Some implementations have stated that they will only support HTTP/2 when it is used over an encrypted connection, and currently no browser supports HTTP/2 unencrypted.
Though HTTP/2 is gaining popularity, not all browsers support HTTP/2. The servers need to support both HTTP/2 and HTTP/1.1 (fallback).
HTTP/2 uses ALPN to extend TLS by including the protocol negotiation in the exchange of hello messages. The client provides a list of protocols it supports, and the server responds with its selected protocol. No additional round-trip required.
Therefore, there are two steps in HTTP/2 connection negotiation between client and server:
- Agree on a protocol
- Establish a secure connection
Here is a very simple diagram explaining the negotiation & securing the connection:
Older clients would either not use ALPN or send HTTP/1.1 only, while newer clients would likely send both HTTP/1.1 and HTTP/2. The server would choose a protocol out of these two. Then the connection will be secured via cipherSpec.
Making HTTP request
Deno provides browser compatible fetch API to make HTTP requests. There is no need to do anything special for HTTP/2. The URL must be an HTTPS URL for HTTP/2 to get advertised. For HTTP URLs (unsecured), only HTTP/1.1 would be used.
HTTP/2 gets advertised only for HTTPS
Deno doesn’t support H2C
Making HTTP/1.1 requests
Any request without HTTPS uses HTTP/1.1. Here is a fetch API call that uses HTTP/1.1:
await fetch("http://deno.land");
A trace of the HTTP data generated by the above fetch shows that HTTP/1.1 got used:
GET / HTTP/1.1
accept: */*
user-agent: Deno/1.17.3
accept-encoding: gzip, br
host: deno.land
Making HTTP/2 requests
A request with HTTPS would go through ALPN. Both HTTP/2 and HTTP/1.1 would be advertised to the server. The server would decide one of the protocol for the secured connection. The fetch API call is the same, except for HTTPS in the URL.
Deno’s fetch API always advertises HTTP/2 and HTTP/1.1
await fetch("https://deno.land"); //always sends HTTP/2 and HTTP/1.1
In short, HTTP versioning doesn’t matter when working as a client
Serving HTTP/2
A server needs to know what it supports so that it can decide a protocol from the client’s offer. In Deno, we can write a native HTTPS server in two ways:
- Use serveTls API from Deno’s standard library
- Use low level core functions like listenTls and serveHttp
Let’s take a look at both.
Using ServeTls API
The following is a simple ‘Hello world’ program serving over HTTPS:
import { serveTls } from "https://deno.land/std/http/mod.ts";const certFile = "./l.crt";
const keyFile = "./l.key";async function reqHandler(req: Request) {
return new Response("Hello world");
}serveTls(reqHandler, { port: 8100, certFile, keyFile });
Let’s do a test with curl:
$ curl https://localhost:8100 -kv
* Connected to localhost (127.0.0.1) port 8100 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* ALPN, server did not agree to a protocol
> GET / HTTP/1.1
> Host: localhost:8100
> User-Agent: curl/7.77.0< HTTP/1.1 200 OK
< content-type: text/plain;charset=UTF-8
< content-length: 11
<
Hello world
As per curl logs, the HTTP server didn’t agree to ALPN.
At the time of writing (Deno Standard library version 0.121.0), serveTls API doesn’t use & support ALPN configuration.
Deno’s serveTls API doesn’t support HTTP/2
To use HTTP/2, we need to use low level APIs: listenTls and serveHttp.
Using low-level APIs
To support HTTP/2, we need to use low-level APIs: listenTls and serveHttp. First, let’s have a look at the default support. The following is an HTTP servver using low-level APIs without explicitly setting ALPN.
const certFile = "./l.crt";
const keyFile = "./l.key";for await (
const conn of Deno.listenTls({
port: 8100,
certFile,
keyFile,
})
) {
for await (const { request: req, respondWith: res } of Deno.serveHttp(conn)) {
res(new Response("Hello world!"));
}
}
Here is a test using curl:
$ curl https://localhost:8100 -kv
* Connected to localhost (127.0.0.1) port 8100 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* ALPN, server did not agree to a protocol
> GET / HTTP/1.1
> Host: localhost:8100
> User-Agent: curl/7.77.0
< HTTP/1.1 200 OK
< content-type: text/plain;charset=UTF-8
< content-length: 11
<
Hello world
The result is the same.
By default, low-level APIs doesn’t support ALPN
The listenTls API takes an additional attributed called alpnProtocols that can take two values: h2, http/1.1.
ALPN is supported under unstable umbrella
The following is the updated code of the HTTP server using low-level APIs:
const certFile = "./l.crt";
const keyFile = "./l.key";for await (
const conn of Deno.listenTls({
port: 8100,
certFile,
keyFile,
alpnProtocols: ["h2", "http/1.1"],
})
) {
for await (const { request: req, respondWith: res } of Deno.serveHttp(conn)) {
res(new Response("Hello world!"));
}
}
Here is a test using curl:
$ curl https://localhost:8100 -kv
* Connected to localhost (127.0.0.1) port 8100 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* ALPN, server accepted to use h2
> GET / HTTP/2
> Host: localhost:8100
> user-agent: curl/7.77.0
> accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 4294967295)!
< HTTP/2 200
< content-type: text/plain;charset=UTF-8
< content-length: 12
<
Hello world
The server now performs ALPN and supports HTTP/2.
To enable HTTP/2, listenTls must be used