bugl
bugl
HomeLearnPatternsPathsSearch
HomeLearnPatternsPathsSearch

Loading lesson path

Learn/Node.js/Node.js Advanced
Node.js•Node.js Advanced

Node.js HTTP/2 Module

Flash cards

Review the key moves

1/4
Core idea

What is the main idea behind Node.js HTTP/2 Module?

Lesson checks

Practice each idea before moving on

Short Mimo-style checks built from this lesson's code, terms, and sequence.

1Quick choice

Which statement best captures the main point of this lesson?

2Fill blank

Complete the missing token from the example code.

___ http2 = require('http2');
3Order

Put the learning moves in the order that makes the concept easiest to apply.

HTTP/2 Server Push
Creating an HTTP/2 Server
Using the HTTP/2 Module

What is HTTP/2?

The Node.js HTTP/2 module provides an implementation of the HTTP/2 protocol, offering improved performance, server push capabilities, header compression, and multiplexing over a single connection.

HTTP/2 improves upon HTTP/1.1 with several key features:

  • Binary protocol : HTTP/2 uses a binary format for data transfer rather than the text format of HTTP/1.1, making it more efficient to parse.
  • Multiplexing : Multiple requests and responses can be sent over a single connection simultaneously.
  • Header compression : HTTP/2 compresses headers to reduce overhead.
  • Server push : Servers can proactively send resources to clients before they request them.
  • Stream prioritization : Resources can be delivered with different priorities.

Using the HTTP/2 Module

In Node.js, the HTTP/2 module can be accessed using:

const http2 = require('http2');

The HTTP/2 module is stable as of Node.js v10.0.0. It's important to note that HTTP/2 requires a secure connection (HTTPS) in most browsers, so most examples will use TLS/SSL.

Creating an HTTP/2 Server

Here's an example of creating a basic HTTP/2 server using TLS:

const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const options = {
 key: fs.readFileSync(path.join(__dirname, 'server.key')),
 cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
};
// Create an HTTP/2 server
const server = http2.createSecureServer(options);
// Handle stream events
server.on('stream', (stream, headers) => {
 // Get the path from headers
 const path = headers[':path'];
 // Send a response
 if (path === '/') {
 stream.respond({
 'content-type': 'text/html',
 ':status': 200
 });
 stream.end('<h1>Hello from HTTP/2!</h1>');
 } else {
 stream.respond({
 ':status': 404
 });
 stream.end('Not found');
}
});
// Start the server
const port = 8080;
server.listen(port, () => {
 console.log(`HTTP/2 server running at https://localhost:${port}`);
});

You can also create an HTTP/2 server without TLS (for direct HTTP/2 connections without encryption):

const http2 = require('http2');
// Create an HTTP/2 server without TLS
const server = http2.createServer();
server.on('stream', (stream, headers) => {
 stream.respond({
 'content-type': 'text/html',
 ':status': 200
 });
 stream.end('<h1>Hello from HTTP/2 without TLS!</h1>');
});
server.listen(8080);

Most modern browsers only support HTTP/2 over TLS, so the insecure HTTP/2 server will typically only work with dedicated HTTP/2 clients that explicitly support cleartext HTTP/2.

HTTP/2 Client

Creating an HTTP/2 client to connect to an HTTP/2 server:

const http2 = require('http2');
// Create a client
const client = http2.connect('https://localhost:8080', {
 // For self-signed certificates in development
 rejectUnauthorized: false
});
// Error handling
client.on('error', (err) => {
 console.error('Client error:', err);
});
// Create a request
const req = client.request({ ':path': '/' });
// Handle response data
req.on('response', (headers) => {
 console.log('Status:', headers[':status']);
 console.log('Headers:', headers);
});
req.on('data', (chunk) => {
 console.log('Received data:', chunk.toString());
});
req.on('end', () => {
 console.log('Request completed');
 client.close();
});
// Send the request
req.end();

HTTP/2 Streams

