Skip to main content

Module serve

Module serve 

Source
Expand description

Serve utilities for running HTTP servers.

This module provides a convenient serve function similar to axum::serve for easily serving Tower services with Hyper.

§When to Use This Module

  • Use serve when you need a simple, batteries-included HTTP server
  • For more control over the Hyper connection builder, use .configure_hyper()
  • For Lambda environments, see the aws-lambda feature and routing::lambda_handler

§How It Works

The serve function creates a connection acceptance loop that:

  1. Accepts connections via the Listener trait (e.g., TcpListener)
  2. Creates per-connection services by calling the make_service with IncomingStream
  3. Converts Tower services to Hyper using TowerToHyperService
  4. Spawns a task for each connection to handle HTTP requests
┌─────────┐      ┌──────────────┐      ┌──────────────┐      ┌────────┐
│Listener │─────▶│IncomingStream│─────▶│ make_service │─────▶│ Hyper  │
│ accept  │      │ (io + addr)  │      │  (Tower)     │      │ spawn  │
└─────────┘      └──────────────┘      └──────────────┘      └────────┘

The IncomingStream provides connection metadata to the service factory, allowing per-connection customization based on remote address or IO type

§HTTP Protocol Selection

By default, serve uses HTTP/1 with upgrade support, allowing clients to negotiate HTTP/2 via the HTTP/1.1 Upgrade mechanism or ALPN. The protocol is auto-detected for each connection.

You can customize this behavior with .configure_hyper():

// Force HTTP/2 only (skips upgrade negotiation)
serve(listener, app.into_make_service())
    .configure_hyper(|builder| {
        builder.http2_only()
    })
    .await?;

// Force HTTP/1 only with keep-alive
serve(listener, app.into_make_service())
    .configure_hyper(|builder| {
        builder.http1().keep_alive(true)
    })
    .await?;

Performance note: When using .http2_only() or .http1(), the server skips the HTTP/1 upgrade preface reading, which can reduce connection setup latency.

§Graceful Shutdown

Graceful shutdown is zero-cost when not used - no watch channels are allocated and no tokio::select! overhead is incurred. Call .with_graceful_shutdown(signal) to enable it:

serve(listener, service)
    .with_graceful_shutdown(async {
        tokio::signal::ctrl_c().await.expect("failed to listen for Ctrl+C");
    })
    .await

This ensures in-flight requests complete before shutdown. Use .with_shutdown_timeout(duration) to set a maximum wait time.

§Common Patterns

§Limiting Concurrent Connections

Use ListenerExt::limit_connections to prevent resource exhaustion:

use aws_smithy_http_server::serve::ListenerExt;

let listener = TcpListener::bind("0.0.0.0:3000")
    .await?
    .limit_connections(1000);  // Max 1000 concurrent connections

serve(listener, app.into_make_service()).await?;

§Accessing Connection Information

Use .into_make_service_with_connect_info::<T>() to access connection metadata in your handlers:

use std::net::SocketAddr;
use aws_smithy_http_server::request::connect_info::ConnectInfo;

// In your handler:
async fn my_handler(ConnectInfo(addr): ConnectInfo<SocketAddr>) -> String {
    format!("Request from: {}", addr)
}

// When serving:
serve(
    listener,
    app.into_make_service_with_connect_info::<SocketAddr>()
).await?;

§Custom TCP Settings

Use ListenerExt::tap_io to configure TCP options:

use aws_smithy_http_server::serve::ListenerExt;

let listener = TcpListener::bind("0.0.0.0:3000")
    .await?
    .tap_io(|stream| {
        let _ = stream.set_nodelay(true);
    });

serve(listener, app.into_make_service()).await?;

§Timeouts and Connection Management

§Available Timeout Types

Timeout TypeWhat It DoesHow to Configure
Header ReadTime limit for reading HTTP headers.configure_hyper() with .http1().header_read_timeout()
RequestTime limit for processing one requestTower’s TimeoutLayer
Connection DurationTotal connection lifetime limitCustom accept loop with tokio::time::timeout
HTTP/2 Keep-AliveIdle timeout between HTTP/2 requests.configure_hyper() with .http2().keep_alive_*()

Examples:

  • examples/header_read_timeout.rs - Configure header read timeout
  • examples/request_timeout.rs - Add request-level timeouts
  • examples/custom_accept_loop.rs - Implement connection duration limits
  • examples/http2_keepalive.rs - Configure HTTP/2 keep-alive
  • examples/connection_limiting.rs - Limit concurrent connections
  • examples/request_concurrency_limiting.rs - Limit concurrent requests

