Search code examples
rustreqwestrust-axum

A proxy with axum 0.7 and reqwest 0.12 based on http 1?


I have the below code:

#[tokio::main]
async fn main() {
  let app = Router::new()
    .route("/", get(handler))
    .route("/custom/*key", get(proxy).post(proxy));

  // ...
}

pub async fn proxy(mut req: Request<Body>) -> Result<Response<Body>, StatusCode> {
  let path_query = req
    .uri()
    .path_and_query()
    .map_or_else(|| req.uri().path(), |v| v.as_str());

  let uri = format!("http://localhost:9000/{}", path_query);

  *req.uri_mut() = Uri::try_from(uri).unwrap();

  let http_client = reqwest::Client::new();

  let res = http_client.execute(req).await.unwrap()

  if res.status().is_server_error() {
    return Err(StatusCode::INTERNAL_SERVER_ERROR);
  }

  Ok(res)
}

But obviously this doesn't work:

error[E0308]: mismatched types
     |
35   |     let res = http_client.execute(req).await.unwrap();
     |                           ------- ^^^ expected `Request`, found `Request<Incoming>`
     |                           |
     |                           arguments to this method are incorrect
     |
     = note: `Request<Incoming>` and `reqwest::Request` have similar names, but are actually distinct types
note: `Request<Incoming>` is defined in crate `http`
    --> C:\Users\Fred\.cargo\registry\src\index.crates.io-6f17d22bba15001f\http-1.1.0\src\request.rs:158:1
     |
158  | pub struct Request<T> {
     | ^^^^^^^^^^^^^^^^^^^^^
note: `reqwest::Request` is defined in crate `reqwest`
    --> C:\Users\Fred\.cargo\registry\src\index.crates.io-6f17d22bba15001f\reqwest-0.12.4\src\async_impl\request.rs:22:1
     |
22   | pub struct Request {
     | ^^^^^^^^^^^^^^^^^^
note: method defined here
    --> C:\Users\Fred\.cargo\registry\src\index.crates.io-6f17d22bba15001f\reqwest-0.12.4\src\async_impl\client.rs:1965:12
     |
1965 |     pub fn execute(
     |            ^^^^^^^

and:

error[E0308]: mismatched types
    |
43  |     Ok(res)
    |     -- ^^^ expected `Response<Incoming>`, found `Response`
    |     |
    |     arguments to this enum variant are incorrect
    |
    = note: `reqwest::Response` and `Response<Incoming>` have similar names, but are actually distinct types
note: `reqwest::Response` is defined in crate `reqwest`
   --> C:\Users\Fred\.cargo\registry\src\index.crates.io-6f17d22bba15001f\reqwest-0.12.4\src\async_impl\response.rs:29:1

I'm using axum 0.7.5 and reqwest 0.12 (both on http 1).

Is it possible to make it work?


Solution

  • As the error says, a reqwest Request is not the same as an axum Request which is an alias to Request from the http crate. However, they are trying to express the same information so we just need to convert one to the other.

    There is a TryFrom implementation for a reqwest Request to be built from an http Request however there is a snag that the axum Body is not convertible to a reqwest Body. But there is a .map() function on http Request that can transform the body type ourselves.

    We can avoid buffering the full request body by mapping them by byte streams like this:

    let req = req.map(|body| reqwest::Body::wrap_stream(body.into_data_stream()));
    

    However, yet again there is a snag: in the versions you are asking about .into_data_stream() from the axum body doesn't implement Sync that reqwest's Body::wrap_stream requires. Fortunately, there is a workaround - previous versions of reqwest didn't require Sync but were able to keep their Body implementation Sync by using a SyncStream from the sync-wrapper crate (read their docs if you want to know how it works).

    So the full request conversion looks like this:

    use sync_wrapper::SyncStream;
    
    let req = req.map(|body| reqwest::Body::wrap_stream(SyncStream::new(body.into_data_stream())));
    let req = reqwest::Request::try_from(req).unwrap();
    
    let res = http_client.execute(req).await.unwrap();
    

    The try_from call can theoretically fail due to re-parsing the URL from a http Uri to a reqwest Url (aliased from the url crate).

    Converting from the reqwest Response back into an axum/http Response isn't as bad since reqwest Body implements the http Body trait and axum's Body can be constructed by anything implementing the Body trait. And neither of these conversions can fail. Here's how that looks:

    let res = axum::http::Response::from(res);
    let res = res.map(|body| Body::new(body));