use core::fmt;
use reqwest::StatusCode;
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
InvalidBaseUrl(String),
InvalidPath(String),
InvalidHeader(String),
InvalidTlsConfig(String),
InvalidTimeout(&'static str),
InvalidParameter(String),
Internal(&'static str),
Http(reqwest::Error),
Transport(&'static str),
Decode(String),
Api {
status: reqwest::StatusCode,
errors: Vec<String>,
},
MissingField(&'static str),
MissingToken,
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidBaseUrl(message) => {
write!(formatter, "invalid OpenBao base URL: {message}")
}
Self::InvalidPath(message) => write!(formatter, "invalid OpenBao path: {message}"),
Self::InvalidHeader(message) => write!(formatter, "invalid OpenBao header: {message}"),
Self::InvalidTlsConfig(message) => {
write!(formatter, "invalid OpenBao TLS configuration: {message}")
}
Self::InvalidTimeout(message) => {
write!(
formatter,
"invalid OpenBao timeout configuration: {message}"
)
}
Self::InvalidParameter(message) => {
write!(formatter, "invalid OpenBao request parameter: {message}")
}
Self::Internal(message) => write!(formatter, "internal OpenBao SDK error: {message}"),
Self::Http(error) => write!(formatter, "OpenBao HTTP error: {error}"),
Self::Transport(message) => write!(formatter, "OpenBao transport error: {message}"),
Self::Decode(error) => write!(formatter, "OpenBao decode error: {error}"),
Self::Api { status, errors } if errors.is_empty() => {
write!(formatter, "OpenBao API returned {status}")
}
Self::Api { status, errors } => {
write!(formatter, "OpenBao API returned {status}: ")?;
for (index, error) in errors.iter().enumerate() {
if index > 0 {
write!(formatter, "; ")?;
}
write!(formatter, "{}", sanitize_api_error(error))?;
}
Ok(())
}
Self::MissingField(field) => {
write!(formatter, "OpenBao response missing field `{field}`")
}
Self::MissingToken => write!(
formatter,
"OpenBao client is missing an authentication token"
),
}
}
}
impl Error {
pub fn status(&self) -> Option<StatusCode> {
match self {
Self::Api { status, .. } => Some(*status),
_ => None,
}
}
pub fn is_not_found(&self) -> bool {
self.status() == Some(StatusCode::NOT_FOUND)
}
pub fn is_forbidden(&self) -> bool {
self.status() == Some(StatusCode::FORBIDDEN)
}
pub fn is_bad_request(&self) -> bool {
self.status() == Some(StatusCode::BAD_REQUEST)
}
pub fn is_rate_limited(&self) -> bool {
self.status() == Some(StatusCode::TOO_MANY_REQUESTS)
}
pub fn is_sealed(&self) -> bool {
self.status() == Some(StatusCode::SERVICE_UNAVAILABLE)
}
pub fn is_temporary(&self) -> bool {
match self {
Self::Transport(_) | Self::Http(_) => true,
Self::Api { status, .. } => {
*status == StatusCode::TOO_MANY_REQUESTS
|| *status == StatusCode::SERVICE_UNAVAILABLE
|| status.is_server_error()
}
_ => false,
}
}
pub fn is_permission_denied(&self) -> bool {
self.status() == Some(StatusCode::FORBIDDEN)
|| matches!(
self,
Self::Api { errors, .. }
if errors.iter().any(|message| message
.to_ascii_lowercase()
.contains("permission denied"))
)
}
pub fn is_conflict(&self) -> bool {
match self {
Self::Api { status, .. } if *status == StatusCode::CONFLICT => true,
Self::Api { status, errors } if *status == StatusCode::BAD_REQUEST => {
errors.iter().any(|message| {
let message = message.to_ascii_lowercase();
message.contains("already in use")
|| message.contains("already exists")
|| message.contains("existing key")
})
}
_ => false,
}
}
}
pub(crate) fn sanitize_api_error(error: &str) -> String {
const MAX_API_ERROR_BYTES: usize = 512;
let mut sanitized = String::new();
for character in error.chars().filter(|character| !character.is_control()) {
let next_len = sanitized.len() + character.len_utf8();
if next_len > MAX_API_ERROR_BYTES {
break;
}
sanitized.push(character);
}
sanitized
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Http(error) => Some(error),
_ => None,
}
}
}
impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
http_transport_error(error)
}
}
pub(crate) fn http_transport_error(error: reqwest::Error) -> Error {
Error::Transport(classify_http_transport_error(&error))
}
fn classify_http_transport_error(error: &reqwest::Error) -> &'static str {
if error.is_timeout() {
"request timed out"
} else if error.is_connect() {
"connection failed"
} else if error.is_redirect() {
"redirect failed"
} else if error.is_body() {
"request or response body failed"
} else if error.is_decode() {
"response body could not be decoded"
} else if error.is_request() {
"request could not be sent"
} else {
"request failed before an OpenBao response was received"
}
}
#[cfg(test)]
mod tests {
use reqwest::StatusCode;
use super::{Error, sanitize_api_error};
#[test]
fn display_sanitizes_api_errors() {
let error = Error::Api {
status: StatusCode::BAD_REQUEST,
errors: vec![format!("bad\nmessage\r{}", "x".repeat(600))],
};
let message = error.to_string();
assert!(!message.contains('\n'));
assert!(!message.contains('\r'));
assert!(message.len() < 600);
}
#[test]
fn api_error_sanitizer_truncates_by_bytes() {
let sanitized = sanitize_api_error(&"💣".repeat(200));
assert!(sanitized.len() <= 512);
assert!(sanitized.is_char_boundary(sanitized.len()));
}
#[test]
fn api_error_helpers_expose_status() {
let error = Error::Api {
status: StatusCode::NOT_FOUND,
errors: Vec::new(),
};
assert_eq!(error.status(), Some(StatusCode::NOT_FOUND));
assert!(error.is_not_found());
assert!(!Error::MissingToken.is_not_found());
}
#[test]
fn api_error_helpers_cover_common_statuses() {
let forbidden = Error::Api {
status: StatusCode::FORBIDDEN,
errors: Vec::new(),
};
assert!(forbidden.is_forbidden());
let sealed = Error::Api {
status: StatusCode::SERVICE_UNAVAILABLE,
errors: Vec::new(),
};
assert!(sealed.is_sealed());
let bad_request = Error::Api {
status: StatusCode::BAD_REQUEST,
errors: Vec::new(),
};
assert!(bad_request.is_bad_request());
let rate_limited = Error::Api {
status: StatusCode::TOO_MANY_REQUESTS,
errors: Vec::new(),
};
assert!(rate_limited.is_rate_limited());
assert!(rate_limited.is_temporary());
let permission_denied = Error::Api {
status: StatusCode::BAD_REQUEST,
errors: vec!["permission denied".to_owned()],
};
assert!(permission_denied.is_permission_denied());
assert!(!permission_denied.is_forbidden());
let duplicate = Error::Api {
status: StatusCode::BAD_REQUEST,
errors: vec!["path is already in use".to_owned()],
};
assert!(duplicate.is_conflict());
}
}