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 workloadsHTTP/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 sizesWhat with_executor() configures:
// Task spawning:
// - Background task creation
// - Custom async runtimes
// - Task instrumentation
// - Resource limitingWhen 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/instrumentationKey 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.