HTTP/2 uses streams for communication between client and server. Each stream represents an independent, bidirectional sequence of frames exchanged between the client and server.

Stream Events

Important stream events include

  • 'headers' : Emitted when headers are received
  • 'data' : Emitted when a chunk of data is received
  • 'end' : Emitted when the stream is finished
  • 'error' : Emitted when an error occurs
const http2 = require('http2');
const fs = require('fs');
const path = require('path');
// Create a server
const server = http2.createSecureServer({
 key: fs.readFileSync(path.join(__dirname, 'server.key')),
 cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
});
server.on('stream', (stream, headers) => {
 // Handle stream events
 stream.on('error', (error) => {
 console.error('Stream error:', error);
 });
 stream.on('close', () => {
 console.log('Stream closed');
 });
 // Handle request
 stream.respond({
 'content-type': 'text/plain',
 ':status': 200
 });
 // Send data in multiple chunks
 stream.write('First chunk of data\n');
 setTimeout(() => {
 stream.write('Second chunk of data\n');
 stream.end('Final chunk of data');
 }, 1000);
});
server.listen(8080);

HTTP/2 Server Push

Server push allows the server to proactively send resources to the client before they are explicitly requested. This can improve performance by eliminating round-trip delays.

const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const options = {
 key: fs.readFileSync(path.join(__dirname, 'server.key')),
 cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
};
const server = http2.createSecureServer(options);
server.on('stream', (stream, headers) => {
 const requestPath = headers[':path'];
 if (requestPath === '/') {
 // Push CSS and JavaScript resources
 stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
 if (err) {
 console.error('Error pushing stream:', err);
 return;
 }
 pushStream.respond({
 'content-type': 'text/css',
 ':status': 200
 });
 pushStream.end('body { color: blue; }');
 });
 stream.pushStream({ ':path': '/script.js' }, (err, pushStream) => {
 if (err) {
 console.error('Error pushing stream:', err);
 return;
 }
 pushStream.respond({
 'content-type': 'application/javascript',
 ':status': 200
 });
 pushStream.end('console.log("Hello from HTTP/2 server push!");');
 });
 // Send the main HTML document
 stream.respond({
 'content-type': 'text/html',
 ':status': 200
 });
 stream.end(`
 <!DOCTYPE html>
 <html>
 <head>
 <title>HTTP/2 Server Push Example</title>
 <link rel="stylesheet" href="/style.css">
 <script src="/script.js"></script>
 </head>
 <body>
 <h1>HTTP/2 Server Push Demo</h1>
 <p>CSS and JavaScript were pushed by the server!</p>
 </body>
 </html>
 `);
 } else {
 // Serve pushed resources if requested directly
 if (requestPath === '/style.css') {
 stream.respond({
 'content-type': 'text/css',
 ':status': 200
 });
 stream.end('body { color: blue; }');
 } else if (requestPath === '/script.js') {
 stream.respond({
 'content-type': 'application/javascript',
 ':status': 200
 });
 stream.end('console.log("Hello from HTTP/2 server push!");');
 } else {
 // Not found
 stream.respond({ ':status': 404 });
 stream.end('Not found');
 }
 }
});
server.listen(8080);

HTTP/2 Headers

HTTP/2 uses a different format for headers. Notably, all headers are lowercase, and request pseudo-headers start with a colon (:).

const http2 = require('http2');
// HTTP/2 pseudo-headers
const {
 HTTP2_HEADER_METHOD,
 HTTP2_HEADER_PATH,
 HTTP2_HEADER_AUTHORITY,
 HTTP2_HEADER_SCHEME,
 HTTP2_HEADER_STATUS
} = http2.constants;
// Create a client
const client = http2.connect('https://localhost:8080', {
 rejectUnauthorized: false
});
// Send a request with custom headers
const req = client.request({
 [HTTP2_HEADER_METHOD]: 'GET',
 [HTTP2_HEADER_PATH]: '/',
 [HTTP2_HEADER_AUTHORITY]: 'localhost:8080',
 [HTTP2_HEADER_SCHEME]: 'https',
 'user-agent': 'node-http2/client',
 'custom-header': 'custom-value'
});
req.on('response', (headers) => {
 console.log('Response status:', headers[HTTP2_HEADER_STATUS]);
 console.log('Response headers:', headers);
});
req.on('data', (chunk) => {
 console.log('Received data:', chunk.toString());
});
req.on('end', () => {
 client.close();
});
req.end();

