use thiserror::Error as ThisError;
#[derive(Debug, ThisError)]
#[non_exhaustive]
pub enum Error {
#[error("network error: {message}")]
Network { message: String },
#[error("authentication error: {message}")]
Auth { message: String },
#[error("request rejected: {message}")]
Rejected { message: String },
#[error("failed to parse response: {message}")]
Parse { message: String },
#[error("unsupported operation: {message}")]
Unsupported { message: String },
}
pub type Result<T> = std::result::Result<T, Error>;
impl From<reqwest::Error> for Error {
fn from(err: reqwest::Error) -> Self {
if err.is_decode() {
Error::Parse {
message: scrub(&err.to_string()),
}
} else {
Error::Network {
message: scrub(&err.to_string()),
}
}
}
}
pub(crate) fn status_error(status: reqwest::StatusCode, url: &str, body: &str) -> Error {
let message = scrub(&format!("HTTP {status} from {url}: {body}"));
if status.as_u16() == 401 || status.as_u16() == 403 {
Error::Auth { message }
} else {
Error::Network { message }
}
}
pub(crate) fn scrub(raw: &str) -> String {
match (raw.find("://"), raw.find('@')) {
(Some(scheme_end), Some(at)) if at > scheme_end + 3 => {
let mut out = String::with_capacity(raw.len());
out.push_str(&raw[..scheme_end + 3]);
out.push_str(&raw[at + 1..]);
out
}
_ => raw.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scrub_strips_userinfo_from_url() {
let raw = "error sending request for url (http://admin:secret@192.0.2.1/status)";
let scrubbed = scrub(raw);
assert!(!scrubbed.contains("secret"));
assert!(scrubbed.contains("192.0.2.1/status"));
}
#[test]
fn scrub_leaves_plain_messages_untouched() {
let raw = "connection refused";
assert_eq!(scrub(raw), raw);
}
#[test]
fn status_error_401_is_auth() {
let err = status_error(
reqwest::StatusCode::UNAUTHORIZED,
"http://192.0.2.1/status",
"",
);
assert!(matches!(err, Error::Auth { .. }));
}
#[test]
fn status_error_403_is_auth() {
let err = status_error(
reqwest::StatusCode::FORBIDDEN,
"http://192.0.2.1/status",
"",
);
assert!(matches!(err, Error::Auth { .. }));
}
#[test]
fn status_error_500_is_network() {
let err = status_error(
reqwest::StatusCode::INTERNAL_SERVER_ERROR,
"http://192.0.2.1/status",
"",
);
assert!(matches!(err, Error::Network { .. }));
}
}