§Connection Duration vs Idle Timeout

Connection duration timeout: Closes the connection after N seconds total, regardless of activity. Implemented with tokio::time::timeout wrapping the connection future.

Idle timeout: Closes the connection only when inactive between requests.

  • HTTP/2: Available via .keep_alive_interval() and .keep_alive_timeout()
  • HTTP/1.1: Not available without modifying Hyper

See examples/custom_accept_loop.rs for a working connection duration timeout example.

§Connection Limiting vs Request Limiting

Connection limiting (.limit_connections()): Limits the number of TCP connections. Use this to prevent socket/file descriptor exhaustion.

Request limiting (ConcurrencyLimitLayer): Limits in-flight requests. Use this to prevent work queue exhaustion. With HTTP/2, one connection can have multiple requests in flight simultaneously.

Most applications should use both - they protect different layers.

§Troubleshooting

§Type Errors

If you encounter complex error messages about trait bounds, check:

  1. Service Error Type: Your service must have Error = Infallible

    // ✓ Correct - handlers return responses, not Results
    async fn handler() -> Response<Body> { ... }
    
    // ✗ Wrong - cannot use Result<Response, E>
    async fn handler() -> Result<Response<Body>, MyError> { ... }
  2. MakeService Wrapper: Use the correct wrapper for your service:

    use aws_smithy_http_server::routing::IntoMakeService;
    
    // For Smithy services:
    app.into_make_service()
    
    // For services with middleware:
    IntoMakeService::new(service)

§Graceful Shutdown Not Working

If graceful shutdown doesn’t wait for connections:

  • Ensure you call .with_graceful_shutdown() before .await
  • The signal future must be Send + 'static
  • Consider adding a timeout with .with_shutdown_timeout()

§Connection Limit Not Applied

Remember that .limit_connections() applies to the listener before passing it to serve():

// ✓ Correct
let listener = TcpListener::bind("0.0.0.0:3000")
    .await?
    .limit_connections(100);
serve(listener, app.into_make_service()).await?;

// ✗ Wrong - limit_connections must be called on listener
serve(TcpListener::bind("0.0.0.0:3000").await?, app.into_make_service())
    .limit_connections(100)  // This method doesn't exist on Serve
    .await?;

§Advanced: Custom Connection Handling

If you need per-connection customization (e.g., different Hyper settings based on the remote address), you can implement your own connection loop using the building blocks provided by this module:

use aws_smithy_http_server::routing::IntoMakeService;
use aws_smithy_http_server::serve::Listener;
use hyper_util::rt::{TokioExecutor, TokioIo};
use hyper_util::server::conn::auto::Builder;
use hyper_util::service::TowerToHyperService;
use tower::ServiceExt;

let mut listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
let make_service = app.into_make_service_with_connect_info::<SocketAddr>();

loop {
    let (stream, remote_addr) = listener.accept().await?;
    let io = TokioIo::new(stream);

    // Per-connection Hyper configuration
    let mut builder = Builder::new(TokioExecutor::new());
    if remote_addr.ip().is_loopback() {
        builder = builder.http2_only();  // Local connections use HTTP/2
    } else {
        builder = builder.http1().keep_alive(true);  // External use HTTP/1
    }

    let tower_service = make_service
        .ready()
        .await?
        .call(IncomingStream { io: &io, remote_addr })
        .await?;

    let hyper_service = TowerToHyperService::new(tower_service);

    tokio::spawn(async move {
        if let Err(err) = builder.serve_connection(io, hyper_service).await {
            eprintln!("Error serving connection: {}", err);
        }
    });
}

This approach provides complete flexibility while still leveraging the efficient Hyper and Tower integration provided by this module.

Portions of the implementation are adapted from axum (https://github.com/tokio-rs/axum), which is licensed under the MIT License. Copyright (c) 2019 Axum Contributors

Structs§

ConnLimiter
Return type of ListenerExt::limit_connections.
ConnLimiterIo
A connection counted by ConnLimiter.
IncomingStream
An incoming stream that bundles connection information.
Serve
A server future that serves HTTP connections.
ServeWithGracefulShutdown
A server future with graceful shutdown enabled.
TapIo
Return type of ListenerExt::tap_io.

Traits§

Listener
Types that can listen for connections.
ListenerExt
Extensions to Listener.

Functions§

serve
Serve the service with the supplied listener.