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 Hyper’s automatic HTTP/1 or HTTP/2 selection with HTTP upgrade support when both protocols are enabled. Customize settings with .configure_hyper().

Set builder.http1_only() or builder.http2_only() in configure_hyper to serve only that protocol. Other protocols are rejected by Hyper before reaching the request service. Without either setting, both protocols are automatically detected.

When both protocols are enabled, serve uses Hyper’s upgrade-capable API. With http1_only() or http2_only(), it uses the regular connection API to preserve the protocol restriction. HTTP/1-only upgrades require a custom serve loop using Hyper’s native HTTP/1 builder.

§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

serve limits concurrently accepted connections to 16,384 by default. Configure or disable the limit on the server future:

ⓘ
serve(listener, app.into_make_service())
    .max_connections(1000)
    .await?;

ListenerExt::limit_connections remains available for listener-level composition. The connection permit stays attached to the socket across HTTP upgrades. Applications that downcast upgraded IO with hyper_util::server::conn::auto::upgrade::downcast must use TokioIo<ConnLimiterIo<L::Io>> as the IO type. IncomingStream continues to expose the listener’s original IO type to the service factory.

A one-minute header-read deadline starts when the connection task begins driving Hyper and ends when Hyper delivers the first complete request head to its service. It includes protocol negotiation and parsing for either HTTP version. Connection service creation, Tower readiness, request middleware, and body reads are excluded. Configure it with Serve::header_read_timeout. The same setting is applied to Hyper’s HTTP/1 header-read timeout for subsequent requests. Header-read timeout protection for every request is currently supported only for HTTP/1. For HTTP/2, only the first request head on the connection is covered; later streams have no header-read deadline. HTTP/2 keep-alive pings do not provide that protection.

§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("127.0.0.1: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 ReadPer-request protection currently supports HTTP/1 only; HTTP/2 covers only the connection’s first request head1 minute by default; override via .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 requests20 s ping interval by default; override via .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("127.0.0.1:3000")
    .await?
    .limit_connections(100);
serve(listener, app.into_make_service()).await?;

// ✗ Wrong - limit_connections must be called on listener
serve(TcpListener::bind("127.0.0.1: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("127.0.0.1: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.

Constants§

DEFAULT_HEADER_READ_TIMEOUT
Default timeout for receiving a complete request head.
DEFAULT_HTTP2_KEEP_ALIVE_INTERVAL
Default interval between HTTP/2 keep-alive pings.
DEFAULT_HTTP2_KEEP_ALIVE_TIMEOUT
Default timeout for receiving an HTTP/2 keep-alive ping acknowledgment.
DEFAULT_MAX_CONNECTIONS
Default maximum number of concurrently accepted TCP connections.
DEFAULT_SHUTDOWN_TIMEOUT
Graceful shutdown waits indefinitely unless a timeout is configured.

Traits§

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

Functions§

serve
Serve the service with the supplied listener.