use colored::Colorize;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AnesisError {
#[error("You are not logged in.")]
NotLoggedIn,
#[error("Authentication failed. Your session may have expired.")]
HttpUnauthorized,
#[error("{0} was not found.")]
HttpNotFound(String),
#[error("The server returned an error while fetching {0}.")]
HttpServerError(String),
#[error("Could not connect to the server.")]
NetworkConnect,
#[error("The request timed out.")]
NetworkTimeout,
}
pub fn classify_reqwest_error(err: reqwest::Error, resource: &str) -> anyhow::Error {
if err.is_connect() {
return AnesisError::NetworkConnect.into();
}
if err.is_timeout() {
return AnesisError::NetworkTimeout.into();
}
if let Some(status) = err.status() {
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
return AnesisError::HttpUnauthorized.into();
}
if status == reqwest::StatusCode::NOT_FOUND {
return AnesisError::HttpNotFound(resource.to_string()).into();
}
if status.is_server_error() {
return AnesisError::HttpServerError(resource.to_string()).into();
}
}
anyhow::anyhow!("Network error while fetching {resource}")
}
pub fn print_error(err: &anyhow::Error) {
if std::env::var("ANESIS_DEBUG").is_ok() {
eprintln!("{} {:?}", "error:".red().bold(), err);
return;
}
for cause in err.chain() {
if let Some(anesis_err) = cause.downcast_ref::<AnesisError>() {
eprintln!("{} {}", "error:".red().bold(), err);
if let Some(hint) = hint_for_anesis_error(anesis_err) {
eprintln!(" {} {}", "hint:".cyan().bold(), hint);
}
return;
}
}
for cause in err.chain() {
if let Some(reqwest_err) = cause.downcast_ref::<reqwest::Error>() {
if err.downcast_ref::<reqwest::Error>().is_some() {
eprintln!(
"{} {}",
"error:".red().bold(),
friendly_reqwest_message(reqwest_err)
);
} else {
eprintln!("{} {}", "error:".red().bold(), err);
}
if let Some(hint) = hint_for_reqwest_error(reqwest_err) {
eprintln!(" {} {}", "hint:".cyan().bold(), hint);
}
return;
}
}
eprintln!("{} {}", "error:".red().bold(), err);
}
fn hint_for_anesis_error(err: &AnesisError) -> Option<&'static str> {
match err {
AnesisError::NotLoggedIn => Some("Run `anesis login` to authenticate."),
AnesisError::HttpUnauthorized => Some("Run `anesis login` to re-authenticate."),
AnesisError::HttpNotFound(_) => {
Some("Check the name is correct and that you have access.")
}
AnesisError::HttpServerError(_) => {
Some("This is likely a temporary issue. Try again in a moment.")
}
AnesisError::NetworkConnect | AnesisError::NetworkTimeout => {
Some("Check your internet connection and try again.")
}
}
}
fn friendly_reqwest_message(err: &reqwest::Error) -> String {
if err.is_connect() {
return "Could not connect to the server.".to_string();
}
if err.is_timeout() {
return "The request timed out.".to_string();
}
if let Some(status) = err.status() {
if status == reqwest::StatusCode::UNAUTHORIZED || status == reqwest::StatusCode::FORBIDDEN {
return "Authentication failed. Your session may have expired.".to_string();
}
if status == reqwest::StatusCode::NOT_FOUND {
return "The requested resource was not found.".to_string();
}
if status.is_server_error() {
return "The server returned an error. This is likely a temporary issue.".to_string();
}
}
"An unexpected network error occurred.".to_string()
}
fn hint_for_reqwest_error(err: &reqwest::Error) -> Option<&'static str> {
if err.is_connect() || err.is_timeout() {
return Some("Check your internet connection and try again.");
}
if err.status().is_some_and(|s| {
s == reqwest::StatusCode::UNAUTHORIZED || s == reqwest::StatusCode::FORBIDDEN
}) {
return Some("Run `anesis login` to re-authenticate.");
}
None
}