What are the differences between hyper::server::conn::Http and hyper::server::Server for HTTP server configuration?

hyper::server::conn::Http is a low-level connection handler that gives you manual control over each individual connection's lifecycle and configuration, while hyper::server::Server is a high-level convenience wrapper that manages connection acceptance and spawning automatically. The key distinction is that Http requires you to explicitly accept incoming streams and call serve_connection for each one, making it suitable for custom connection handling, protocol upgrades, or integration with other runtimes. Server abstracts this away by providing bind and serve methods that handle the entire accept-serve loop, making it the preferred choice for standard HTTP servers where you just want to handle requests without managing connection details.

Basic Server Usage

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Hello, World!")))
}
 
#[tokio::main]
async fn main() {
    // Server::bind creates a high-level server
    let addr = ([127, 0, 0, 1], 3000).into();
    
    let make_service = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });
    
    // Server handles everything:
    // 1. Binds to address
    // 2. Accepts connections
    // 3. Spawns tasks for each connection
    // 4. Serves HTTP on each connection
    let server = Server::bind(&addr).serve(make_service);
    
    // Run until completion (or error)
    if let Err(e) = server.await {
        eprintln!("Server error: {}", e);
    }
}

Server::bind().serve() handles the entire server lifecycle with minimal configuration.

Basic Http Connection Handler

use hyper::server::conn::Http;
use hyper::{Body, Request, Response, Version};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Hello from Http!")))
}
 
#[tokio::main]
async fn main() {
    // Http gives low-level control
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // Create Http connection handler
    let http = Http::new();
    
    loop {
        // Manually accept each connection
        let (stream, remote_addr) = listener.accept().await.unwrap();
        
        // Manually call serve_connection for each stream
        let service = service_fn(handle);
        
        tokio::spawn(async move {
            // This handles HTTP for this specific connection
            if let Err(e) = http.serve_connection(stream, service).await {
                eprintln!("Connection error from {}: {}", remote_addr, e);
            }
        });
    }
}

Http::new().serve_connection() requires manual connection acceptance but offers more control.

Server Abstracts the Accept Loop

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Response")))
}
 
#[tokio::main]
async fn main() {
    let addr = ([127, 0, 0, 1], 3000).into();
    
    let make_service = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });
    
    // Server::bind does this internally:
    // 1. Creates TcpListener
    // 2. Loops accepting connections
    // 3. For each connection:
    //    - Creates Http connection handler
    //    - Calls serve_connection
    //    - Spawns task for handling
    
    // You just provide the service factory
    let server = Server::bind(&addr).serve(make_service);
    
    server.await.unwrap();
}

Server internally uses Http but hides the accept loop and task spawning.

Http Configuration Options

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use std::time::Duration;
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Configured response")))
}
 
#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // Http can be configured before serving
    let http = Http::new()
        // HTTP/1 configuration
        .http1_keepalive(true)
        .http1_header_read_timeout(Duration::from_secs(10))
        .http1_writev(true)  // Use writev for headers
        
        // HTTP/2 configuration
        .http2_only(false)  // Allow HTTP/1
        .http2_max_concurrent_streams(100);
    
    // Server has similar builder methods, but Http gives
    // per-connection control
    
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        let service = service_fn(handle);
        
        // Each connection can use different Http configuration
        let http = http.clone();
        
        tokio::spawn(async move {
            http.serve_connection(stream, service).await.unwrap();
        });
    }
}

Http provides fine-grained HTTP protocol configuration per connection.

Server Builder Methods

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use std::time::Duration;
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Response")))
}
 
#[tokio::main]
async fn main() {
    let addr = ([127, 0, 0, 1], 3000).into();
    
    let make_service = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });
    
    // Server also has builder methods
    let server = Server::bind(&addr)
        .http1_keepalive(true)
        .http1_header_read_timeout(Duration::from_secs(10))
        .http2_only(false)
        .http2_max_concurrent_streams(100)
        .serve(make_service);
    
    // These configure the underlying Http for all connections
    server.await.unwrap();
}

Server exposes similar configuration methods that apply to all connections.

Handling Protocol Upgrades

use hyper::server::conn::Http;
use hyper::{Body, Request, Response, StatusCode};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use std::convert::Infallible;
use tokio::io::{AsyncRead, AsyncWrite};
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("HTTP response")))
}
 
#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let http = Http::new();
    
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        let service = service_fn(handle);
        
        tokio::spawn(async move {
            // serve_connection returns Ok(Upgraded) for protocol upgrades
            // This allows handling WebSocket upgrades, HTTP/2 prior knowledge, etc.
            let result = http.serve_connection(stream, service).await;
            
            match result {
                Ok(_) => println!("Connection completed normally"),
                Err(e) => eprintln!("Connection error: {}", e),
            }
            
            // With Server, you don't have this level of control
            // over the connection result
        });
    }
}

Http returns connection results including upgrades; Server handles them internally.

WebSocket Upgrade Example

use hyper::server::conn::Http;
use hyper::{Body, Request, Response, HeaderMap, StatusCode};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use std::convert::Infallible;
 
