How does hyper::server::conn::Http::new configure HTTP protocol options compared to Http::with_executor?

Http::new creates an HTTP connection service with default protocol settings for HTTP/1 and HTTP/2, while Http::with_executor configures a custom executor for spawning background tasks used by HTTP/2 connection management and other async operations. These serve different purposes: new is the constructor that establishes baseline protocol behavior, and with_executor customizes where spawned tasks execute when the default executor isn't appropriate.

The Http Connection Service

use hyper::server::conn::Http;
use hyper::service::service_fn;
use hyper::{Body, Request, Response};
use tokio::net::TcpListener;
 
async fn basic_server() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // Http::new() creates the protocol handler
    let http = Http::new();
    
    // The Http service handles HTTP/1 and HTTP/2 connections
    loop {
        let (stream, _addr) = listener.accept().await.unwrap();
        
        // serve_connection uses the Http configuration
        http.serve_connection(stream, service_fn(handle)).await;
    }
}
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
    Ok(Response::new(Body::from("Hello")))
}

Http::new() creates the connection handler with default protocol settings.

Http::new Protocol Configuration

use hyper::server::conn::Http;
use hyper::body::Bytes;
 
fn http_new_defaults() {
    // Http::new creates a service with default settings:
    // - HTTP/1 enabled
    // - HTTP/2 enabled (with default configuration)
    // - No custom executor (uses default spawn)
    // - Default buffer sizes
    
    let http = Http::new();
    
    // The defaults are suitable for most use cases:
    // - Reasonable timeouts
    // - Standard HTTP/1.1 behavior
    // - HTTP/2 with default frame sizes
    // - Standard header limits
}
 
// Http is a builder that can be configured further
fn configured_http() {
    let http = Http::new()
        .http1_keepalive(true)           // Enable HTTP/1 keep-alive
        .http1_title_case_headers(false) // Lowercase headers (default)
        .http1_preserve_header_case(true) // Preserve original header case
        .http2_keep_alive_interval(None)  // Disable HTTP/2 keep-alive
        .http2_max_concurrent_streams(100); // Limit concurrent streams
    
    // These methods configure protocol behavior
    // The Http instance is then used for connections
}

Http::new() creates a configuration that can be customized with builder methods.

HTTP/1 Configuration Options

use hyper::server::conn::Http;
 
fn http1_options() {
    let http = Http::new()
        // Keep-alive settings
        .http1_keepalive(true)  // Allow persistent connections (default: true)
        
        // Header handling
        .http1_title_case_headers(true)     // Use "Title-Case" for headers
        .http1_preserve_header_case(true)    // Keep original case
        
        // Other HTTP/1 options
        .http1_allow_obsolete_multiline_headers_in_responses(true)
        .http1_allow_spaces_after_header_name_in_responses(true)
        .http1_max_buf_size(1024 * 1024);   // Max buffer size
    
    // These control how HTTP/1 connections are handled
}
 
// Example showing header case differences
async fn header_case_example() {
    use hyper::{Body, Request, Response};
    
    // Default: lowercase headers
    let http_default = Http::new();
    // Request header: "Content-Type: application/json"
    // Parsed as: "content-type: application/json"
    
    // With preserve_header_case:
    let http_preserve = Http::new()
        .http1_preserve_header_case(true);
    // Request header: "Content-Type: application/json"
    // Parsed as: "Content-Type: application/json" (preserved)
    
    // With title_case_headers:
    let http_title = Http::new()
        .http1_title_case_headers(true);
    // Response headers: "Content-Type: application/json"
    // (First letter of each word capitalized)
}

HTTP/1 options control header handling, keep-alive, and buffer sizes.

HTTP/2 Configuration Options

use hyper::server::conn::Http;
use std::time::Duration;
 
