Reputation: 420
Is it possible to extract URL path params into a Axum route_service
handler that is implemented using the tower::service_fn
error handling method seen in my main
below? From the Tower docs I see that service_fn
receives the request, but beyond that I can't figure out how to use Path
to extract the person_id
path param part of my /person/:person_id
path.
Here's the page from the Axum docs I used to implement what I have so far: https://docs.rs/axum/latest/axum/error_handling/index.html#routing-to-fallible-services
I've arrived at this implementation for a couple of reasons.
?
operator in order get the actual response from the external API, whether that was success or error.reqwest::Error
status code when there is an error and return that status code for my API's error response.async fn get_person(/*
Is it possible to extract the person_id here given my implemntation?
*/) -> Result<Json<serde_json::Value>, reqwest::Error> {
let request_url = format!("https://swapi.dev/api/people/{}", "1asdf".to_owned());
let response = reqwest::get(request_url).await?;
if response.status().is_success() {
let json = response.json::<serde_json::Value>().await?;
Ok(Json(json))
} else {
Err(response.error_for_status().unwrap_err())
}
}
#[tokio::main]
async fn main() {
dotenv().ok();
let client = reqwest::Client::new();
let cors = CorsLayer::new()
.allow_methods([Method::GET])
.allow_origin(Any);
let faillible_person_service = tower::service_fn(|req: Request| async {
let body = get_person().await?;
Ok::<_, reqwest::Error>(body.into_response())
});
let app = Router::new()
.route("/", get(get_weather))
.route_service(
"/person/:person_id",
HandleError::new(faillible_person_service, handle_reqwest_error),
)
.layer(cors)
.with_state(client);
let listener = tokio::net::TcpListener::bind("127.0.0.1:1337")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn handle_reqwest_error(err: reqwest::Error) -> Response {
// Rather than have my API return a 200, return the status code from the Reqwest call
let status = err.status();
let status = status.unwrap_or(reqwest::StatusCode::INTERNAL_SERVER_ERROR);
let status_as_u16 = status.as_u16();
let axum_status =
StatusCode::from_u16(status_as_u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let res_json = Json(serde_json::json!({
"error": {
"message": format!("Something went wrong: {}", err),
},
}));
return (axum_status, res_json).into_response();
}
Upvotes: 2
Views: 623
Reputation: 60517
Extracting data like path parameters (a.k.a. extractors), are expressed via the FromRequest
and FromRequestParts
traits. Path parameters only need the latter since the body is not relevant.
To make use of them in your tower service, you can split the request and call the extractor methods manually. Something like this:
use axum::extract::{FromRequestParts, Path, Request};
use axum::response::{IntoResponse, Response};
use axum::{Json, Router};
async fn get_person(person_id: String) -> Result<Json<serde_json::Value>, reqwest::Error> {
...
}
let faillible_person_service = tower::service_fn(|req: Request| async {
let (mut req_parts, _req_body) = req.into_parts();
let person_id = match Path::<String>::from_request_parts(&mut req_parts, &()).await {
Ok(Path(person_id)) => person_id,
Err(rejection) => return Ok(rejection.into_response()),
};
let body = get_person(person_id).await?;
...
});
The match
is done this way because path extraction (and most any extractor) can fail but the way Axum is designed is such that all its errors return a response, so that's what this does. You can use more FromRequestParts
extractors using this pattern. If you need the body (i.e. a FromRequest
extractor) it must be done last and given the reassembled request via Request::from_parts
.
Honestly, I would not really recommend this and your reasons don't sound like the effort here is justified. If your goal is to simply return a reqwest response as an Axum response, I have an answer here that describes how you could do that: A proxy with axum 0.7 and reqwest 0.12?
Upvotes: 0