Node.js gained built-in HTTP/2 support in version 8.4.0, but you’d be forgiven for missing it, as it’s tucked away in a separate module.
Let’s see it in action. We’ll set up a simple HTTP/2 server and then connect to it from a client.
First, the server. We need a certificate and a private key. For local testing, openssl is your friend.
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -keyout localhost.key -out localhost.crt -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost"
Now, the server code:
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('localhost.key'),
cert: fs.readFileSync('localhost.crt'),
});
server.on('stream', (stream, headers) => {
stream.respond({
'content-type': 'text/plain',
':status': 200,
});
stream.end('Hello from HTTP/2!');
});
server.listen(8443, () => {
console.log('HTTP/2 server listening on https://localhost:8443');
});
And the client:
const http2 = require('http2');
const fs = require('fs');
const client = http2.connect('https://localhost:8443', {
ca: fs.readFileSync('localhost.crt'), // Trust our self-signed cert
});
client.on('connect', () => {
const req = client.request({
':path': '/',
});
req.on('response', (headers) => {
let data = '';
req.on('data', (chunk) => {
data += chunk;
});
req.on('end', () => {
console.log('Response:', data);
client.close();
});
});
req.end();
});
When you run the server and then the client, you’ll see Response: Hello from HTTP/2!.
HTTP/2 is designed to overcome the limitations of HTTP/1.1, primarily head-of-line blocking. In HTTP/1.1, if you have multiple requests over a single connection, and one request is slow to respond, all subsequent requests on that same connection are blocked, even if they are ready to be processed. HTTP/2 introduces multiplexing, allowing multiple requests and responses to be interleaved on a single TCP connection. This is achieved through binary framing, where requests and responses are broken down into smaller frames that can be sent independently and reassembled at the destination.
The http2 module in Node.js provides a low-level API. You’re directly dealing with streams, headers, and frames. The createSecureServer function is analogous to http.createServer, but it requires TLS/SSL options. Similarly, http2.connect is the client-side equivalent, and it also expects TLS options for secure connections, which is standard for HTTP/2. The :status and :path pseudo-headers are crucial for HTTP/2; they replace the standard HTTP/1.1 request line and response status line.
The core of HTTP/2’s performance gain comes from its stream-based architecture. Each request and response pair is assigned a unique stream ID. These streams are multiplexed over a single TCP connection. If a frame for one stream is delayed, frames for other streams can still be processed. This fundamentally eliminates head-of-line blocking at the HTTP layer. Additionally, HTTP/2 introduces header compression (HPACK) which significantly reduces the overhead of sending repetitive headers, further improving efficiency. Server Push, another feature, allows the server to proactively send resources to the client that it anticipates the client will need, reducing the number of round trips.
When you configure the http2.connect options, you’ll notice the ca option for trusting the server’s certificate. For production, you’d use certificates issued by a trusted Certificate Authority. For local development, using the localhost.crt generated by openssl and passing it via ca allows Node.js to trust your self-signed certificate. This is a common pattern for testing TLS/SSL configurations without relying on external CAs.
The stream event on the server is where you handle incoming requests. The stream object itself is a Duplex stream, allowing you to read the request body and write the response body. The respond method is used to send the initial response headers, including the crucial :status pseudo-header. The end method on the stream signals the completion of the response body.
The http2 module’s stream abstraction means you can manage multiple concurrent requests on a single connection very efficiently. Each request is its own stream, and the underlying connection handles the interleaving of frames. This is a stark contrast to HTTP/1.1 where you might need multiple connections to achieve similar concurrency, leading to higher resource utilization.
The client.request method initiates a new stream. The response from the server comes back as a stream as well, and you listen for data events to collect the response body, similar to how you’d handle a readable stream in Node.js.
One subtle but powerful aspect of HTTP/2 is how it handles flow control. Each stream has its own flow control window, and the connection as a whole also has a window. This prevents a fast sender from overwhelming a slow receiver on a per-stream basis, and also at the connection level. The http2 module exposes mechanisms to manage these windows, though for basic usage, the defaults are usually sufficient.
If you’re using Node.js versions prior to 8.4.0, you’ll need to upgrade to use the built-in http2 module. For older applications that need HTTP/2, external libraries like spdy were common before native support.