fn http2_options() {
    let http = Http::new()
        // Keep-alive for HTTP/2
        .http2_keep_alive_interval(Some(Duration::from_secs(30)))
        .http2_keep_alive_timeout(Duration::from_secs(10))
        
        // Stream limits
        .http2_max_concurrent_streams(100)  // Max concurrent streams per connection
        .http2_initial_stream_window_size(65535)  // Initial window size
        
        // Frame sizes
        .http2_initial_connection_window_size(65535)
        .http2_max_frame_size(16384)
        
        // Other HTTP/2 options
        .http2_adaptive_window(true)  // Adaptive flow control
        .http2_max_header_list_size(16 * 1024);  // Max header size
    
    // These control HTTP/2 protocol behavior
}
 
// HTTP/2 keep-alive sends PING frames
fn http2_keepalive() {
    let http = Http::new()
        // Enable HTTP/2 keep-alive with PING frames
        .http2_keep_alive_interval(Some(Duration::from_secs(30)));
    
    // Without keep-alive: idle connections may be dropped by load balancers
    // With keep-alive: periodic PING frames keep connection active
    
    // Useful behind load balancers that terminate idle connections
}

HTTP/2 options control stream limits, frame sizes, and keep-alive behavior.

The Executor Role

use hyper::server::conn::Http;
use std::future::Future;
use std::pin::Pin;
 
// The executor is used for spawning async tasks
// This is important for HTTP/2 which spawns background tasks
 
// By default, Http uses tokio's spawn
// But custom executors can be configured
 
trait Executor {
    fn execute(&self, fut: Pin<Box<dyn Future<Output = ()> + Send>>);
}
 
// Why customize the executor?
// 1. Testing with mock executors
// 2. Custom async runtimes (not tokio)
// 3. Instrumented spawning (metrics, tracing)
// 4. Resource limiting (limit concurrent tasks)

The executor spawns background tasks for HTTP/2 and other operations.

Http::with_executor Configuration

use hyper::server::conn::Http;
use std::sync::Arc;
 
// Default executor (tokio)
fn with_default_executor() {
    let http = Http::new();
    // Uses tokio::spawn internally for background tasks
    // Works automatically when using #[tokio::main]
}
 
// Custom executor
fn with_custom_executor() {
    // Custom executor that tracks spawned tasks
    #[derive(Clone)]
    struct TrackingExecutor {
        spawned_count: Arc<std::sync::atomic::AtomicU64>,
    }
    
    impl<Fut> hyper::rt::Executor<Fut> for TrackingExecutor
    where
        Fut: std::future::Future<Output = ()> + Send + 'static,
    {
        fn execute(&self, fut: Fut) {
            self.spawned_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
            tokio::spawn(fut);
        }
    }
    
    let executor = TrackingExecutor {
        spawned_count: Arc::new(std::sync::atomic::AtomicU64::new(0)),
    };
    
    // Create Http with custom executor
    let http = Http::new().with_executor(executor);
    
    // Now all background tasks go through TrackingExecutor
}

with_executor allows custom task spawning for HTTP/2 background work.

Why HTTP/2 Needs an Executor

use hyper::server::conn::Http;
 
// HTTP/2 spawns background tasks for:
// 1. Connection-level flow control
// 2. Stream multiplexing
// 3. Keep-alive PING frames
// 4. Push promises (if enabled)
 
fn http2_background_tasks() {
    // When HTTP/2 is used, Hyper spawns tasks for:
    // - Sending PING frames (if keep-alive enabled)
    // - Managing stream windows
    // - Processing concurrent streams
    
    let http = Http::new()
        .http2_keep_alive_interval(Some(std::time::Duration::from_secs(30)));
    
    // This spawns a background task per connection
    // The executor handles these spawns
    
    // With default executor: tokio::spawn
    // With custom executor: your implementation
}
 
// HTTP/1 doesn't typically need background task spawning
// (except for some advanced features)
fn http1_no_executor_needed() {
    let http = Http::new()
        .http1_keepalive(true);
    
    // HTTP/1 connections are simpler
    // No background tasks spawned for basic HTTP/1
}

HTTP/2 requires background tasks; HTTP/1 is simpler.

Separation of Concerns

use hyper::server::conn::Http;
 
// Http::new: Protocol configuration
// - HTTP/1 vs HTTP/2 settings
// - Buffer sizes, frame sizes
// - Keep-alive behavior
// - Header handling
 