HTTP/2 Settings

HTTP/2 allows configuring various protocol settings

const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const options = {
 key: fs.readFileSync(path.join(__dirname, 'server.key')),
 cert: fs.readFileSync(path.join(__dirname, 'server.crt')),
 // HTTP/2 settings
 settings: {
 // Max concurrent streams per connection
 maxConcurrentStreams: 100,
 // Initial window size for flow control
 initialWindowSize: 1024 * 1024, // 1MB
 // Enable server push
 enablePush: true
 }
};
const server = http2.createSecureServer(options);
server.on('stream', (stream, headers) => {
 stream.respond({
 'content-type': 'text/html',
 ':status': 200
 });
 stream.end('<h1>HTTP/2 Server with Custom Settings</h1>');
});
server.listen(8080);

Compatibility with HTTP/1.1

HTTP/2 servers can also handle HTTP/1.1 requests, providing a seamless upgrade path:

const http2 = require('http2');
const http = require('http');
const fs = require('fs');
const path = require('path');
// For HTTP/2 secure server
const options = {
 key: fs.readFileSync(path.join(__dirname, 'server.key')),
 cert: fs.readFileSync(path.join(__dirname, 'server.crt')),
 allowHTTP1: true // Allow HTTP/1.1 connections
};
const server = http2.createSecureServer(options);
// Handler function for both HTTP/1.1 and HTTP/2
const handler = (req, res) => {
 res.writeHead(200, { 'Content-Type': 'text/plain' });
 res.end(`Hello from ${req.httpVersion} server!`);
};
// HTTP/1.1 compatibility request handler
server.on('request', handler);
// HTTP/2 specific stream handler
server.on('stream', (stream, headers) => {
 stream.respond({
 'content-type': 'text/plain',
 ':status': 200
 });
 stream.end(`Hello from HTTP/2 stream API!`);
});
server.listen(8080, () => {
 console.log('Server running at https://localhost:8080/');
});

Performance Considerations

While HTTP/2 offers performance improvements, it's important to optimize your usage:

  • Connection Reuse - With HTTP/2, you should aim to use a single connection for multiple requests, rather than creating new connections.
  • Proper Stream Management - Remember to close streams when they're no longer needed, and monitor the number of concurrent streams.
  • Server Push Strategy - Only push resources that are likely to be needed. Excessive pushing can waste bandwidth and resources.
  • Header Compression - Take advantage of HTTP/2's header compression by minimizing the number and size of custom headers.

HTTP/2 vs HTTP/1.1

Key differences between HTTP/2 and HTTP/1.1:

FeatureHTTP/1.1HTTP/2
Protocol FormatText-basedBinary-based
MultiplexingNo (requires multiple connections)Yes (multiple streams over one connection)
Header CompressionNoneYes (HPACK)
Server PushNoYes
Flow ControlBasicAdvanced, per-stream
PrioritizationNoYes

Real-World Example: Serving a Complete Website

A complete example of serving a website with HTTP/2:

