2

I needed to read a simple text data from API using reqwest::get, so I've implemented the following:

#[tokio::main]
async fn process_api() {
    let response_text = reqwest::get("https://httpbin.org/anything")
        .await.unwrap().text()
        .await.unwrap();
    println!("{}", response_text);
}

As per documentation

Because this function may panic, its use is generally discouraged.

So I decided to rewrite it in a safer variant to handle error cases explicitly:

#[tokio::main]
async fn process_api() {
    let response = reqwest::get("https://httpbin.org/anything").await;
    match response {
        Ok(resp) => {
            match resp.text().await {
                Ok(response_text) => {
                    println!("{}", response_text);
                }
                Err(e) => {
                    println!("Error with API while fetching text {}", e)
                }
            }
        }
        Err(e) => {
            println!("Error with API {}", e)
        }
    }
}

But that piece of code looks way too verbose I also tried if let construct:

#[tokio::main]
async fn process_api() {
    let response = reqwest::get("https://httpbin.org/anything").await;
    if let Ok(resp) = response {
        if let Ok(response_text) = resp.text().await {
            println!("{}", response_text);
        } else {
            println!("Error with API while fetching text!")
        }
    } else {
        println!("Error with API");
    }
}

But even though it looks shorter there would be no Error in logs. So it seems even worse in this particular case.

My question - is there a better way to write this?

Note, that I don't want to panic all the way to main but rather gracefully log potential errors.

I honestly tried different variations of map*/and_then and the "best" I got was:

#[tokio::main]
async fn process_api() {
    let response = reqwest::get("https://httpbin.org/anything").await;
    response
        .map(|resp| async move {
            match resp.text().await {
                Ok(response_text) => {
                    println!("{}", response_text);
                }
                Err(e) => {
                    println!("Error with API {}", e)
                }
            }
        }).unwrap().await;
}

but it just looks horrendous.

Enigo
  • 3,685
  • 5
  • 29
  • 54

1 Answers1

3

Change process_response to return a Result, then you can use the ? operator to handle errors.

async fn process_api() -> reqwest::Result<String> {
    let response_text = reqwest::get("https://httpbin.org/anything")
        .await?
        .text().await?;
    Ok(response_text)
}

#[tokio::main]
async fn main() {
    match process_api().await {
        Ok(response) => println!("{}", response)
        Err(e) => eprintln!("Error in request: {:?}", e)
    }
}

If you want extra infomation on your errors, you can utilize anyhow's context method.

async fn process_api() -> anyhow::Result<String> {
    let response_text = reqwest::get("https://httpbin.org/anything")
        .await.context("Failed get request")?
        .text().await.connext("Failed to get text body")?;
    Ok(response_text)
}
pigeonhands
  • 3,066
  • 15
  • 26
  • that would work, but I don't want to change the method signature. I'd like the `process_api` method to be self-sufficient, coz imagine that it would need to return the processed response back to the caller – Enigo Jan 10 '23 at 04:06
  • 1
    @Enigo In that case, my route would to be create a method like `get_text(endpoint: &str)` that just wraps the `reqwest::get(endpoint).text()` and create a method like `get_post()`/`process_api()` that is self-sufficient and calls `get_text("https://httpbin.org/anything")`. – pigeonhands Jan 10 '23 at 04:12
  • Could you add an example of such, maybe? Not sure how that would look like – Enigo Jan 10 '23 at 04:21
  • @Enigo Something like this https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=cb95b16090dd5bc2f35f5b5f85eb2969 – pigeonhands Jan 10 '23 at 04:26
  • Cool, thanks! Maybe you can add it to your answer as well? Altho a bit excessive that I'd need an *extra* method for such. I thought it could be done with `map` somehow – Enigo Jan 10 '23 at 08:44
  • @Enigo The first hurdle with using `map` is the callback isnt `async`, so no `.await` inside the `map` function. In general _best practice_, the implementation and user interaction (such as println) should be separate, so im not sure it would be the best for future reference to change the answer to fit your use-case. imo the current answer outlines how to tidy up error handing in a general circumstance. – pigeonhands Jan 10 '23 at 08:56
  • Well, that's indeed a very subjective topic what to consider *best practices* here. I could argue that following encapsulation principle error handling should be in the module as long as API is clearly defined. That especially true for complex systems – Enigo Jan 10 '23 at 13:10