use std::{error::Error, fmt::Debug, str::from_utf8};
use tonic::codegen::http;
use tonic::metadata::errors::ToStrError;
use tonic::metadata::MetadataMap;
#[derive(Debug, Clone, PartialEq)]
pub enum MomentoErrorCode {
InvalidArgumentError,
UnknownServiceError,
AlreadyExistsError,
CacheNotFoundError,
StoreNotFoundError,
ItemNotFoundError,
InternalServerError,
PermissionError,
AuthenticationError,
CancelledError,
LimitExceededError,
BadRequestError,
TimeoutError,
ServerUnavailable,
ClientResourceExhausted,
FailedPreconditionError,
UnknownError,
Miss,
TypeError,
}
#[derive(Debug, thiserror::Error)]
#[error("{details}")]
pub struct MomentoGrpcErrorDetails {
pub code: tonic::Code,
pub details: String,
pub message: String,
pub metadata: tonic::metadata::MetadataMap,
}
impl From<tonic::Status> for MomentoGrpcErrorDetails {
fn from(status: tonic::Status) -> Self {
MomentoGrpcErrorDetails {
code: status.code(),
details: from_utf8(status.details()).unwrap_or_default().into(),
message: status.message().into(),
metadata: status.metadata().clone(),
}
}
}
#[derive(Debug, thiserror::Error)]
#[error("{message}")]
pub struct MomentoError {
pub message: String,
pub error_code: MomentoErrorCode,
#[source]
pub inner_error: Option<ErrorSource>,
pub details: Option<MomentoGrpcErrorDetails>,
}
impl MomentoError {
pub(crate) fn unknown_error(method_name: &str, details: Option<String>) -> Self {
Self {
message: "Unknown error has occurred, unable to parse ".to_string()
+ method_name
+ " : "
+ details.as_deref().unwrap_or(""),
error_code: MomentoErrorCode::UnknownError,
inner_error: None,
details: None,
}
}
pub(crate) fn miss(method_name: &str) -> Self {
Self {
message: "Received a MISS for ".to_string() + method_name,
error_code: MomentoErrorCode::UnknownError,
inner_error: None,
details: None,
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum ErrorSource {
#[error("unknown source")]
Unknown(#[from] Box<dyn std::error::Error + Send + Sync>),
#[error("tonic transport error")]
TonicTransport(#[from] tonic::transport::Error),
#[error("tonic status error")]
TonicStatus(#[from] tonic::Status),
#[error("uri is invalid")]
InvalidUri(#[from] http::uri::InvalidUri),
#[error("unable to parse response metadata value")]
MetadataValueError(#[from] ToStrError),
}
impl From<tonic::Status> for MomentoError {
fn from(s: tonic::Status) -> Self {
status_to_error(s)
}
}
pub(crate) fn status_to_error(status: tonic::Status) -> MomentoError {
log::debug!("translating raw status to error: {status:?}");
match status.code() {
tonic::Code::InvalidArgument => MomentoError {
message: "Invalid argument passed to Momento client".into(),
error_code: MomentoErrorCode::InvalidArgumentError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::Unimplemented => MomentoError {
message: "The request was invalid; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::BadRequestError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::OutOfRange => MomentoError {
message: "The request was invalid; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::BadRequestError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::FailedPrecondition => MomentoError {
message: "System is not in a state required for the operation's execution".into(),
error_code: MomentoErrorCode::FailedPreconditionError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::Cancelled => MomentoError {
message: "The request was cancelled by the server; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::CancelledError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::DeadlineExceeded => MomentoError {
message: "The client's configured timeout was exceeded; you may need to use a Configuration with more lenient timeouts".into(),
error_code: MomentoErrorCode::TimeoutError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::PermissionDenied => MomentoError {
message: "Insufficient permissions to perform an operation on a cache".into(),
error_code: MomentoErrorCode::PermissionError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::Unauthenticated => MomentoError {
message: "Invalid authentication credentials to connect to cache service".into(),
error_code: MomentoErrorCode::AuthenticationError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::ResourceExhausted => MomentoError {
message: determine_limit_exceeded_message_wrapper(status.metadata(), status.message()),
error_code: MomentoErrorCode::LimitExceededError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::NotFound => {
match status.metadata().get("err") {
None => MomentoError {
message: "A cache with the specified name does not exist. To resolve this error, make sure you have created the cache before attempting to use it".into(),
error_code: MomentoErrorCode::CacheNotFoundError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
Some(err) => match err.to_str() {
Ok(err_str) => {
match err_str {
"store_not_found" => MomentoError {
message: "A store with the specified name does not exist. To resolve this error, make sure you have created the store before attempting to use it".into(),
error_code: MomentoErrorCode::StoreNotFoundError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
"item_not_found" => MomentoError {
message: "An item with the specified key does not exist. To resolve this error, make sure you have created the item before attempting to use it".into(),
error_code: MomentoErrorCode::ItemNotFoundError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
_ => MomentoError {
message: "A cache with the specified name does not exist. To resolve this error, make sure you have created the cache before attempting to use it".into(),
error_code: MomentoErrorCode::CacheNotFoundError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
}
}
}
Err(e) => MomentoError {
message: "Unknown error has occurred, unable to convert the error metadata into a string".into(),
error_code: MomentoErrorCode::UnknownError,
inner_error: Some(e.into()),
details: Some(status.into())
}
}
}
},
tonic::Code::AlreadyExists => MomentoError {
message: "A cache with the specified name already exists. To resolve this error, either delete the existing cache and make a new one, or use a different name".into(),
error_code: MomentoErrorCode::AlreadyExistsError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::Unknown => {
match status
.source()
.and_then(|e| e.downcast_ref::<hyper::Error>())
.and_then(|hyper_error| hyper_error.source())
.and_then(|hyper_source| hyper_source.downcast_ref::<h2::Error>())
{
Some(h2_detailed_error) => {
if Some(h2::Reason::NO_ERROR) == h2_detailed_error.reason() {
if h2_detailed_error.is_remote() {
MomentoError {
message: "An unexpected error occurred while trying to fulfill the request, the request was interrupted by the server without an error; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::InternalServerError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
}
} else {
MomentoError {
message: "Unknown error has occurred, the request was terminated locally without an error".into(),
error_code: MomentoErrorCode::UnknownError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
}
}
} else {
MomentoError {
message: "An unexpected error occurred while trying to fulfill the request, an internal http2 error terminated the request; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::InternalServerError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
}
}
}
None => MomentoError {
message: "An unexpected error occurred while trying to fulfill the request, an unknown error terminated the request; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::InternalServerError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
}
}
}
tonic::Code::Aborted => MomentoError {
message: "An unexpected error occurred while trying to fulfill the request, request was aborted; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::InternalServerError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::Internal => MomentoError {
message: "An unexpected internal error occurred while trying to fulfill the request; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::InternalServerError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::Unavailable => MomentoError {
message: "The server was unavailable to handle the request; consider retrying. If the error persists, please contact Momento.".into(),
error_code: MomentoErrorCode::ServerUnavailable,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
tonic::Code::DataLoss => MomentoError {
message: "An unexpected data loss error occurred while trying to fulfill the request; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::InternalServerError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
_ => MomentoError {
message: "The service returned an unknown response; please contact us at support@momentohq.com".into(),
error_code: MomentoErrorCode::UnknownServiceError,
inner_error: Some(status.clone().into()),
details: Some(status.into())
},
}
}
enum LimitExceededMessageWrapper {
TopicSubscriptions,
OperationsRate,
ThroughputRate,
RequestSize,
ItemSize,
ElementSize,
Unknown,
}
impl LimitExceededMessageWrapper {
pub fn value(&self) -> &str {
match self {
LimitExceededMessageWrapper::TopicSubscriptions => {
"Topic subscriptions limit exceeded for this account"
}
LimitExceededMessageWrapper::OperationsRate => {
"Request rate limit exceeded for this account"
}
LimitExceededMessageWrapper::ThroughputRate => {
"Bandwidth limit exceeded for this account"
}
LimitExceededMessageWrapper::RequestSize => {
"Request size limit exceeded for this account"
}
LimitExceededMessageWrapper::ItemSize => "Item size limit exceeded for this account",
LimitExceededMessageWrapper::ElementSize => {
"Element size limit exceeded for this account"
}
LimitExceededMessageWrapper::Unknown => "Limit exceeded for this account",
}
}
}
fn determine_limit_exceeded_message_wrapper(metadata: &MetadataMap, message: &str) -> String {
let wrapper;
if let Some(err_cause) = metadata.get("err") {
if let Ok(err_str) = err_cause.to_str() {
wrapper = match err_str {
"topic_subscriptions_limit_exceeded" => {
LimitExceededMessageWrapper::TopicSubscriptions
}
"operations_rate_limit_exceeded" => LimitExceededMessageWrapper::OperationsRate,
"throughput_rate_limit_exceeded" => LimitExceededMessageWrapper::ThroughputRate,
"request_size_limit_exceeded" => LimitExceededMessageWrapper::RequestSize,
"item_size_limit_exceeded" => LimitExceededMessageWrapper::ItemSize,
"element_size_limit_exceeded" => LimitExceededMessageWrapper::ElementSize,
_ => LimitExceededMessageWrapper::Unknown,
};
return wrapper.value().to_string();
}
}
let lower_cased_message = message.to_lowercase();
wrapper = if lower_cased_message.contains("subscribers") {
LimitExceededMessageWrapper::TopicSubscriptions
} else if lower_cased_message.contains("operations") {
LimitExceededMessageWrapper::OperationsRate
} else if lower_cased_message.contains("throughput") {
LimitExceededMessageWrapper::ThroughputRate
} else if lower_cased_message.contains("request limit") {
LimitExceededMessageWrapper::RequestSize
} else if lower_cased_message.contains("item size") {
LimitExceededMessageWrapper::ItemSize
} else if lower_cased_message.contains("element size") {
LimitExceededMessageWrapper::ElementSize
} else {
LimitExceededMessageWrapper::Unknown
};
wrapper.value().to_string()
}