const http2 = require('http2');
const fs = require('fs');
const path = require('path');
const mime = require('mime-types');
const options = {
 key: fs.readFileSync(path.join(__dirname, 'server.key')),
 cert: fs.readFileSync(path.join(__dirname, 'server.crt'))
};
const server = http2.createSecureServer(options);
// Serve files from the public directory
const publicDir = path.join(__dirname, 'public');
server.on('stream', (stream, headers) => {
 const reqPath = headers[':path'] === '/' ? '/index.html' : headers[':path'];
 const filePath = path.join(publicDir, reqPath);
 // Basic security check to prevent path traversal
 if (!filePath.startsWith(publicDir)) {
 stream.respond({ ':status': 403 });
 stream.end('Forbidden');
 return;
 }
 fs.stat(filePath, (err, stats) => {
 if (err || !stats.isFile()) {
 // File not found
 stream.respond({ ':status': 404 });
 stream.end('Not found');
 return;
 }
 // Determine content type
 const contentType = mime.lookup(filePath) || 'application/octet-stream';
 // Serve the file
 stream.respond({
 'content-type': contentType,
 ':status': 200
 });
 const fileStream = fs.createReadStream(filePath);
 fileStream.pipe(stream);
 fileStream.on('error', (err) => {
 console.error('File stream error:', err);
 stream.close(http2.constants.NGHTTP2_INTERNAL_ERROR);
 });
 });
});
server.listen(8080, () => {
 console.log('HTTP/2 server running at https://localhost:8080/');
});

This example requires the mime-types package

npm install mime-types

Advanced Stream Management

HTTP/2's stream management capabilities allow for efficient handling of multiple concurrent requests. Here's an advanced example demonstrating stream prioritization and flow control:

const http2 = require('http2');
const fs = require('fs');
// Create a server with custom settings
const server = http2.createSecureServer({
 key: fs.readFileSync('server.key'),
 cert: fs.readFileSync('server.crt'),
 settings: {
 initialWindowSize: 65535, // 64KB initial window
 maxConcurrentStreams: 100,
 enablePush: true
 }
});
server.on('stream', (stream, headers) => {
 // Get priority information
 const weight = stream.priority && stream.priority.weight || 1;
 const parent = stream.priority && stream.priority.parent ? 'with parent' : 'no parent';
 console.log(`New stream ${stream.id} (weight: ${weight}, ${parent})`);
 // Handle different priority levels
 if (headers[':path'] === '/high-priority') {
 stream.priority({ weight: 256, exclusive: true });
 stream.respond({ ':status': 200, 'content-type': 'text/plain' });
 stream.end('High priority content');
 } else {
 // Default priority
 stream.respond({ ':status': 200, 'content-type': 'text/plain' });
 stream.end('Standard priority content');
}
// Handle stream errors
stream.on('error', (error) => {
 console.error(`Stream ${stream.id} error:`, error);
 stream.end();
});
// Handle stream close
stream.on('close', () => {
 console.log(`Stream ${stream.id} closed`);
});
});
server.listen(8443);

Error Handling and Debugging

Proper error handling is crucial for reliable HTTP/2 applications. Here's how to implement comprehensive error handling:

const http2 = require('http2');
const fs = require('fs');
const { promisify } = require('util');
const readFile = promisify(fs.readFile);
async function startServer() {
 try {
 const [key, cert] = await Promise.all([
 readFile('server.key'),
 readFile('server.crt')
 ]);
 const server = http2.createSecureServer({ key, cert });
 // Global error handler
 server.on('error', (err) => {
 console.error('Server error:', err);
 // Implement proper error recovery
 });
 // Handle uncaught exceptions
 process.on('uncaughtException', (err) => {
 console.error('Uncaught exception:', err);
 // Graceful shutdown
 server.close(() => process.exit(1));
 });
 // Handle unhandled promise rejections
 process.on('unhandledRejection', (reason, promise) => {
 console.error('Unhandled Rejection at:', promise, 'reason:', reason);
 });
 // Stream handler with error boundaries
 server.on('stream', (stream, headers) => {
 try {
 // Simulate async operation
 setTimeout(() => {
 try {
 if (Math.random() > 0.8) {
 throw new Error('Random error for demonstration');
 }
 stream.respond({ ':status': 200 });
 stream.end('Success!');
 } catch (err) {
 handleStreamError(stream, err);
 }
 }, 100);
 } catch (err) {
 handleStreamError(stream, err);
 }
});
function handleStreamError(stream, error) {
 console.error('Stream error:', error);
 if (!stream.destroyed) {
 stream.respond({
 ':status': 500,
 'content-type': 'text/plain'
 });
 stream.end('Internal Server Error');
 }
}
server.listen(8443, () => {
 console.log('Server running on https://localhost:8443');
});
} catch (err) {
console.error('Failed to start server:', err);
process.exit(1);
}
}
startServer();

