
Author | wanago
Translator | Xiaodafei
Editor | Yonie
The HTTP protocol was introduced in 1991 and has been around for nearly 30 years. Since the first documented version (later known as 0.9), it has undergone a significant evolution. In this article, we will briefly review the history of the HTTP protocol, highlight what HTTP/2 brings, and how we can benefit from it. We will implement it using a Node.js server.
Brief History of HTTP
The first version of HTTP could only transfer Hypertext Markup Language (HTML) files, which is why we call it the Hypertext Transfer Protocol. It was very simple, with only one available method: GET. There were no HTTP headers or status codes. If there was an issue, the server could respond with an HTML file containing an error description.
Version 1.0 appeared in 1996. Compared to the previous version, it included many improvements, the most important of which were status codes, POST, and additional methods like headers. Now, we could use the Content-Type header to transfer files other than plain HTML.
HTTP/1.1, released in 1997, introduced several other improvements. In addition to adding methods like OPTIONS, it also introduced the Keep-Alive header. This allowed a connection to remain open for multiple HTTP requests. Because of this, connections did not have to close and reopen after each request. In HTTP/1.1, we typically could only have 6 connections at a time. For example, if one of the requests got stuck due to some complex logic on the server, each of them could only handle one request at a time, causing the entire connection to freeze and wait for a response. This issue is known as head-of-line blocking.
Benefits of Implementing Node.js HTTP/2
There are many ways to implement HTTP/2 in the stack. A common approach might be to implement it on a web server, but in this article, we will implement it at the application layer to expand our knowledge of Node.js. Since browsers do not support unencrypted HTTP/2, this means we need to establish a TLS connection via HTTPS. To do this locally, we use the following command to generate a certificate:
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out certificate.pem -days 365 -nodes
If you want to learn more, please refer to “Implementing HTTPS with OpenSSL Certificates,” available at: https://wanago.io/2019/04/01/node-js-typescript-8-implementing-https-with-our-own-openssl-certificate/
import * as http2 from 'http2';
import * as fs from 'fs';
import * as util from 'util';
const readFile = util.promisify(fs.readFile);
async function startServer() {
const [key, cert] = await Promise.all([
readFile('key.pem'),
readFile('certificate.pem')
]);
const server = http2.createSecureServer({ key, cert })
.listen(8080, () => {
console.log('Server started');
});
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end('<h1>Hello World</h1>');
});
server.on('error', (err) => console.error(err));
}
startServer();
Solving Head-of-Line Blocking Issues in Complex Applications
Even with 6 parallel connections using HTTP/1.1, it may not be sufficient, especially when we encounter head-of-line blocking. HTTP/2 addresses this issue by allowing a single connection to handle multiple requests simultaneously, meaning that even if one request gets stuck, the others can continue. In the simple example above, we respond with plain HTML. Whenever someone makes a request to our server, the stream event is triggered.
If you want to learn more about streams, please refer to “Paused and Flowing Modes of a Readable Stream” and “Writable Streams, Pipes, and Process Streams,” available at:
“Paused and Flowing Modes of a Readable Stream”: https://wanago.io/2019/03/04/node-js-typescript-4-paused-and-flowing-modes-of-a-readable-stream/
“Writable Streams, Pipes, and Process Streams”: https://wanago.io/2019/03/11/node-js-typescript-5-writable-streams-pipes-and-the-process-streams/
In the headers parameter, we have all the headers of the incoming request. It serves as a check, such as the request method and path. Now, since we are using HTTP/2, the browser uses a non-blocking connection. A notable example is the grid demo created by the Golang team (see the link below). When using HTTP/2, you can see significant performance improvements due to handling parallel requests on the same connection and addressing head-of-line blocking issues.
Related link:
The Golang Team’s Grid Demo: https://http2.golang.org/gophertiles
Header Compression
HTTP/2 uses a new header compression algorithm, which we call HPACK. Some developers involved in defining the HTTP/2 protocol also developed SPDY, which was previously used for header compression. Unfortunately, it was later found to be vulnerable to CRIME attacks. HPACK not only makes the entire process more secure but also, in some cases, faster.
Using Server Push to Cache Data
When we implement HTTP/2, all the improvements mentioned above come out of the box. This is not all its capabilities. With server push, we can now populate data in the client cache. We can even do this before the browser requests it. A basic use case is when a user requests the index.html file.
server.on('stream', (stream, headers) => {
const path = headers[":path"];
switch(path) {
case '/': {
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end(`
<head>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>Hello World</h1>
</body>
`);
break;
}
case '/style.css': {
stream.respond({
'content-type': 'text/css',
':status': 200
});
stream.end(`
body {
color: red;
}
`);
break;
}
default: {
stream.respond({
':status': 404
});
stream.end();
}
}
});
In the example above, when a user accesses the homepage, it requests the index.html file. When it receives a stylesheet, it notices that it also needs the style.css file. There is some delay between the requests for index.html and style.css, and we can use server push to handle it. Since we know the user will need the style.css file, we can send it along with index.html.
server.on('stream', (stream, headers) => {
const path = headers[":path"];
switch(path) {
case '/': {
stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
if (err) throw err;
pushStream.respond({
'content-type': 'text/css',
':status': 200
});
pushStream.end(`
body {
color: red;
}
`);
});
stream.respond({
'content-type': 'text/html',
':status': 200
});
stream.end(`
<head>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<h1>Hello World</h1>
</body>
`);
break;
}
case '/style.css': {
stream.respond({
'content-type': 'text/css',
':status': 200
});
stream.end(`
body {
color: red;
}
`);
break;
}
default: {
stream.respond({
':status': 404
});
stream.end();
}
}
});
Now, using stream.pushStream, whenever someone requests index.html, we send the style.css file. When the browser processes it, it will see the <link> tag and notice that it also needs the style.css file. Thanks to server push, it has already been cached in the browser, so no additional request is needed.

Conclusion
HTTP/2 is designed to improve performance by meeting the demands of increasingly complex web pages. The amount of data sent using the HTTP protocol has increased, and HTTP/2 addresses this issue through methods like handling head-of-line blocking. Additionally, by processing parallel requests on the same connection, it alleviates some of the limitations of HTTP/1.1.
Original English text: https://wanago.io/2019/07/15/node-js-typescript-benefits-http2-protocol/