What are the trade-offs between hyper::server::conn::Http and Http2 for protocol-specific server configuration?

Http is a generic connection service that handles both HTTP/1 and HTTP/2 with automatic protocol detection, while Http2 is a specialized service builder that enforces HTTP/2-only connections with explicit configuration for HTTP/2-specific features like prior knowledge, H2C (HTTP/2 cleartext), and stream window sizes. The trade-off is flexibility versus control: Http provides automatic protocol negotiation with sensible defaults, while Http2 gives precise control over HTTP/2 behavior at the cost of HTTP/1 compatibility.

The Connection Service Architecture

use hyper::server::conn::{Http, Http2};
use hyper::{Request, Response, Body};
use tokio::net::TcpListener;
 
// Both Http and Http2 are "connection service builders"
// They configure how incoming connections are handled
 
async fn connection_architecture() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let (stream, _addr) = listener.accept().await.unwrap();
    
    // Http handles protocol detection
    let http = Http::new();
    // Inspects the connection and chooses HTTP/1 or HTTP/2
    
    // Http2 enforces HTTP/2
    let http2 = Http2::new();
    // Requires HTTP/2, fails otherwise
}

Both types are connection builders that configure how hyper handles incoming TCP connections.

Http: The Flexible Protocol Handler

use hyper::server::conn::Http;
use hyper::{Server, Request, Response, Body};
use tokio::net::TcpListener;
 
async fn http_basic() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // Http::new() creates a connection service with defaults
    let http = Http::new();
    
    // It automatically detects HTTP/1 vs HTTP/2:
    // - HTTP/1.1: Plain text requests
    // - HTTP/2: Via ALPN negotiation (TLS) or HTTP/1 upgrade
    
    // Serve with automatic protocol handling
    let server = http.serve_connection(stream, service);
    
    // Benefits:
    // - Single code path for both protocols
    // - Automatic ALPN negotiation with TLS
    // - No need to know protocol in advance
}

Http automatically handles both HTTP/1 and HTTP/2 based on client negotiation.

Http2: The Strict HTTP/2 Handler

use hyper::server::conn::Http2;
use hyper::{Server, Request, Response, Body};
use tokio::net::TcpListener;
 
async fn http2_basic() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // Http2::new() creates a connection service for HTTP/2 only
    let http2 = Http2::new();
    
    // Enforces HTTP/2 on the connection
    // Useful for:
    // - HTTP/2 prior knowledge (no protocol negotiation)
    // - HTTP/2 cleartext (h2c without TLS)
    // - Strict HTTP/2-only servers
    
    // Serve with HTTP/2 enforcement
    let server = http2.serve_connection(stream, service);
    
    // Benefits:
    // - Explicit HTTP/2 configuration
    // - Prior knowledge mode
    // - h2c (HTTP/2 without TLS)
    // - Fine-grained HTTP/2 settings
}

Http2 enforces HTTP/2 and provides HTTP/2-specific configuration options.

Protocol Detection Behavior

use hyper::server::conn::{Http, Http2};
 
fn protocol_detection() {
    // HTTP/1.1 over plain TCP
    // Client sends: "GET / HTTP/1.1\r\n..."
    // Http: Detects HTTP/1, handles accordingly
    // Http2: Would reject or fail to parse
    
    // HTTP/2 over TLS with ALPN
    // Client sends TLS ClientHello with ALPN extension
    // Server negotiates "h2" or "http/1.1"
    // Http: Respects ALPN result
    // Http2: Requires "h2" selection
    
    // HTTP/2 prior knowledge (no negotiation)
    // Client sends HTTP/2 connection preface
    // Http: Can handle via http2_only flag
    // Http2: Always expects this
    
    // HTTP/2 cleartext (h2c)
    // Client starts with HTTP/1 upgrade or preface
    // Http: Can upgrade from HTTP/1 to HTTP/2
    // Http2: Expects direct HTTP/2 preface
    
    // Key difference:
    // Http: Negotiates protocol
    // Http2: Assumes HTTP/2
}

Http negotiates; Http2 assumes HTTP/2 from the start.

Configuration Options Compared

use hyper::server::conn::{Http, Http2};
use std::time::Duration;
 
