API design: taking self by shared or exclusive ref, and figuring out if tower::Service is a good choice for HTTP

I'm writing a client for a type of API-based HTTP service. Because the API crate is useful as a library crate and HTTP is used trivially I'd like to not pick the HTTP implementation myself, though so far I'll be using hyper in the top-level program. I'd also like the HTTP client to be required as late as possible in the build for better build parallelism.

hyper::Client::request is the only method I really need from an http client, and I can easily keep an hyper::Client in my ApiClient struct.

If I try to switch to the Tower Service trait, which seems like a sufficiently generic abstraction that has no dependencies, my ApiClient<S: tower_service::Service> is forced to take &mut self on almost all methods, despite keeping no relevant state. This is a poor abstraction because my API is stateless, the HTTP layer also is, and my API becomes much harder to use.

I might still be able to work with tower by having ApiClient require an S: Clone bound as well and implementing Service on &hyper::Client (references are cloned for free), but at this point the traits are working against me.

What is the simplest abstraction I could use for HTTP? Something like Fn(Request) -> ResponseFuture? Is there something like that already defined?

Implementing tower::Service for a shared reference type seems reasonable to me (and it follows a pattern that appears even in std, like impl<'a> Read for &'a TcpStream). I don't understand what you have in mind here:

Why would Clone necessarily be involved with this approach?

The clone idea was something like this:

struct ApiClient<S: Service + Clone> {
    http: S,
}

impl<S: Service + Clone> ApiClient {
    pub async fn frobs(&self) {
        let mut http = self.http.clone();
        http.call(http::Request::get("http://example.com")).await
    }
}

Now frobs doesn't need &mut self, but it's not great, because you wouldn't want to actually use it on a random Service impl, only one that is trivial to clone.

Anyway, I went with my own trait, which is easily satisfied by hyper, isahc and surf:

pub trait HttpClient {
    fn call(&self, request: Request<Body>) -> Self::ResponseFuture;
    type ResponseFuture: Future<Output = std::result::Result<Response<Body>, Self::Error>> + Send;
    type Error: Into<crate::Error>;
}

pub struct HttpFn<F> {
    f: F,
}

pub fn http_fn<F>(f: F) -> HttpFn<F> {
    HttpFn { f }
}

impl<F, R, E> HttpClient for HttpFn<F>
where
    F: Fn(Request<Body>) -> R,
    R: Future<Output = std::result::Result<Response<Body>, E>> + Send,
    E: Into<crate::Error>,
{
    type ResponseFuture = R;

    type Error = E;

    fn call(&self, request: Request<Body>) -> Self::ResponseFuture {
        (self.f)(request)
    }
}

Can be used like this:

let client = hyper::Client::new();
let http = http_fn(move |r| client.request(r));

It can't wrap a tower_service::Service without interior mutability or cloning shenanigans, but it seems Tower support is less practical than the rest of the http ecosystem.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.