Crate tower_http_client

source
Expand description

§Overview

This library provides middlewares and various utilities for HTTP-clients.

Thus, it extends the tower_http functionality for creating HTTP clients using tower middlewares.

At the moment, the de facto standard client library is reqwest, which is poorly compatible with the tower services, but thanks to the tower_reqwest crate, it can be used with the any tower_http layers.

The first goal of the project is to create a more flexible and extensible alternative for reqwest_middleware.

§Examples

Simple client usage with layers from the tower_http.

use http::{header::USER_AGENT, HeaderValue};
use tower::{ServiceBuilder, ServiceExt};
use tower_http::ServiceBuilderExt;
use tower_http_client::{ServiceExt as _, ResponseExt as _};
use tower_reqwest::HttpClientLayer;

/// Implementation agnostic HTTP client.
type HttpClient = tower::util::BoxCloneService<
    http::Request<reqwest::Body>,
    http::Response<reqwest::Body>,
    anyhow::Error,
>;

/// Creates HTTP client with Tower layers on top of the given client.
fn make_client(client: reqwest::Client) -> HttpClient {
    ServiceBuilder::new()
        // Add some layers.
        .override_request_header(USER_AGENT, HeaderValue::from_static("tower-http-client"))
        // Make client compatible with the `tower-http` layers.
        .layer(HttpClientLayer)
        .service(client)
        .map_err(anyhow::Error::from)
        .boxed_clone()
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create a new client
    let client = make_client(reqwest::Client::new());
    // Execute request by using this service.
    let response = client
        .clone()
        .get("http://ip.jsontest.com")
        .send()?
        .await?;

    let text = response.body_reader().utf8().await?;
    println!("{text}");

    Ok(())
}

An example of multi-threaded concurrent requests sending routine with the requests rate limit.

use std::time::Duration;

use http::{Request, Response};
use reqwest::Body;
use tower::{ServiceBuilder, ServiceExt as _};
use tower_http_client::ServiceExt as _;
use tower_reqwest::HttpClientLayer;
use wiremock::{
    matchers::{method, path},
    Mock, MockServer, ResponseTemplate,
};

type HttpClient = tower::util::BoxCloneService<Request<Body>, Response<Body>, anyhow::Error>;

#[derive(Clone)]
struct State {
    host: String,
    client: HttpClient,
}

impl State {
    async fn get_hello(&mut self) -> anyhow::Result<()> {
        let response = self
            .client
            .get(format!("{}/hello", self.host))
            .send()?
            .await?;

        anyhow::ensure!(response.status().is_success(), "response failed");

        Ok(())
    }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    eprintln!("-> Spawning a mock http server...");

    let mock_server = MockServer::start().await;
    let mock_uri = mock_server.uri();

    // Arrange the behaviour of the MockServer adding a Mock:
    // when it receives a GET request on '/hello' it will respond with a 200.
    Mock::given(method("GET"))
        .and(path("/hello"))
        .respond_with(ResponseTemplate::new(200))
        // Mounting the mock on the mock server - it's now effective!
        .mount(&mock_server)
        .await;

    eprintln!("-> Creating an HTTP client with Tower layers...");

    let state = State {
        host: mock_uri,
        client: ServiceBuilder::new()
            // Add some layers.
            .buffer(64)
            .rate_limit(2, Duration::from_secs(1))
            .concurrency_limit(5)
            // Make client compatible with the `tower-http` layers.
            .layer(HttpClientLayer)
            .service(reqwest::Client::new())
            .map_err(anyhow::Error::msg)
            .boxed_clone(),
    };

    eprintln!("-> Sending concurrent requests...");

    let tasks = (0..5).map({
        |i| {
            let state = state.clone();
            tokio::spawn(async move {
                let mut state = state.clone();
                for j in 0..5 {
                    state.get_hello().await?;
                    eprintln!("[task {i}]: Request #{j} completed successfully!");
                }

                anyhow::Ok(())
            })
        }
    });

    let results = futures_util::future::join_all(tasks).await;
    for result in results {
        result??;
    }

    Ok(())
}

Re-exports§

Modules§

  • adaptersreqwest
    Adapters for various clients.
  • Extensions for Tower services that provide HTTP clients implementation.
  • utilutil
    Various extra utility types and functions.