fn configuration_options() {
    // Http configuration
    let http = Http::new()
        .http1_only(false)           // Allow HTTP/2
        .http1_keep_alive(true)       // HTTP/1 keep-alive
        .http1_title_case_headers(false)
        .http1_preserve_header_case(false)
        .http2_only(false)            // Allow HTTP/1
        .http2_initial_stream_window_size(65535)
        .http2_initial_connection_window_size(65535)
        .http2_adaptive_window(true)  // Adaptive flow control
        .http2_max_frame_size(16384);
    
    // Http2 configuration (HTTP/2 specific)
    let http2 = Http2::new()
        .initial_stream_window_size(65535)
        .initial_connection_window_size(65535)
        .adaptive_window(true)
        .max_frame_size(16384)
        .max_concurrent_streams(100)
        .max_send_buffer_size(1024 * 1024)
        .enable_connect_protocol(false);
    
    // Key differences:
    // Http: Has http1_* and http2_* methods
    // Http2: Only has HTTP/2-specific settings
    
    // Http2-specific options not on Http:
    // - enable_connect_protocol (extended CONNECT)
    // - Some advanced HTTP/2 settings
}

Http has mixed protocol options; Http2 focuses on HTTP/2-specific settings.

HTTP/2 Prior Knowledge

use hyper::server::conn::Http2;
use tokio::net::TcpListener;
 
// Prior knowledge: Both sides know HTTP/2 will be used
// No protocol negotiation needed
 
async fn prior_knowledge() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let (stream, _addr) = listener.accept().await.unwrap();
    
    // Client MUST send HTTP/2 connection preface immediately
    // Server expects HTTP/2 from the start
    
    let http2 = Http2::new();
    
    // Use Http2 for:
    // 1. Known HTTP/2-only clients
    // 2. gRPC servers (gRPC requires HTTP/2)
    // 3. Internal services with prior knowledge
    // 4. HTTP/2 without TLS (h2c)
    
    // With Http, you'd need:
    // Http::new().http2_only(true)
    // But Http2 is more explicit and cleaner
    
    // Prior knowledge setup requires:
    // - Client knows to send HTTP/2 preface
    // - No TLS handshake with ALPN
    // - No HTTP/1 fallback
}

Http2 is designed for prior knowledge scenarios where HTTP/2 is guaranteed.

H2C: HTTP/2 Without TLS

use hyper::server::conn::Http2;
use tokio::net::TcpListener;
 
// h2c: HTTP/2 over cleartext TCP
// Useful for internal services, development, load balancers
 
async fn h2c_server() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // For h2c, use Http2
    // Client must either:
    // 1. Send HTTP/2 connection preface directly
    // 2. Start with HTTP/1 and upgrade (h2c upgrade)
    
    let http2 = Http2::new();
    
    // Some clients support h2c:
    // - curl --http2-prior-knowledge
    // - gRPC clients without TLS
    // - Internal microservices
    
    // Why use h2c?
    // - Development without cert setup
    // - Internal networks (already secure)
    // - Behind TLS-terminating load balancers
    // - Performance (no TLS overhead)
    
    // Security consideration:
    // - h2c sends unencrypted HTTP/2
    // - Only use in trusted environments
}

Http2 enables h2c servers without TLS overhead.

Window Size Configuration

use hyper::server::conn::{Http, Http2};
 
fn window_size_config() {
    // HTTP/2 uses flow control with window sizes
    
    // Stream window: Per-stream data buffering
    // Connection window: Total connection buffering
    
    // Smaller windows: Less memory, more flow control overhead
    // Larger windows: More memory, less overhead
    
    // Http configuration
    let http = Http::new()
        .http2_initial_stream_window_size(65535)
        .http2_initial_connection_window_size(65535);
    
    // Http2 configuration
    let http2 = Http2::new()
        .initial_stream_window_size(65535)
        .initial_connection_window_size(65535);
    
    // Adaptive window adjusts dynamically based on usage
    let http2_adaptive = Http2::new()
        .adaptive_window(true);
    
    // Use cases:
    // - Large files: Increase window size
    // - Many streams: Decrease per-stream, increase connection
    // - Memory-constrained: Smaller windows
}

Window sizes control HTTP/2 flow control behavior, affecting memory and throughput.

Max Frame Size

use hyper::server::conn::{Http, Http2};
 
fn frame_size_config() {
    // HTTP/2 splits data into frames (max 2^24 - 1 bytes)
    // Smaller frames: More granular, more overhead
    // Larger frames: Less overhead, more buffering
    
    // Default frame size: 16384 bytes (2^14)
    
    let http = Http::new()
        .http2_max_frame_size(16384);  // Default
    
    let http2 = Http2::new()
        .max_frame_size(16384);
    
    // Valid range: 16384 to 16777215 (2^14 to 2^24 - 1)
    
    // Larger frame size benefits:
    // - Less per-frame overhead
    // - Better for large payloads
    
    // Smaller frame size benefits:
    // - More responsive interleaving
    // - Better for many concurrent streams
    
    // Note: Setting too large may cause compatibility issues
    // Most clients expect reasonable frame sizes
}

