1

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.
Redline
  • 441
  • 8
  • 20

1 Answers1

2

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()
}

Redline
  • 441
  • 8
  • 20