// with_executor: Task spawning
// - Where background tasks run
// - How tasks are spawned
// - Custom async runtimes
// - Instrumentation/tracing
 
fn configure_both() {
    // You can configure protocol AND executor
    let http = Http::new()
        // Protocol options from Http::new
        .http1_keepalive(true)
        .http2_keep_alive_interval(Some(std::time::Duration::from_secs(30)))
        .http2_max_concurrent_streams(100)
        // Executor configuration
        .with_executor(tokio::runtime::Handle::current());
    
    // Protocol options and executor are independent
}

Protocol options and executor configuration are separate concerns.

Using with_executor in Practice

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use std::sync::Arc;
use tokio::runtime::Handle;
 
// Example 1: Use specific tokio runtime
fn with_runtime_handle() {
    let http = Http::new()
        .with_executor(Handle::current());
    
    // Use a specific runtime handle
    // Useful when multiple runtimes exist
}
 
// Example 2: Instrumented executor
#[derive(Clone)]
struct InstrumentedExecutor {
    handle: Handle,
    metrics: Arc<Metrics>,
}
 
impl<Fut> hyper::rt::Executor<Fut> for InstrumentedExecutor
where
    Fut: std::future::Future<Output = ()> + Send + 'static,
{
    fn execute(&self, fut: Fut) {
        self.metrics.spawn_count.increment();
        self.handle.spawn(async move {
            fut.await;
            self.metrics.complete_count.increment();
        });
    }
}
 
struct Metrics {
    spawn_count: std::sync::atomic::AtomicU64,
    complete_count: std::sync::atomic::AtomicU64,
}
 
// Example 3: Bounded executor (limit concurrent tasks)
#[derive(Clone)]
struct BoundedExecutor {
    handle: Handle,
    semaphore: Arc<tokio::sync::Semaphore>,
}
 
impl<Fut> hyper::rt::Executor<Fut> for BoundedExecutor
where
    Fut: std::future::Future<Output = ()> + Send + 'static,
{
    fn execute(&self, fut: Fut) {
        let permit = self.semaphore.clone();
        self.handle.spawn(async move {
            let _permit = permit.acquire().await.unwrap();
            fut.await
        });
    }
}

Custom executors enable instrumentation, rate limiting, and runtime selection.

The serve_connection Method

use hyper::server::conn::Http;
use hyper::service::service_fn;
use hyper::{Body, Request, Response};
use tokio::net::TcpListener;
 
async fn serve_with_configuration() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // Configured Http instance
    let http = Http::new()
        .http1_keepalive(true)
        .http2_keep_alive_interval(Some(std::time::Duration::from_secs(30)))
        .http2_max_concurrent_streams(100);
    
    loop {
        let (stream, _addr) = listener.accept().await.unwrap();
        
        // serve_connection applies the configuration
        let serve = http.serve_connection(stream, service_fn(|_req| async {
            Ok::<_, hyper::Error>(Response::new(Body::from("Hello")))
        }));
        
        // This spawns tasks using the configured executor
        // (or default tokio::spawn)
        if let Err(e) = serve.await {
            eprintln!("Connection error: {}", e);
        }
    }
}

serve_connection applies both protocol settings and executor configuration.

HTTP/1 vs HTTP/2 Executor Usage

use hyper::server::conn::Http;
 
// HTTP/1: Minimal executor usage
fn http1_executor() {
    let http = Http::new();
    // serve_connection for HTTP/1 is mostly synchronous
    // Background tasks rarely spawned
    
    // Custom executor has minimal impact on HTTP/1
}
 
// HTTP/2: Heavy executor usage
fn http2_executor() {
    let http = Http::new()
        .http2_keep_alive_interval(Some(std::time::Duration::from_secs(30)));
    
    // HTTP/2 spawns background tasks for:
    // - Keep-alive PING frames
    // - Stream management
    // - Flow control updates
    
    // Custom executor matters for HTTP/2
    // Affects how these tasks are scheduled
}
 
// If you're only using HTTP/1:
// - Custom executor is rarely needed
// - Default tokio::spawn is usually sufficient
 
// If you're using HTTP/2:
// - Custom executor can be useful
// - Important for connection-heavy workloads

