Extract route path param into Axum handler using tower::service_fn?

180 views Asked by At

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.

  1. I want to use the ? operator in order get the actual response from the external API, whether that was success or error.
  2. I want to be able to use the 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();
}
0

There are 0 answers