Frame size affects HTTP/2's interleaving behavior and overhead.

Concurrent Streams

use hyper::server::conn::Http2;
 
fn concurrent_streams() {
    // HTTP/2 allows multiple concurrent streams per connection
    
    let http2 = Http2::new()
        .max_concurrent_streams(100);
    
    // Controls how many streams can be open simultaneously
    
    // Lower limit:
    // - Less resource usage per connection
    // - Protection against stream exhaustion
    // - Better isolation between clients
    
    // Higher limit:
    // - More parallelism per connection
    // - Better for clients making many requests
    // - More memory per connection
    
    // Default is typically 200 (varies by implementation)
    
    // Consider setting based on:
    // - Expected requests per client
    // - Server memory capacity
    // - Client count
}

max_concurrent_streams limits resource usage per connection.

Extended CONNECT Protocol

use hyper::server::conn::Http2;
 
fn extended_connect() {
    // HTTP/2 CONNECT method can be extended for WebSocket
    // RFC 8441 defines "Extended CONNECT Protocol"
    
    let http2 = Http2::new()
        .enable_connect_protocol(true);
    
    // With extended CONNECT:
    // - Client can upgrade to WebSocket over HTTP/2
    // - Uses CONNECT with :protocol pseudo-header
    // - More efficient than HTTP/1 WebSocket upgrade
    
    // Use case:
    // - WebSocket over HTTP/2
    // - gRPC-Web with streaming
    // - Custom protocols over HTTP/2
    
    // Note: Requires client support for extended CONNECT
    // Only available with Http2, not configurable via Http
}

Extended CONNECT enables WebSocket over HTTP/2, available only with Http2.

Choosing Between Http and Http2

use hyper::server::conn::{Http, Http2};
 
fn choosing() {
    // Use Http when:
    // - Serving to browsers (may use HTTP/1 or HTTP/2)
    // - TLS with ALPN negotiation
    // - Mixed client support
    // - Public internet services
    
    let http = Http::new();  // Flexible, handles both
    
    // Use Http2 when:
    // - gRPC servers (require HTTP/2)
    // - Prior knowledge setup
    // - h2c (HTTP/2 without TLS)
    // - Internal microservices with known protocol
    // - Extended CONNECT for WebSocket over HTTP/2
    
    let http2 = Http2::new();  // Strict HTTP/2
    
    // Use Http::new().http2_only(true) when:
    // - Want HTTP/2 enforcement with Http builder
    // - But still need Http's other configuration
    
    let http_strict = Http::new()
        .http2_only(true);
    
    // This is similar to Http2 but:
    // - Still has http1_* config methods (ignored)
    // - Different defaults
    // - Different internal handling
}

Choose Http for flexibility, Http2 for strict HTTP/2 control.

TLS Integration

use hyper::server::conn::{Http, Http2};
use tokio_rustls::TlsAcceptor;
use tokio::net::TcpListener;
 
async fn tls_integration() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let tls_acceptor = create_tls_acceptor();
    
    loop {
        let (stream, _addr) = listener.accept().await.unwrap();
        let tls_stream = tls_acceptor.accept(stream).await.unwrap();
        
        // With TLS and ALPN, Http works well
        // Client and server negotiate protocol during TLS handshake
        let http = Http::new();
        // http.serve_connection(tls_stream, service).await;
        
        // Http2 can also be used with TLS
        // But requires client to negotiate "h2" in ALPN
        let http2 = Http2::new();
        // http2.serve_connection(tls_stream, service).await;
    }
}
 
fn create_tls_acceptor() -> tokio_rustls::TlsAcceptor {
    // Configure TLS with ALPN
    // ALPN offers: ["h2", "http/1.1"]
    // Server selects based on client offer
    // Implementation details omitted for brevity
    unimplemented!()
}

TLS with ALPN negotiation works naturally with Http; Http2 requires HTTP/2 ALPN selection.

Performance Considerations

use hyper::server::conn::{Http, Http2};
 
