How to include own data in error enum that wraps other errors

I'm trying to use reqwest to do a series of HTTP GETs and although many of the error states will be from reqwest not all will be, so I decided to create my own error enum to wrap the reqwest ones and provide the others.

I find though that many reqwest errors do not report the URL that was attempted so I would like for my error display to report that. I cannot work out how to do it though, because when the error variant is wrapped it can only have a source member, not anything else like a String for holding the URL.

Here is a minimal example using thiserror for removing some boiler plate from the error enum.

The first GET succeeds and the second one fails because it's not a valid URL. I can print the attempted URL from the caller but I'd like for my error message to report it. How should that be done?

 use anyhow::Result;

#[derive(Debug, thiserror::Error)]
pub enum WebClientError {
    // I can't use a format string to get the URL our of the `reqwest::Error` because
    // this is a private struct with only some public methods. Also even if I could,
    // `e.url()` doesn't always return a URL, for example when the error occurs during
    // building the URL. I want to report what the caller asked for.
    #[error("request error")]
    Reqwest(#[from] reqwest::Error),
    #[error("unknown web get error")]
    Unknown,
}

pub async fn do_get(
    client: &reqwest::Client,
    url: &str,
) -> Result<reqwest::Response, WebClientError> {
    let response = client.get(url).send().await;

    match response {
        Ok(r) => {
            return Ok(r);
        }
        Err(e) => {
            return Err(WebClientError::Reqwest(e));
        }
    }

    // I haven't got an example of a different kind of error yet, but assume if one does happen
    // then it'll be ::Unknown for now.
    //response.map_err(|_| WebClientError::Unknown)
}

#[tokio::main]
async fn main() -> Result<()> {
    let client = reqwest::Client::new();

    // Some URLs to try to GET. First one should work; the latter two should produce an error from
    // reqwest.
    let urls = ["http://example.com", "foo", "telnet://"];

    for url in urls.iter() {
        let result = do_get(&client, url).await?;
        println!("Successful GET for <{}>:", url);
        println!("\t{:?}", result);
    }

    Ok(())
}

When run the above will produce:

Successful GET for <http://example.com>:
        Response { url: Url { scheme: "http", cannot_be_a_base: false, username: "", password: None, host: Some(Domain("example.com")), port: None, path: "/", query: None, fragment: None }, status: 200, headers: {"accept-ranges": "bytes", "age": "311703", "cache-control": "max-age=604800", "content-type": "text/html; charset=UTF-8", "date": "Fri, 17 May 2024 18:34:07 GMT", "etag": "\"3147526947\"", "expires": "Fri, 24 May 2024 18:34:07 GMT", "last-modified": "Thu, 17 Oct 2019 07:18:26 GMT", "server": "ECAcc (nyd/D13B)", "vary": "Accept-Encoding", "x-cache": "HIT", "content-length": "1256"} }
Error: request error

Caused by:
    0: builder error
    1: relative URL without a base

Instead of just builder error I'd prefer if it reported something like builder error for <foo>.

Or should I just live with this? If I get rid of the "foo" case so the "telnet://" case is attempted, the error report will be:

Caused by:
    0: builder error for url (telnet://)
    1: URL scheme is not allowed

so reqwest does actually report the URL when it has it, Still I feel like it's bad that I am unable to show what the caller supplied.

Thanks for your thoughts.

#[derive(Debug, thiserror::Error)]
pub enum WebClientError {
    #[error("request to {url} errored")]
    Reqwest {source: reqwest::Error, url: String},
    #[error("unknown web get error")]
    Unknown,
}

pub async fn do_get(
    client: &reqwest::Client,
    url: &str,
) -> Result<reqwest::Response, WebClientError> {
    let response = client.get(url).send().await;

    match response {
        Ok(r) => {
            return Ok(r);
        }
        Err(e) => {
            return Err(WebClientError::Reqwest {source: e, url: url.to_owned()});
        }
    }
}
2 Likes

Awesome, thank you! I can't believe I didn't just try that. I am getting bogged down in why I should use thiserror's #[from] (or the equivalent from impl of my own).

Do I only need to do that when I want to say that "this error variant is actually exactly some other type of error" whereas I can just say the source when I want to say "this error variant was caused by this other error"?

#[from] in thiserror does everything that #[source] does, and it also means that instances of the marked inner error type will be convertable to your error type using the From & Into traits, which in turn lets you apply ? to a Result<T, MarkedInnerErrorType> inside a function that returns Result<U, YourErrorType>. Naturally, this can't be used in an enum variant that contains additional fields (like url here) alongside the inner error, as From::from() doesn't have access to that other data.