Use tower-http service as generic inner HTTP client

I'd like to implement a client for some HTTP API and want it to be generic over the actual inner HTTP client, i.e. the user should pass their own setup reqwest or Hyper Client instance (possibly with some layers to adapt request/response types, but I didn't even get to that part yet)

use std::convert::Infallible;
use std::fmt::Debug;
use std::marker::PhantomData;

// the actual implementation would take some domain types as parameters
// and create a Full body
fn create_request() -> http::Request<impl http_body::Body<Error = Infallible>> {
    http::Request::builder()
        .body(http_body_util::Empty::<bytes::Bytes>::new())
        .unwrap()
}

pub struct Client<S, ReqBody, ResBody> {
    service: S,

    _req_body: PhantomData<ReqBody>,
    _res_body: PhantomData<ResBody>,
}

impl<S, ReqBody, ResBody> Client<S, ReqBody, ResBody>
where
    S: tower::Service<http::Request<ReqBody>, Response = http::Response<ResBody>> + Clone,
    ReqBody: http_body::Body<Error = Infallible>,
    <S as tower::Service<http::Request<ReqBody>>>::Error: Debug, // for unwrap
{
    pub fn new(service: S) -> Self {
        Self {
            service,
            _req_body: PhantomData,
            _res_body: PhantomData,
        }
    }

    pub async fn send_request(&self) {
        use tower::ServiceExt as _;

        let request = create_request();

        let response = self
            .service
            .clone()
            .ready()
            .await
            .unwrap()
            .call(request)
            .await
            .unwrap();

        todo!()
    }
}

This results in an error about types not matching:

error[E0308]: mismatched types
   --> src/lib.rs:43:19
    |
  5 | fn create_request() -> http::Request<impl http_body::Body<Error = Infallible>> {
    |                                      ---------------------------------------- the found opaque type
...
 18 | impl<S, ReqBody, ResBody> Client<S, ReqBody, ResBody>
    |         ------- expected this type parameter
...
 43 |             .call(request)
    |              ---- ^^^^^^^ expected `Request<ReqBody>`, found `Request<impl Body<Error = Infallible>>`
    |              |
    |              arguments to this method are incorrect
    |
    = note: expected struct `http::Request<ReqBody>`
               found struct `http::Request<impl Body<Error = Infallible>>`
    = help: type parameters must be constrained to match other types
    = note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
help: the return type of this call is `http::Request<impl Body<Error = Infallible>>` due to the type of the argument passed
   --> src/lib.rs:37:24
    |
 37 |           let response = self
    |  ________________________^
 38 | |             .service
 39 | |             .clone()
 40 | |             .ready()
 41 | |             .await
 42 | |             .unwrap()
 43 | |             .call(request)
    | |___________________-------^
    |                     |
    |                     this argument influences the return type of `call`
note: method defined here
   --> /playground/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tower-service-0.3.3/src/lib.rs:355:8
    |
355 |     fn call(&mut self, req: Request) -> Self::Future;
    |        ^^^^

I'm stuck here since I don't understand why the types aren't matching, didn't I constrain them to be bound by the same traits? Those do not show up in the error however
This is likely Rust basics and not specific to tower-http, just my first time dabbling into more complex generics and where clauses

I've briefly tried to add S::Request: TryFrom<http::Request<impl Body<Error = Infallible>>> but that just lead to this trait not being implemented (and I think this also wouldn't work with orphan rule in all those thirdparty types?)

Does this require Box<dyn trait> ? If so I think it would not be compatible with tower::Service anymore
I got it compiling by returning / using http_body_util::Full instead of impl http_body::Body, but I would like to avoid naming a concrete (and thirdparty) type

P.S.: I've posted this as a question in the tower Discord, but that only helped confirm the general approach so far, not how to solve the concrete compilation error

ReqBody is determined outside of the function; inside the function, it represents a single type. And inside the function, you can only rely on your trait bounds, like:

S: Service<Request<ReqBody>>