fn performance() {
    // Http overhead:
    // - Protocol detection on each connection
    // - Additional branching in hot paths
    // - But minimal in practice
    
    // Http2 overhead:
    // - No protocol detection
    // - Direct HTTP/2 handling
    // - Slightly cleaner code paths
    
    // In practice:
    // - Difference is minimal for most workloads
    // - Protocol detection happens once per connection
    // - HTTP/2's multiplexing benefits outweigh detection cost
    
    // Memory considerations:
    // - Both use similar HTTP/2 internals when HTTP/2
    // - Window sizes affect buffering more than choice
    
    // Choose based on requirements, not performance:
    // - Need HTTP/1 support? Use Http
    // - HTTP/2 only? Either works, Http2 is clearer
}

Performance difference is minimal; choose based on requirements, not benchmarks.

Real-World Example: gRPC Server

use hyper::server::conn::Http2;
use tokio::net::TcpListener;
 
// gRPC requires HTTP/2
// Using Http2 is the natural choice
 
async fn grpc_server() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    let http2 = Http2::new()
        .max_concurrent_streams(100)  // Limit concurrent gRPC calls
        .initial_stream_window_size(1024 * 1024)  // 1MB per stream
        .enable_connect_protocol(false);  // gRPC doesn't need extended CONNECT
    
    loop {
        let (stream, _addr) = listener.accept().await.unwrap();
        
        // gRPC client will send HTTP/2 preface
        // Http2 expects this and handles it properly
        
        // No need for protocol detection
        // No HTTP/1 support needed
    }
}

gRPC servers naturally use Http2 since gRPC requires HTTP/2.

Real-World Example: Mixed Protocol Server

use hyper::server::conn::Http;
use tokio::net::TcpListener;
 
// Public internet server must handle both HTTP/1 and HTTP/2
 
async fn public_server() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    let http = Http::new()
        .http1_keep_alive(true)       // Enable keep-alive for HTTP/1
        .http2_adaptive_window(true); // Adaptive flow control for HTTP/2
    
    loop {
        let (stream, _addr) = listener.accept().await.unwrap();
        
        // With TLS + ALPN:
        // - Browsers negotiate protocol
        // - Http handles whatever client selects
        
        // With plain TCP:
        // - Http detects HTTP/1 vs HTTP/2 preface
        // - Handles both transparently
    }
}

Public servers use Http for broad client compatibility.

Summary Table

fn summary_table() {
    // | Aspect | Http | Http2 |
    // |--------|------|-------|
    // | Protocols | HTTP/1 and HTTP/2 | HTTP/2 only |
    // | Protocol detection | Automatic | Prior knowledge |
    // | h2c support | Via http2_only | Native |
    // | HTTP/1 config | Yes | No |
    // | Extended CONNECT | No | Yes (enable_connect_protocol) |
    // | Use case | General purpose | HTTP/2 specific |
    
    // | Feature | Http | Http2 |
    // |---------|------|-------|
    // | http1_only | Yes | No |
    // | http2_only | Yes | Always |
    // | ALPN negotiation | Yes | Requires h2 |
    // | Prior knowledge | Partial | Full |
    // | Max concurrent streams | Yes | Yes |
    // | Window size config | Yes | Yes |
    // | Adaptive window | Yes | Yes |
}

Synthesis

Quick reference:

use hyper::server::conn::{Http, Http2};
 
fn quick_reference() {
    // Http: Flexible, handles both protocols
    let http = Http::new()
        .http1_keep_alive(true)
        .http2_adaptive_window(true);
    // Use for: public servers, TLS with ALPN, mixed clients
    
    // Http2: Strict HTTP/2
    let http2 = Http2::new()
        .max_concurrent_streams(100)
        .initial_stream_window_size(65535)
        .enable_connect_protocol(true);
    // Use for: gRPC, prior knowledge, h2c, extended CONNECT
    
    // Http::new().http2_only(true) is similar to Http2::new()
    // But Http2 provides cleaner HTTP/2-only configuration
}

Key insight: Http and Http2 represent different philosophies for HTTP connection handling. Http embraces flexibility—it negotiates between HTTP/1 and HTTP/2 based on TLS ALPN or connection preface detection, providing a single configuration interface for both protocols. Http2 embraces specificity—it assumes HTTP/2 from the start, enabling prior knowledge setups, h2c (HTTP/2 cleartext), and HTTP/2-specific features like extended CONNECT. Choose Http for public-facing servers where client protocol diversity matters, or Http2 for controlled environments like gRPC services, internal microservices, or when you need explicit HTTP/2-only configuration. The performance difference is negligible; the choice is about API clarity and protocol support guarantees. Both use the same HTTP/2 implementation internally when serving HTTP/2, so window sizes, frame sizes, and other settings behave consistently across both.