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.
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 State
s 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()
}