Performance Optimization

Optimizing HTTP/2 performance requires understanding its unique characteristics. Here are key strategies:

Connection Pooling

const http2 = require('http2');
const { URL } = require('url');
class HTTP2ConnectionPool {
 constructor() {
 this.connections = new Map();
 }
 async getConnection(url) {
 const { origin } = new URL(url);
 if (!this.connections.has(origin)) {
 const client = http2.connect(origin, {
 rejectUnauthorized: false // Only for development
 });
 // Handle connection errors
 client.on('error', (err) => {
 console.error('Connection error:', err);
 this.connections.delete(origin);
 });
 // Remove connection when closed
 client.on('close', () => {
 this.connections.delete(origin);
 });
 this.connections.set(origin, {
 client,
 lastUsed: Date.now(),
 inUse: 0
 });
 }
 const conn = this.connections.get(origin);
 conn.lastUsed = Date.now();
 conn.inUse++;
 return {
 client: conn.client,
 release: () => {
 conn.inUse--;
 }
 };
 }
 // Clean up idle connections
 startCleanup(interval = 30000) {
 setInterval(() => {
 const now = Date.now();
 for (const [origin, conn] of this.connections.entries()) {
 if (conn.inUse === 0 && (now - conn.lastUsed) > 60000) {
 conn.client.destroy();
 this.connections.delete(origin);
 }
 }
 }, interval);
 }
}
// Usage example
const pool = new HTTP2ConnectionPool();
pool.startCleanup();
async function makeRequest(url) {
 const { client, release } = await pool.getConnection(url);
 return new Promise((resolve, reject) => {
 const req = client.request({ ':path': new URL(url).pathname });
 let data = '';
 req.on('response', (headers) => {
 console.log('Status:', headers[':status']);
 });
 req.on('data', (chunk) => data += chunk);
 req.on('end', () => {
 release();
 resolve(data);
 });
 req.on('error', (err) => {
 release();
 reject(err);
 });
 req.end();
 });
}

Header Compression Optimization

HTTP/2 uses HPACK compression for headers. Optimize by:

  • Minimizing cookie sizes
  • Using short but descriptive header names
  • Avoiding duplicate headers
  • Using HTTP/2-specific headers when possible

Security Best Practices

When using HTTP/2, follow these security practices:

const http2 = require('http2');
const fs = require('fs');
const { createSecureContext } = require('tls');
// Security headers middleware
function securityHeaders(req, res, next) {
 // Set security headers
 res.setHeader('X-Content-Type-Options', 'nosniff');
 res.setHeader('X-Frame-Options', 'DENY');
 res.setHeader('X-XSS-Protection', '1; mode=block');
 res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
 res.setHeader('Content-Security-Policy', "default-src 'self'");
 // Remove server header
 res.removeHeader('X-Powered-By');
 next();
}
// Create secure server with modern TLS settings
const options = {
 key: fs.readFileSync('server.key'),
 cert: fs.readFileSync('server.crt'),
 secureOptions:
 require('constants').SSL_OP_NO_SSLv3 |
 require('constants').SSL_OP_NO_TLSv1 |
 require('constants').SSL_OP_NO_TLSv1_1,
 ciphers: [
 'TLS_AES_256_GCM_SHA384',
 'TLS_CHACHA20_POLY1305_SHA256',
 'TLS_AES_128_GCM_SHA256'
 ].join(':'),
 minVersion: 'TLSv1.3',
 maxVersion: 'TLSv1.3',
 // OCSP Stapling
 requestCert: false,
 rejectUnauthorized: true
};
const server = http2.createSecureServer(options);
// Apply security middleware
server.on('request', (req, res) => {
 securityHeaders(req, res, () => {
 // Request handling logic
 res.setHeader('Content-Type', 'text/plain');
 res.end('Secure HTTP/2 Response');
 });
});
// Handle TLS errors
server.on('tlsClientError', (err, tlsSocket) => {
 console.error('TLS Error:', err);
 tlsSocket.destroy();
});
server.listen(8443);