HTTP/2 benefits more from custom executor configuration.

Comparison Summary

use hyper::server::conn::Http;
 
fn comparison() {
    // Http::new()
    // Purpose: Create HTTP protocol handler with defaults
    // Returns: Http<DefaultExecutor>
    // Configures: HTTP/1 and HTTP/2 protocol settings
    // Usage: Starting point for all Http configurations
    
    let http = Http::new();
    // Default protocol settings
    // Default executor (tokio::spawn)
    
    // Http::with_executor()
    // Purpose: Customize task spawning
    // Returns: Http<CustomExecutor>
    // Configures: Where/how background tasks run
    // Usage: When default spawn isn't appropriate
    
    let http = Http::new().with_executor(my_executor);
    // Custom executor for background tasks
    // Protocol settings still apply
}

Http::new() creates the handler; with_executor customizes task spawning.

Practical Patterns

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use tokio::runtime::Handle;
 
// Pattern 1: Default configuration (most common)
fn default_config() {
    let http = Http::new();
    // Suitable for most applications
    // Uses tokio::spawn for background tasks
}
 
// Pattern 2: HTTP/2 optimized
fn http2_optimized() {
    let http = Http::new()
        .http2_keep_alive_interval(Some(std::time::Duration::from_secs(30)))
        .http2_max_concurrent_streams(200)
        .http2_adaptive_window(true);
    // Optimized for HTTP/2 workloads
}
 
// Pattern 3: HTTP/1 optimized
fn http1_optimized() {
    let http = Http::new()
        .http1_keepalive(true)
        .http1_preserve_header_case(true)
        .http1_max_buf_size(1024 * 1024);
    // Optimized for HTTP/1 workloads
}
 
// Pattern 4: Custom runtime
fn custom_runtime(handle: Handle) {
    let http = Http::new()
        .with_executor(handle);
    // Uses specific runtime for background tasks
}
 
// Pattern 5: Instrumented production setup
fn production_setup(handle: Handle, metrics: Arc<Metrics>) {
    let executor = InstrumentedExecutor { handle, metrics };
    
    let http = Http::new()
        .http2_keep_alive_interval(Some(std::time::Duration::from_secs(30)))
        .http2_max_concurrent_streams(100)
        .with_executor(executor);
    // Production-ready with metrics
}

Choose configuration based on your workload and requirements.

Synthesis

Key differences:

Aspect Http::new() with_executor()
Purpose Protocol configuration Task spawning
Configures HTTP/1 and HTTP/2 settings Where background tasks run
Default Standard protocol settings tokio::spawn
Returns Http<DefaultExecutor> Http<CustomExecutor>
Common use All servers HTTP/2, custom runtimes

What Http::new() configures:

// Protocol-level settings:
// - HTTP/1 keepalive
// - HTTP/1 header handling
// - HTTP/1 buffer sizes
// - HTTP/2 keep-alive PING
// - HTTP/2 stream limits
// - HTTP/2 frame sizes
// - HTTP/2 window sizes

What with_executor() configures:

// Task spawning:
// - Background task creation
// - Custom async runtimes
// - Task instrumentation
// - Resource limiting

When to use each:

// Use Http::new() alone when:
// - Default tokio runtime is fine
// - Standard HTTP/1 or HTTP/2 settings work
// - No custom task spawning needed
 
// Use with_executor() when:
// - Multiple tokio runtimes
// - Need to track spawned tasks
// - Custom async runtime (not tokio)
// - Resource limiting for background tasks
// - Production metrics/instrumentation

Key insight: Http::new() and with_executor() configure entirely different aspects of the HTTP connection handler. Http::new() creates a protocol handler with default settings for HTTP/1 and HTTP/2, while with_executor() customizes where Hyper spawns background tasks—important for HTTP/2's internal task spawning but rarely needed for HTTP/1. They're typically used together: Http::new() establishes protocol configuration, and subsequent builder methods like http1_keepalive() or http2_max_concurrent_streams() refine it, while with_executor() optionally replaces the default task spawner. The two configurations are independent: protocol settings don't affect task spawning, and executor choice doesn't change protocol behavior.