HTTP/2 protocol with multiplexing, server push, header compression, and stream prioritization
Scope: HTTP/2 binary protocol, multiplexing, server push, header compression (HPACK) Lines: ~360 Last Updated: 2025-10-27 Format Version: 1.0 (Atomic)
Activate this skill when:
HTTP/1.1 Issues:
HTTP/2 Solutions:
Connection Model:
HTTP/1.1: HTTP/2:
┌─────┐ ┌─────────────┐
│Req 1│───────────>│Stream 1 ────┤
└─────┘ │Stream 2 ────┤
┌─────┐ │Stream 3 ────┼──> Single TCP Connection
│Req 2│───────────>│Stream 4 ────┤
└─────┘ │Stream 5 ────┤
(sequential) └─────────────┘
(multiplexed)
Frame Types:
Frame Structure:
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
Single Connection, Multiple Streams:
# HTTP/1.1 - requires 6 connections
conn1.request('GET', '/style.css')
conn2.request('GET', '/script.js')
conn3.request('GET', '/image1.png')
conn4.request('GET', '/image2.png')
conn5.request('GET', '/image3.png')
conn6.request('GET', '/data.json')
# HTTP/2 - single connection
h2_conn = h2.connection.H2Connection()
h2_conn.send_headers(stream_id=1, headers=[(':path', '/style.css')])
h2_conn.send_headers(stream_id=3, headers=[(':path', '/script.js')])
h2_conn.send_headers(stream_id=5, headers=[(':path', '/image1.png')])
h2_conn.send_headers(stream_id=7, headers=[(':path', '/image2.png')])
h2_conn.send_headers(stream_id=9, headers=[(':path', '/image3.png')])
h2_conn.send_headers(stream_id=11, headers=[(':path', '/data.json')])
Benefits:
Concept: Server proactively sends resources client will need
Client Server
| |
|-- GET /index.html --------------->|
| |
|<-- PUSH_PROMISE /style.css -------|
|<-- PUSH_PROMISE /script.js -------|
| |
|<-- HEADERS (index.html) -----------|
|<-- DATA (index.html content) ------|
| |
|<-- HEADERS (style.css) ------------|
|<-- DATA (style.css content) -------|
| |
|<-- HEADERS (script.js) ------------|
|<-- DATA (script.js content) -------|
Nginx Configuration:
server {
listen 443 ssl http2;
server_name example.com;
location = /index.html {
http2_push /css/style.css;
http2_push /js/script.js;
http2_push /images/logo.png;
}
}
Node.js Implementation:
const http2 = require('http2');
const fs = require('fs');
const server = http2.createSecureServer({
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem')
});
server.on('stream', (stream, headers) => {
if (headers[':path'] === '/') {
// Push CSS before sending HTML
stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
pushStream.respond({ ':status': 200, 'content-type': 'text/css' });
pushStream.end(fs.readFileSync('style.css'));
});
// Send HTML
stream.respond({ ':status': 200, 'content-type': 'text/html' });
stream.end('<html><link rel="stylesheet" href="/style.css"></html>');
}
});
server.listen(8443);
Problem: HTTP/1.1 headers are redundant
# Request 1
GET /api/users/1 HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 ...
Accept: application/json
Authorization: Bearer token123
# Request 2 - same headers repeated!
GET /api/users/2 HTTP/1.1
Host: api.example.com
User-Agent: Mozilla/5.0 ...
Accept: application/json
Authorization: Bearer token123
HPACK Solution:
Example Encoding:
First request:
:method: GET -> Index 2 (static table)
:path: /api/users/1 -> Literal, added to dynamic table (index 62)
host: api.example.com -> Literal, added to dynamic table (index 63)
authorization: Bearer... -> Literal, added to dynamic table (index 64)
Second request:
:method: GET -> Index 2
:path: /api/users/2 -> Literal (path changed)
host: api.example.com -> Index 63 (dynamic table)
authorization: Bearer... -> Index 64 (dynamic table)
Compression Ratio: 85-95% size reduction typical
Use Case: Critical resources load first
// Node.js HTTP/2 client
const http2 = require('http2');
const client = http2.connect('https://example.com');
// High priority - CSS (weight 256)
const cssStream = client.request({
':path': '/critical.css'
}, { weight: 256 });
// Medium priority - JavaScript (weight 128)
const jsStream = client.request({
':path': '/app.js'
}, { weight: 128 });
// Low priority - Images (weight 64)
const imgStream = client.request({
':path': '/background.jpg'
}, { weight: 64 });
Benefits:
Use Case: Only push if client doesn't have cached resource
server.on('stream', (stream, headers) => {
const cookieHeader = headers['cookie'] || '';
const hasCache = cookieHeader.includes('has_css=1');
if (!hasCache) {
// Push CSS only if client doesn't have it
stream.pushStream({ ':path': '/style.css' }, (err, pushStream) => {
pushStream.respond({
':status': 200,
'content-type': 'text/css',
'cache-control': 'public, max-age=31536000'
});
pushStream.end(cssContent);
});
// Set cookie to indicate client has CSS
stream.respond({
':status': 200,
'set-cookie': 'has_css=1; Max-Age=31536000'
});
}
stream.end(htmlContent);
});
Use Case: Prevent overwhelming slow clients
# Python HTTP/2 with flow control
import h2.connection
import h2.events
conn = h2.connection.H2Connection()
conn.initiate_connection()
# Set initial window size
conn.update_settings({
h2.settings.SettingCodes.INITIAL_WINDOW_SIZE: 65535
})
# Send data respecting flow control
stream_id = 1
data = b"Large payload..."
while data:
# Check how much we can send
max_send = min(len(data), conn.local_settings.initial_window_size)
# Send chunk
conn.send_data(stream_id, data[:max_send])
data = data[max_send:]
# Wait for WINDOW_UPDATE if needed
if len(data) > 0:
events = conn.receive_data(socket.recv(4096))
for event in events:
if isinstance(event, h2.events.WindowUpdated):
# Can send more data now
pass
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Check if HTTP/2
if r.ProtoMajor == 2 {
fmt.Printf("HTTP/2 request: %s\n", r.URL.Path)
}
// Server push
if pusher, ok := w.(http.Pusher); ok {
// Push CSS before HTML
if err := pusher.Push("/style.css", nil); err != nil {
log.Printf("Failed to push: %v", err)
}
}
w.Header().Set("Content-Type", "text/html")
w.Write([]byte("<html><link rel='stylesheet' href='/style.css'></html>"))
})
mux.HandleFunc("/style.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Write([]byte("body { font-family: sans-serif; }"))
})
log.Fatal(http.ListenAndServeTLS(":8443", "cert.pem", "key.pem", mux))
}
use hyper::{Body, Client, Request};
use hyper::client::HttpConnector;
use hyper_tls::HttpsConnector;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create HTTP/2 client
let https = HttpsConnector::new();
let client = Client::builder()
.http2_only(true)
.build::<_, Body>(https);
// Make multiple concurrent requests (multiplexed)
let urls = vec![
"https://example.com/api/users/1",
"https://example.com/api/users/2",
"https://example.com/api/users/3",
];
let futures: Vec<_> = urls.into_iter()
.map(|url| {
let req = Request::get(url).body(Body::empty()).unwrap();
client.request(req)
})
.collect();
// All requests use same connection
let responses = futures::future::join_all(futures).await;
for response in responses {
println!("Status: {}", response?.status());
}
Ok(())
}
# ✅ Good: Enable HTTP/2
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
}
// ❌ Bad: Pushing everything
stream.pushStream({ ':path': '/rarely-used-library.js' }, ...);
// ✅ Good: Only push critical resources
stream.pushStream({ ':path': '/critical.css' }, ...);
# Same IP, same certificate -> reuse connection
https://example.com (IP: 1.2.3.4)
https://www.example.com (IP: 1.2.3.4)
https://api.example.com (IP: 1.2.3.4)
└─> All use same HTTP/2 connection
Check Protocol:
curl -I --http2 https://example.com
# Look for: HTTP/2 200
# Or
openssl s_client -connect example.com:443 -alpn h2
# Look for: ALPN protocol: h2
Symptom: Pushed resources not cached Solution: Check browser support and cache headers
// Ensure proper cache headers on pushed resources
pushStream.respond({
':status': 200,
'cache-control': 'public, max-age=31536000',
'content-type': 'text/css'
});
This skill includes executable scripts, reference documentation, and configuration examples in the resources/ directory.
resources/scripts/)benchmark_http2.py: Compare HTTP/1.1 and HTTP/2 performance
# Benchmark 50 requests
python resources/scripts/benchmark_http2.py --url https://example.com --requests 50
# JSON output
python resources/scripts/benchmark_http2.py --url https://example.com --json results.json
analyze_hpack.py: Analyze HPACK header compression efficiency
# Demo with sample requests
python resources/scripts/analyze_hpack.py --demo
# Analyze custom headers
python resources/scripts/analyze_hpack.py --requests headers.txt --verbose
test_server_push.sh: Test HTTP/2 server push functionality
# Test server push support
./resources/scripts/test_server_push.sh --url https://example.com
# Detailed analysis with nghttp
./resources/scripts/test_server_push.sh --url https://example.com --verbose
resources/REFERENCE.md)resources/examples/)Nginx (resources/examples/nginx/http2.conf):
Node.js (resources/examples/node/http2-server.js):
Python (resources/examples/python/http2_client.py):
See resources/scripts/README.md for complete documentation.
protocols-http-fundamentals - HTTP/1.1 basicsprotocols-http3-quic - HTTP/3 and QUICproxies-nginx-configuration - Nginx HTTP/2 setupproxies-envoy-proxy - Envoy HTTP/2 configurationfrontend-performance - Front-end performance optimizationLast Updated: 2025-10-27