rustreqwestrust-axum

Axum State for Reqwest Client


I am trying to create a shared reqwest Client to be used by request handlers in Axum, but I can't figure out how to add, extract, or wrap it so the type checking on the request handler passes.

I tried this:

use axum::{extract::State, routing::get, Router};
use reqwest::Client;

struct Config {
    secret: String,
}

#[tokio::main]
async fn main() {
    let client = reqwest::Client::new();
    let config = &*Box::leak(Box::new(Config {
        secret: "".to_string(),
    }));

    let app = Router::new()
        .route("/", get(index))
        .with_state(config)
        .with_state(client);

    axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

async fn index(State(config): State<Config>, State(client): State<Client>) -> String {
    client
        .get("https://example.com/")
        .header("Cookie", &config.secret)
        .send()
        .await
        .unwrap()
        .text()
        .await
        .unwrap()
}

But it gave me this compiler error:

error[E0277]: the trait bound `fn(State<Config>, State<Client>) -> impl Future<Output = String> {index}: Handler<_, _, _>` is not satisfied
   --> grocers_app/src/main.rs:16:25
    |
16  |         .route("/", get(index))
    |                     --- ^^^^^ the trait `Handler<_, _, _>` is not implemented for fn item `fn(State<Config>, State<Client>) -> impl Future<Output = String> {index}`
    |                     |
    |                     required by a bound introduced by this call
    |
    = help: the following other types implement trait `Handler<T, S, B>`:
              <Layered<L, H, T, S, B, B2> as Handler<T, S, B2>>
              <MethodRouter<S, B> as Handler<(), S, B>>
note: required by a bound in `axum::routing::get`
   --> /home/redline/.cargo/registry/src/github.com-1ecc6299db9ec823/axum-0.6.10/src/routing/method_routing.rs:403:1
    |
403 | top_level_handler_fn!(get, GET);
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `get`
    = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0277`.```
And also tried references and `Arc`, but references didn't work, and reqwest docs mention that wrapping the `Client` is not needed.

I also read in the docs that the type inside of `State` needs to be `Clone`, and I assume this is why it isn't working.

Solution

  • The issue is not the fact that request::Client cannot be shared with axum State, but that you can only have a single state for a router, and in the example above they are overwriting each other so it's not possible to use both States in the handler.

    However you can have substates by implementing FromRef. In a blog post of axum they show it as being derived, but since it didn't work for me I did it manually like in the docs for v0.6.11. https://docs.rs/axum/0.6.11/axum/extract/struct.State.html#substates

    The following code works:

    use axum::{
        extract::{FromRef, State},
        routing::get,
        Router,
    };
    use reqwest::Client;
    
    struct Config {
        secret: String,
    }
    
    #[derive(Clone)]
    struct AppState {
        config: &'static Config,
        client: Client,
    }
    
    impl FromRef<AppState> for &Config {
        fn from_ref(app_state: &AppState) -> &'static Config {
            app_state.config.clone()
        }
    }
    
    impl FromRef<AppState> for Client {
        fn from_ref(app_state: &AppState) -> Client {
            app_state.client.clone()
        }
    }
    
    #[tokio::main]
    async fn main() {
        let client = reqwest::Client::new();
        let config = &*Box::leak(Box::new(Config {
            secret: "".to_string(),
        }));
    
        let state = AppState { config, client };
    
        let app = Router::new()
            .route("/", get(index))
            .with_state(state);
    
        axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
            .serve(app.into_make_service())
            .await
            .unwrap();
    }
    
    async fn index(State(config): State<&Config>, State(client): State<Client>) -> String {
        client
            .get("https://example.com/")
            .header("Cookie", &config.secret)
            .send()
            .await
            .unwrap()
            .text()
            .await
            .unwrap()
    }