API Gateway with HTTP/2

Building a high-performance API gateway with HTTP/2:

const http2 = require('http2');
const { URL } = require('url');
const path = require('path');
const fs = require('fs');
// Service registry
const services = {
 '/users': 'http://users-service:3000',
 '/products': 'http://products-service:3000',
 '/orders': 'http://orders-service:3000'
};
// Create HTTP/2 server
const server = http2.createSecureServer({
 key: fs.readFileSync('server.key'),
 cert: fs.readFileSync('server.crt')
});
// Route requests to appropriate services
server.on('stream', (stream, headers) => {
 const path = headers[':path'];
 const method = headers[':method'];
 try {
 // Find matching service
 const servicePath = Object.keys(services).find(prefix =>
 path.startsWith(prefix)
 );
 if (!servicePath) {
 stream.respond({ ':status': 404 });
 return stream.end('Not Found');
 }
 const targetUrl = new URL(path.slice(servicePath.length), services[servicePath]);
 // Forward request to target service
 const client = http2.connect(targetUrl.origin);
 const req = client.request({
 ...headers,
 ':path': targetUrl.pathname + targetUrl.search,
 ':method': method,
 ':authority': targetUrl.host
 });
 // Pipe the response back to client
 req.pipe(stream);
 stream.pipe(req);
 // Handle errors
 req.on('error', (err) => {
 console.error('Request error:', err);
 if (!stream.destroyed) {
 stream.respond({ ':status': 502 });
 stream.end('Bad Gateway');
 }
 });
 stream.on('error', (err) => {
 console.error('Stream error:', err);
 req.destroy();
 });
} catch (err) {
console.error('Gateway error:', err);
if (!stream.destroyed) {
 stream.respond({ ':status': 500 });
 stream.end('Internal Server Error');
}
}
});
server.listen(443);

Real-Time Data Streaming

Efficient real-time data streaming with HTTP/2

const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
 key: fs.readFileSync('server.key'),
 cert: fs.readFileSync('server.crt')
});
// In-memory storage for active streams
const streams = new Set();
// Broadcast data to all connected clients
function broadcast(data) {
 const payload = JSON.stringify(data);
 for (const stream of streams) {
 try {
 stream.write(`data: ${payload}\n\n`);
 } catch (err) {
 console.error('Stream write error:', err);
 streams.delete(stream);
 }
}
}
// Simulate data updates
setInterval(() => {
 broadcast({
 time: new Date().toISOString(),
 value: Math.random() * 100
 });
}, 1000);
server.on('stream', (stream, headers) => {
 // Only handle GET requests
 if (headers[':method'] !== 'GET') {
 stream.respond({ ':status': 405 });
 return stream.end();
 }
 // Set up Server-Sent Events headers
 stream.respond({
 'content-type': 'text/event-stream',
 'cache-control': 'no-cache',
 'connection': 'keep-alive',
 ':status': 200
 });
 // Add to active streams
 streams.add(stream);
 // Handle client disconnect
 stream.on('close', () => {
 streams.delete(stream);
 });
 // Send initial data
 stream.write('event: connect\ndata: Connected\n\n');
});
server.listen(8443, () => {
 console.log('HTTP/2 Server running on https://localhost:8443');
});

Previous

Node.js WebAssembly

Next

Node.js Performance Hooks Module