async fn handle_request(req: Request<Body>) -> Result<Response<Body>, Infallible> {
    // Check for WebSocket upgrade
    if let Some(ws_key) = req.headers().get("Upgrade") {
        if ws_key == "websocket" {
            // Return upgrade response
            let mut response = Response::new(Body::empty());
            *response.status_mut() = StatusCode::SWITCHING_PROTOCOLS;
            response.headers_mut().insert("Upgrade", "websocket".parse().unwrap());
            return Ok(response);
        }
    }
    
    Ok(Response::new(Body::from("Regular HTTP response")))
}
 
#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let http = Http::new();
    
    loop {
        let (stream, _addr) = listener.accept().await.unwrap();
        let service = service_fn(handle_request);
        
        tokio::spawn(async move {
            // serve_connection allows inspecting the upgrade result
            let conn = http.serve_connection(stream, service);
            
            // Use with_upgrades() to handle protocol upgrades
            // conn.with_upgrades().await is needed for WebSocket support
            if let Err(e) = conn.await {
                eprintln!("Connection error: {}", e);
            }
        });
    }
}

Http::serve_connection with with_upgrades() enables WebSocket and other protocol upgrades.

Graceful Shutdown

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use std::convert::Infallible;
use tokio::signal;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Hello")))
}
 
#[tokio::main]
async fn main() {
    let addr = ([127, 0, 0, 1], 3000).into();
    
    let make_service = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });
    
    // Server provides graceful_shutdown helper
    let server = Server::bind(&addr).serve(make_service);
    
    // with_graceful_shutdown accepts a future that signals shutdown
    let graceful = server.with_graceful_shutdown(signal::ctrl_c());
    
    if let Err(e) = graceful.await {
        eprintln!("Server error: {}", e);
    }
}

Server::with_graceful_shutdown handles graceful shutdown; Http requires manual implementation.

Manual Graceful Shutdown with Http

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use tokio::sync::broadcast;
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Response")))
}
 
#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let http = Http::new();
    
    // Shutdown signal channel
    let (shutdown_tx, mut shutdown_rx) = broadcast::channel::<()>(1);
    
    // Accept loop
    loop {
        tokio::select! {
            // Accept new connections
            accept_result = listener.accept() => {
                let (stream, _) = accept_result.unwrap();
                let service = service_fn(handle);
                let http = http.clone();
                let mut shutdown_rx = shutdown_tx.subscribe();
                
                tokio::spawn(async move {
                    // Use into_owned() to allow graceful shutdown
                    let conn = http.serve_connection(stream, service);
                    tokio::select! {
                        _ = conn => {}
                        _ = shutdown_rx.recv() => {
                            // Graceful shutdown signal received
                        }
                    }
                });
            }
            
            // Shutdown signal
            _ = tokio::signal::ctrl_c() => {
                println!("Shutdown signal received");
                shutdown_tx.send(()).unwrap();
                break;
            }
        }
    }
    
    // Wait for connections to finish
    println!("Waiting for connections to close...");
}

With Http, graceful shutdown requires manual coordination.

Connection Information Access

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use std::net::SocketAddr;
use std::convert::Infallible;
 
async fn handle_with_addr(
    _req: Request<Body>,
    remote_addr: SocketAddr
) -> Result<Response<Body>, Infallible> {
    println!("Request from: {}", remote_addr);
    Ok(Response::new(Body::from(format!("Hello from {}", remote_addr))))
}
 
#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let http = Http::new();
    
    loop {
        // Http pattern: you have access to connection info
        let (stream, remote_addr) = listener.accept().await.unwrap();
        
        // Can pass connection info to service
        let service = service_fn(move |req| {
            handle_with_addr(req, remote_addr)
        });
        
        tokio::spawn(async move {
            http.serve_connection(stream, service).await.unwrap();
        });
    }
}

Http gives you access to the remote address and connection details.

Server with Addr Access

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};
use std::convert::Infallible;
use std::net::SocketAddr;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Hello")))
}
 
#[tokio::main]
async fn main() {
    let addr = ([127, 0, 0, 1], 3000).into();
    
    // make_service_fn provides connection info
    let make_service = make_service_fn(|conn: &Server::conn::AddrStream| {
        // Access remote address
        let remote_addr = conn.remote_addr();
        println!("New connection from: {}", remote_addr);
        
        async move {
            Ok::<_, Infallible>(service_fn(handle))
        }
    });
    
    let server = Server::bind(&addr).serve(make_service);
    server.await.unwrap();
}

Server provides AddrStream in make_service_fn for connection info access.

Http for Custom Runtimes

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use hyper::service::service_fn;
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("Hello")))
}
 
// Http can work with any stream type that implements AsyncRead + AsyncWrite
// This makes it usable with:
// - Tokio TcpStream
// - Async-std TcpStream
// - Custom transports (Unix sockets, TLS streams, etc.)
// - Mock streams for testing
 