So you need to pass a Request<ReqBody> to call. But instead you're passing whatever gets returned from create_request -- a Request<SomeOpaqueType>.

That's the type mismatch. It's like this situation.


Maybe you want something like

// "S impements Service<Request<ReqBody>> for all ReqBody where ..."
S: for<ReqBody: http_body::Body<Error = Infallible>> Service<Request<ReqBody>>

Rust doesn't support that yet. dyn Trait is sometimes a way to work around it.

Unfortunately I'm out of time right now, so I can't attempt to get it working with dyn Trait. (I might try later if no one beats me to it.)

1 Like

One way you can make it work is to use a generic on create_request but then the drawback is that you will have to provide a body when you call send_request. Depends on your design, this approach might not work for you.

2 Likes

Here's a dyn approach.

The Data associated type had to be specified. If that's too restrictive, you'd need to add generics or maybe type erase that associated type too... depending again on your design.

1 Like

I've only now got to test this with my codebase, sorry for the delayed response.

This is what tripped me up, Rust basics indeed!
Not sure what rustc errors could have done differently, but your explanation and example helped.

I guess so; I want it to work with "all" impl Body, but really just the concrete ones from within my functions.
Unfortunate that this does not work yet.

Yeah, I think this is just kicking the can down the road for a bit. The user does not provide the body impl and when I do - just a bit later in another function - then I run into the above issue of it being determined inside my function.

I believe this is not in the spirit of tower-http, so I'll see how far I get without dynamic dispatch.


Thank you both @quinedot @chung ! :heart:

I've now arrived at something like:

pub type Body = http_body_util::Full<bytes::Bytes>;

pub struct Client<S> {
     service: S,
}

impl<S, B> Client<S>
where
    S: tower::Service<http::Request<crate::Body>, Response = http::Response<B>, Error: Error>
        + Clone,
    B: http_body::Body,
{
    pub fn new(service: S) -> Self {}

    pub async fn send_request(&self) {}
}

And to wire this into reqwest:

impl<S> tower::Service<http::Request<crate::Body>> for ReqwestService<S>
where
    S: Service<reqwest::Request, Response = reqwest::Response, Error = reqwest::Error>,
{
    type Response = http::Response<reqwest::Body>;
    type Error = S::Error;
    type Future = ReqwestFuture<S>;
}

impl<S> Future for ReqwestFuture<S>
where
    S: Service<reqwest::Request>,
    http::Response<reqwest::Body>: From<S::Response>,
{
    type Output = Result<http::Response<reqwest::Body>, S::Error>;
}

impl Client<ReqwestService<reqwest::Client>> {
    pub fn from_reqwest(
        client: reqwest::Client,
    ) -> Self {
        let compat = ReqwestLayer(());
        let service = compat.layer(client);
        Client::new(service)
    }
}

Haven't yet tried with hyper (legacy) client, might not even need a compatibility layer.

This works for sending requests, I still got to write code to deal with the response and body..

Now this does use a concrete crate::Body instead of impl http_body::Body, so I guess I'll have to revisit the implementation when Rust supports the for constraint.

I was going to suggest something like

// This is here to simplify your bounds and encapsulates the original bounds
// on the associated types etc.  It's implemented for all types that meet the
// bounds.
pub trait Bikeshed<ReqBody>: Service<ReqBody, ...> { ... }

// `ResBody` may not be needed anymore but anyway
impl<S, ReqBody, ResBody> Client<S, ReqBody, ResBody>
where
    S: Clone
        + Bikeshed<http_body_util::Empty<bytes::Bytes>>
        + Bikeshed<http_body_util::Full<bytes::Bytes>>
{ ... }

so that S has to work with all the body types you want (a finite list -- the concrete ones from your functions).[1]

However, when I applied it to your OP the multiple bounds made the call to .ready ambiguous. Maybe it's still useful if you don't mind needing to disambiguate "manually", but maybe it's just not worth it. :person_shrugging:


  1. You could simplify this again to a single bound. â†Šī¸Ž