use crate::responses;
use thiserror::Error;
use backtrace::Backtrace;
use reqwest::{
StatusCode, Url,
header::{HeaderMap, InvalidHeaderValue},
};
use serde::Deserialize;
#[derive(Error, Debug)]
pub enum EndpointValidationError {
#[error("unsupported endpoint scheme (expected http:// or https://): {endpoint}")]
UnsupportedScheme { endpoint: String },
#[error("failed to build HTTP client: {source}")]
ClientBuildError {
#[source]
source: reqwest::Error,
},
}
#[derive(Error, Debug)]
pub enum ConversionError {
#[error("Unsupported argument value for property (field) {property}")]
UnsupportedPropertyValue { property: String },
#[error("Missing the required argument")]
MissingProperty { argument: String },
#[error("Could not parse a value: {message}")]
ParsingError { message: String },
#[error("Invalid type: expected {expected}")]
InvalidType { expected: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct ErrorDetails {
pub error: Option<String>,
pub reason: Option<String>,
}
impl ErrorDetails {
pub fn from_json(body: &str) -> Option<Self> {
serde_json::from_str(body).ok()
}
pub fn reason(&self) -> Option<&str> {
self.reason.as_deref().or(self.error.as_deref())
}
}
#[derive(Error, Debug)]
pub enum Error<U, S, E, BT> {
#[error("API responded with a client error: status code of {status_code}")]
ClientErrorResponse {
url: Option<U>,
status_code: S,
body: Option<String>,
error_details: Option<ErrorDetails>,
headers: Option<HeaderMap>,
backtrace: BT,
},
#[error("API responded with a server error: status code of {status_code}")]
ServerErrorResponse {
url: Option<U>,
status_code: S,
body: Option<String>,
error_details: Option<ErrorDetails>,
headers: Option<HeaderMap>,
backtrace: BT,
},
#[error("Health check failed")]
HealthCheckFailed {
path: String,
details: responses::HealthCheckFailureDetails,
status_code: S,
},
#[error("API responded with a 404 Not Found")]
NotFound,
#[error(
"Cannot delete a binding: multiple matching bindings were found, provide additional properties"
)]
MultipleMatchingBindings,
#[error("could not convert provided value into an HTTP header value: {error}")]
InvalidHeaderValue { error: InvalidHeaderValue },
#[error("Unsupported argument value for property (field) {property}")]
UnsupportedArgumentValue { property: String },
#[error("Missing required argument")]
MissingProperty { argument: String },
#[error("Response is incompatible with the target data type: {error}")]
IncompatibleBody {
error: ConversionError,
backtrace: BT,
},
#[error("Could not parse a value: {message}")]
ParsingError { message: String },
#[error("encountered an error when performing an HTTP request: {error}")]
RequestError { error: E, backtrace: BT },
#[error("an unspecified error")]
Other,
}
#[allow(unused)]
pub type HttpClientError = Error<Url, StatusCode, reqwest::Error, Backtrace>;
impl From<reqwest::Error> for HttpClientError {
fn from(req_err: reqwest::Error) -> Self {
match req_err.status() {
None => HttpClientError::RequestError {
error: req_err,
backtrace: Backtrace::new(),
},
Some(status_code) => {
if status_code.is_client_error() {
return HttpClientError::ClientErrorResponse {
url: req_err.url().cloned(),
status_code,
body: None,
error_details: None,
headers: None,
backtrace: Backtrace::new(),
};
};
if status_code.is_server_error() {
return HttpClientError::ServerErrorResponse {
url: req_err.url().cloned(),
status_code,
body: None,
error_details: None,
headers: None,
backtrace: Backtrace::new(),
};
};
HttpClientError::RequestError {
error: req_err,
backtrace: Backtrace::new(),
}
}
}
}
}
impl From<InvalidHeaderValue> for HttpClientError {
fn from(err: InvalidHeaderValue) -> Self {
HttpClientError::InvalidHeaderValue { error: err }
}
}
impl From<ConversionError> for HttpClientError {
fn from(value: ConversionError) -> Self {
match value {
ConversionError::UnsupportedPropertyValue { property } => {
HttpClientError::UnsupportedArgumentValue { property }
}
ConversionError::MissingProperty { argument } => {
HttpClientError::MissingProperty { argument }
}
ConversionError::ParsingError { message } => HttpClientError::ParsingError { message },
ConversionError::InvalidType { expected } => HttpClientError::ParsingError {
message: format!("invalid type: expected {expected}"),
},
}
}
}
impl HttpClientError {
pub fn is_not_found(&self) -> bool {
matches!(self, HttpClientError::NotFound)
|| matches!(
self,
HttpClientError::ClientErrorResponse { status_code, .. }
if *status_code == StatusCode::NOT_FOUND
)
}
pub fn is_already_exists(&self) -> bool {
matches!(
self,
HttpClientError::ClientErrorResponse { status_code, .. }
if *status_code == StatusCode::CONFLICT
)
}
pub fn is_unauthorized(&self) -> bool {
matches!(
self,
HttpClientError::ClientErrorResponse { status_code, .. }
if *status_code == StatusCode::UNAUTHORIZED
)
}
pub fn is_forbidden(&self) -> bool {
matches!(
self,
HttpClientError::ClientErrorResponse { status_code, .. }
if *status_code == StatusCode::FORBIDDEN
)
}
pub fn is_client_error(&self) -> bool {
matches!(
self,
HttpClientError::ClientErrorResponse { .. } | HttpClientError::NotFound
)
}
pub fn is_server_error(&self) -> bool {
matches!(self, HttpClientError::ServerErrorResponse { .. })
}
pub fn status_code(&self) -> Option<StatusCode> {
match self {
HttpClientError::ClientErrorResponse { status_code, .. } => Some(*status_code),
HttpClientError::ServerErrorResponse { status_code, .. } => Some(*status_code),
HttpClientError::HealthCheckFailed { status_code, .. } => Some(*status_code),
HttpClientError::NotFound => Some(StatusCode::NOT_FOUND),
_ => None,
}
}
pub fn url(&self) -> Option<&Url> {
match self {
HttpClientError::ClientErrorResponse { url, .. } => url.as_ref(),
HttpClientError::ServerErrorResponse { url, .. } => url.as_ref(),
HttpClientError::RequestError { error, .. } => error.url(),
_ => None,
}
}
pub fn error_details(&self) -> Option<&ErrorDetails> {
match self {
HttpClientError::ClientErrorResponse { error_details, .. } => error_details.as_ref(),
HttpClientError::ServerErrorResponse { error_details, .. } => error_details.as_ref(),
_ => None,
}
}
pub fn is_connection_error(&self) -> bool {
matches!(
self,
HttpClientError::RequestError { error, .. }
if error.is_connect() && !self.is_tls_handshake_error()
)
}
pub fn is_timeout(&self) -> bool {
matches!(
self,
HttpClientError::RequestError { error, .. }
if error.is_timeout()
)
}
pub fn is_tls_handshake_error(&self) -> bool {
match self {
HttpClientError::RequestError { error, .. } if error.is_connect() => {
let debug = format!("{error:?}");
debug.contains("certificate")
|| debug.contains("CertificateRequired")
|| debug.contains("HandshakeFailure")
|| debug.contains("InvalidCertificate")
|| debug.contains("tls")
|| debug.contains("TLS")
|| debug.contains("ssl")
|| debug.contains("SSL")
}
_ => false,
}
}
pub fn as_reqwest_error(&self) -> Option<&reqwest::Error> {
match self {
HttpClientError::RequestError { error, .. } => Some(error),
_ => None,
}
}
pub fn user_message(&self) -> String {
match self {
HttpClientError::ClientErrorResponse {
error_details,
status_code,
..
} => {
if let Some(details) = error_details
&& let Some(reason) = details.reason()
{
return reason.to_owned();
}
format!("Client error: {status_code}")
}
HttpClientError::ServerErrorResponse {
error_details,
status_code,
..
} => {
if let Some(details) = error_details
&& let Some(reason) = details.reason()
{
return reason.to_owned();
}
format!("Server error: {status_code}")
}
HttpClientError::HealthCheckFailed { details, .. } => {
format!("Health check failed: {}", details.reason())
}
HttpClientError::NotFound => "Resource not found".to_owned(),
HttpClientError::MultipleMatchingBindings => {
"Multiple matching bindings found, provide additional properties".to_owned()
}
HttpClientError::InvalidHeaderValue { .. } => "Invalid header value".to_owned(),
HttpClientError::UnsupportedArgumentValue { property } => {
format!("Unsupported value for property: {property}")
}
HttpClientError::MissingProperty { argument } => {
format!("Missing required argument: {argument}")
}
HttpClientError::IncompatibleBody { error, .. } => {
format!("Response parsing error: {error}")
}
HttpClientError::ParsingError { message } => format!("Parsing error: {message}"),
HttpClientError::RequestError { error, .. } => {
format!("Request error: {error}")
}
HttpClientError::Other => "An unspecified error occurred".to_owned(),
}
}
}