The Async Ecosystem in Rust

The Rust web development ecosystem includes several powerful crates that work together to facilitate building robust web applications. This guide provides an overview of the tower, http, http_body, and axum crates and explains how they work together.

http Crate

The http crate provides common types for HTTP requests and responses, such as Request, Response, Method, and StatusCode. It defines types that are independent of any specific HTTP implementation, allowing different libraries and frameworks to interoperate by using these shared abstractions.

http_body Crate

The http_body crate defines the Body trait, an abstraction over HTTP bodies, ensuring that different libraries can work with various body implementations interchangeably. The abstraction handles the streaming of bodies, which is a detail that is often overlooked.

axum, hyper, tower_http and many others use the http_body crate to handle bodies.

tower Crate

The tower crate is a library of modular components for building robust networking clients and servers. It provides the Service trait:

// This single trait enables the entire middleware ecosystem
pub trait Service<Request> {
    type Response;
    type Error;
    type Future: Future;

    fn call(&mut self, req: Request) -> Self::Future;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
}

The Service trait is the foundation of the tower ecosystem, it represents an asynchronous function from a request to a response. The tokio blog has a great explanation of why and how the Service trait was created.

tower-layer Crate

The tower-layer crate provides the Layer trait, which is how middleware is composed in the Tower ecosystem:

pub trait Layer<S> {
    type Service;
    fn layer(&self, inner: S) -> Self::Service;
}

This simple trait enables powerful middleware composition. For example, you can add timeout and tracing middleware to any service:

use tower::{ServiceBuilder, timeout::TimeoutLayer, trace::TraceLayer};

let service = ServiceBuilder::new()
    .layer(TimeoutLayer::new(Duration::from_secs(30)))
    .layer(TraceLayer::new_for_http())
    .service(my_service);

Each layer wraps the previous service, forming a middleware stack. This is similar to how express middlewares from the node ecosystem work.

tower_http Crate

The tower_http crate is a collection of middleware components for tower. Unlike tower, tower_http is not a framework, but a set of utilities that can be reused, such as load balancing, timeouts, and retries.

The name could be more accurately named tower_http_utils, but alas.

hyper Crate

The hyper crate is a fast HTTP library. It's the foundation for the axum framework that we'll cover next. There's not much to cover here, as it is a low level library that you probably won't use directly.

But it is good to know that it uses http and http_body under the hood.

axum Framework

The axum framework is a web application framework built on top of tower and hyper. It leverages the Service trait from tower to compose routes and handlers in a modular fashion.

As such, you can use tower middlewares, such as tower_http utilities that we just discussed. However, axum also provides its own set of middlewares, that are more ergonomic to use, but are specific to axum and eat a slight performance hit.

Interoperability

Let's review the interoperability between these crates:

  • Shared Types: By utilizing the http crate's common types, these libraries ensure compatibility at the fundamental level of HTTP messages.
  • Body Abstraction: The http_body crate's Body trait allows different body implementations to be used interchangeably across tower services and axum handlers.
  • Service Composition: axum builds upon tower's Service trait, enabling the use of tower middleware within axum applications for features like logging, authentication, and rate limiting.
  • Middleware and Utilities: Libraries in this ecosystem can share middleware components, thanks to the standardized interfaces provided by tower.

Case Study: Building an API Client

I wanted to build an API client, but I wanted to be able to plug in different HTTP implementations (like hyper or reqwest) or even explore different protocols (like grpc) in the future.

I read Designing Rust bindings for REST APIs and while I liked the idea of a Query trait, I didn't like the fact that you essentially were re-inventing http and tower::Service.

Instead, I found out about the tower ecosystem and realized I could build a Query trait that is generic over the client, and even the protocol. So I embraced the ecosystem, leaving me to only implement a Query trait:

pub trait Query<Input, Output, Message, Reply> {
    /// The error type for the query
    type Error;

    /// Query the service
    fn query(&mut self, input: Input) -> impl Future<Output = Result<Output, Self::Error>>;
}

The trait is generic over Input, Output, Message, and Reply.

  • Input is the type of the input to the query.
  • Output is the type of the output of the query.
  • Message is the type of the message to send to the service.
  • Reply is the type of the reply to expect from the service.

The trait is then implemented for any Service that implements tower::Service<http::Request<http_body::Body>, http::Response<http_body::Body>>. I can then implement my reqwest service, or hyper service, and plug it in without changing the client's API.

Since the Query trait's input is anything that implements Into<http::Request<http_body::Body>>, I can plug in anything that can be converted into an HTTP request. This makes the client very flexible.

As an example, here's a GetUser struct that acts as a command to get a user by their ID:

struct GetUser {
    id: u64,
}

impl From<GetUser> for http::Request<String> {
    fn from(value: GetUser) -> Self {
        let url = format!("https://api.example.com/users/{}", value.id);
        http::Request::get(url)
    }
}

The GetUser struct can now be converted into an HTTP request:

let user = my_reqwest_client.query(GetUser { id: 1 }).await?;
// Or with hyper:
let user = my_hyper_client.query(GetUser { id: 1 }).await?;
// Or through grpc:
let user = my_grpc_client.query(GetUser { id: 1 }).await?;

Summary

The combination of tower, http, http_body, and axum creates an ecosystem for building asynchronous, modular applications in Rust. By adhering to common interfaces and abstractions, these libraries enable developers to build services that are easier to understand, test, and reuse.