#[tokio::main]
async fn main() {
    // For example, using with a custom transport:
    // let custom_stream = MyCustomTransport::new();
    // let service = service_fn(handle);
    // http.serve_connection(custom_stream, service).await;
    
    // Server requires tokio runtime and TcpListener
    // Http is transport-agnostic
    
    let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap();
    let http = Http::new();
    
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        let service = service_fn(handle);
        
        tokio::spawn(http.serve_connection(stream, service));
    }
}

Http is transport-agnostic; Server is tokio-tied.

TLS Integration Example

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use tokio_native_tls::native_tls::TlsAcceptor;
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new(Body::from("HTTPS response")))
}
 
#[tokio::main]
async fn main() {
    // Http works well with TLS because you control the stream
    let listener = TcpListener::bind("127.0.0.1:443").await.unwrap();
    let http = Http::new();
    
    // TLS acceptor (simplified - would need proper certificate)
    // let tls_acceptor = TlsAcceptor::from(...);
    
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        
        // Wrap stream with TLS
        // let tls_stream = tls_acceptor.accept(stream).await.unwrap();
        
        // Then serve HTTP over TLS stream
        // http.serve_connection(tls_stream, service_fn(handle)).await;
        
        // Server can also work with TLS, but Http gives explicit control
        // over the TLS handshake timing and error handling
    }
}

Http provides explicit control for TLS and other transport wrapping.

Connection Timeouts

use hyper::server::conn::Http;
use hyper::{Body, Request, Response};
use hyper::service::service_fn;
use tokio::net::TcpListener;
use std::time::Duration;
use std::convert::Infallible;
 
async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
    // Simulate slow processing
    tokio::time::sleep(Duration::from_secs(2)).await;
    Ok(Response::new(Body::from("Done")))
}
 
#[tokio::main]
async fn main() {
    let listener = TcpListener::bind("127.0.0.1:3000").await.unwrap();
    
    // Configure per-connection timeouts
    let http = Http::new()
        .http1_header_read_timeout(Duration::from_secs(5));
    
    loop {
        let (stream, _) = listener.accept().await.unwrap();
        let service = service_fn(handle);
        let http = http.clone();
        
        tokio::spawn(async move {
            // Connection will timeout if headers aren't read within 5 seconds
            if let Err(e) = http.serve_connection(stream, service).await {
                eprintln!("Connection error: {}", e);
            }
        });
    }
}

Http allows per-connection timeout configuration; Server applies configuration to all connections.

When to Use Each

// Use Server::bind when:
// - Standard HTTP server use case
// - Want minimal boilerplate
// - Don't need per-connection customization
// - Don't need protocol upgrade handling
// - Don't need custom transport types
 
// Use Http when:
// - Need to handle protocol upgrades (WebSocket, HTTP/2 prior knowledge)
// - Want to customize each connection differently
// - Using custom transports (TLS, Unix sockets)
// - Need to inspect connection results
// - Integrating with non-standard runtimes
// - Want fine-grained connection management
// - Implementing custom graceful shutdown

Choose Server for simplicity; Http for control.

Comparison Summary

// Server (high-level):
// ✓ Automatic accept loop
// ✓ Built-in graceful shutdown helper
// ✓ Simple API: bind().serve()
// ✓ Builder methods for configuration
// ✗ Less control per connection
// ✗ Tied to TcpListener
 
// Http (low-level):
// ✓ Per-connection configuration
// ✓ Protocol upgrade handling
// ✓ Custom transport support
// ✓ Connection result inspection
// ✓ Transport agnostic
// ✗ Manual accept loop
// ✗ More boilerplate
// ✗ Manual graceful shutdown

Server for simplicity; Http for control.

Synthesis

Architectural relationship:

  • Server::bind().serve() internally creates TcpListener and uses Http::serve_connection
  • Server is a convenience wrapper around Http
  • Both use the same underlying HTTP protocol implementation

Configuration capabilities:

  • Both support http1_keepalive, http2_only, and other HTTP options
  • Http allows different configuration per connection
  • Server applies configuration uniformly to all connections

Key differences:

  • Server: automatic accept loop, simple API, built-in shutdown
  • Http: manual accept, per-connection control, protocol upgrades

Use Server for:

  • Standard HTTP servers
  • Simple request handling
  • Don't need connection-level customization

Use Http for:

  • WebSocket upgrades
  • Per-connection configuration
  • Custom transports (TLS, Unix sockets)
  • Integrating with non-tokio runtimes
  • Need connection result inspection

Key insight: The Server type is a high-level convenience that handles the accept-serve-spawn loop automatically, while Http is the low-level primitive that actually handles HTTP on a single connection. When you call Server::bind().serve(), it creates a TcpListener, accepts connections in a loop, and for each one creates an Http instance and calls serve_connection. If you need to customize this process—like wrapping streams with TLS before serving, handling WebSocket upgrades explicitly, or managing graceful shutdown yourself—you drop down to Http and implement the accept loop manually. The separation means Http can work with any stream type that implements AsyncRead + AsyncWrite, making it suitable for testing with mock streams or using with non-TCP transports, while Server is specifically designed for TCP listeners in production HTTP servers.