Skip to content

Instantly share code, notes, and snippets.

@jmwample
Created March 18, 2026 17:54
Show Gist options
  • Select an option

  • Save jmwample/f6827a9c24c620a597a7d4118a64bbb8 to your computer and use it in GitHub Desktop.

Select an option

Save jmwample/f6827a9c24c620a597a7d4118a64bbb8 to your computer and use it in GitHub Desktop.
Rust nested error handling

Reqwest Error Handling Demo

This is a small PoC for working with nested error handling related to the reqwest crate.

This can demo handling:

  • TCP errors
    • Connection Reset
    • Connection Refused
  • Network Errors
    • Network Unreachable
  • TLS Errors
    • Self Signed
    • wrong-host
    • Expired cert
    • null ciphersuite
    • ... (see https://badssl.com/ ) for more domains
  • DNS errors (can be handled, types not in this demo)
  • Content / Response / Status errors (not what I am interested in handling)

Once sharp edge that I found here is that std::io::Error wants to believe that it is special and that it isn't actually an error layer. So trying to use std::error::Error::source() doesn't return what you think it will return. Instead I have to use get_ref() and then map the dyn type for compatibility.

Seems like this is a conscious choice by the rust devs https://users.rust-lang.org/t/question-about-implementation-of-std-source/121117 so who am I to argue.

[package]
name = "reqwest-err"
version = "0.1.0"
edition = "2024"
[dependencies]
reqwest = "0.13.2"
tokio = { version = "1", features = ["full"] }
rustls = "0.23.37"
use std::error::Error;
use std::io::ErrorKind;
#[tokio::main]
async fn main() -> Result<(), Box<dyn Error>> {
let domain = "httpbin.org";
let addrs = ["127.0.0.1:9000".parse().unwrap()];
let client = reqwest::ClientBuilder::new();
let res = client
.resolve_to_addrs(domain, &addrs)
.build()?
.post("http://wrong.host.badssl.com")
// .post("http://httpbin.org/post")
//.post("http://asdfasdfawefasdfhttpbin.org/post")
.send()
.await
.inspect_err(|err| println!("relevant: {:?}", is_network_error(err)))
.inspect_err(|err| println!("error source: {:?}", err.source().unwrap().source()))?;
println!("{res:?}");
Ok(())
}
const MAX_ERR_SOURCE_ITERATIONS: usize = 4;
/// only if there was a network issue should we consider updating the host info
pub(crate) fn is_network_error(err: &reqwest::Error) -> bool {
if err.is_timeout() {
return true;
}
#[cfg(not(target_arch = "wasm32"))]
if !(err.is_connect() || err.is_request()) {
return false;
}
// The io::Error source is several layers deep, for clarity this is done as a loop
// * reqwest::Error -> hyper_util::Error
// * hyper_util::Error -> hyper_util::ClientError
// * hyper_util::ClientError -> io::Error
let mut inner = err.source();
for _ in 0..MAX_ERR_SOURCE_ITERATIONS {
match inner {
None => break,
Some(e) => {
// try downcast to io::Error from <dyn std::error:Error>
if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
match io_err.kind() {
// device not connected to the internet
ErrorKind::NetworkUnreachable | ErrorKind::NetworkDown => return false,
// connection errors can indicate connection interference
ErrorKind::ConnectionReset
| ErrorKind::HostUnreachable
| ErrorKind::ConnectionRefused => return true,
// TLS errors get wrapped in custom io::Errors
ErrorKind::Other | ErrorKind::InvalidData => {
// io::Error get_ref works while source doesn't here -_-
// if you don't like it take it up with the rust devs https://users.rust-lang.org/t/question-about-implementation-of-std-source/121117
inner = io_err.get_ref().map(|e| e as &dyn std::error::Error);
}
_ => return false,
}
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
return true;
} else {
inner = e.source();
}
}
}
}
false
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment