use thiserror::Error;
use tonic::Status;
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ServerError {
#[error("configuration error: {0}")]
Config(String),
#[error("transport error: {0}")]
Transport(#[from] tonic::transport::Error),
#[error("query error: {0}")]
Query(String),
#[error("authentication error: {0}")]
Auth(String),
#[error("blob error: {0}")]
Blob(String),
#[error("internal error: {0}")]
Internal(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("not found: {0}")]
NotFound(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("rate limit exceeded: {0}")]
RateLimited(String),
}
const INTERNAL_ERROR_MESSAGE: &str = "An internal error occurred. Please try again later.";
impl From<ServerError> for Status {
fn from(err: ServerError) -> Self {
match &err {
ServerError::Config(msg)
| ServerError::Query(msg)
| ServerError::InvalidArgument(msg) => Self::invalid_argument(msg.clone()),
ServerError::Transport(e) => {
tracing::warn!(error = %e, "Transport error");
Self::unavailable("Service temporarily unavailable")
},
ServerError::Auth(msg) => Self::unauthenticated(msg.clone()),
ServerError::Blob(msg) => {
tracing::error!(error = %msg, "Blob storage error");
Self::internal(INTERNAL_ERROR_MESSAGE)
},
ServerError::Internal(msg) => {
tracing::error!(error = %msg, "Internal server error");
Self::internal(INTERNAL_ERROR_MESSAGE)
},
ServerError::NotFound(msg) => Self::not_found(msg.clone()),
ServerError::PermissionDenied(msg) => Self::permission_denied(msg.clone()),
ServerError::Io(e) => {
tracing::error!(error = %e, "I/O error");
Self::internal(INTERNAL_ERROR_MESSAGE)
},
ServerError::RateLimited(msg) => Self::resource_exhausted(msg.clone()),
}
}
}
impl From<query_router::RouterError> for ServerError {
fn from(err: query_router::RouterError) -> Self {
Self::Query(err.to_string())
}
}
impl From<tensor_blob::BlobError> for ServerError {
fn from(err: tensor_blob::BlobError) -> Self {
Self::Blob(err.to_string())
}
}
pub type Result<T> = std::result::Result<T, ServerError>;
pub fn sanitize_internal_error<E: std::fmt::Display>(error: E) -> Status {
tracing::error!(error = %error, "Internal server error");
Status::internal(INTERNAL_ERROR_MESSAGE)
}
pub fn sanitize_error<E: std::fmt::Display>(error: E, code: tonic::Code) -> Status {
let msg = match code {
tonic::Code::Internal => {
tracing::error!(error = %error, "Internal server error");
INTERNAL_ERROR_MESSAGE.to_string()
},
tonic::Code::Unavailable => {
tracing::warn!(error = %error, "Service unavailable");
"Service temporarily unavailable".to_string()
},
_ => error.to_string(),
};
Status::new(code, msg)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_error_to_status() {
let err = ServerError::Config("invalid port".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
#[test]
fn test_query_error_to_status() {
let err = ServerError::Query("syntax error".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
#[test]
fn test_auth_error_to_status() {
let err = ServerError::Auth("invalid token".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::Unauthenticated);
}
#[test]
fn test_not_found_error_to_status() {
let err = ServerError::NotFound("artifact not found".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::NotFound);
}
#[test]
fn test_permission_denied_to_status() {
let err = ServerError::PermissionDenied("access denied".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::PermissionDenied);
}
#[test]
fn test_internal_error_to_status() {
let err = ServerError::Internal("unexpected error".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::Internal);
}
#[test]
fn test_blob_error_to_status() {
let err = ServerError::Blob("storage error".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::Internal);
}
#[test]
fn test_invalid_argument_to_status() {
let err = ServerError::InvalidArgument("bad input".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::InvalidArgument);
}
#[test]
fn test_error_display() {
let err = ServerError::Config("test config error".to_string());
assert_eq!(err.to_string(), "configuration error: test config error");
let err = ServerError::Query("test query error".to_string());
assert_eq!(err.to_string(), "query error: test query error");
}
#[test]
fn test_rate_limited_to_status() {
let err = ServerError::RateLimited("too many requests".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::ResourceExhausted);
}
#[test]
fn test_rate_limited_error_display() {
let err = ServerError::RateLimited("too many requests".to_string());
assert_eq!(err.to_string(), "rate limit exceeded: too many requests");
}
#[test]
fn test_internal_error_sanitization() {
let err = ServerError::Internal("secret database connection string".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::Internal);
assert!(!status.message().contains("secret"));
assert!(status.message().contains("internal error"));
}
#[test]
fn test_blob_error_sanitization() {
let err = ServerError::Blob("failed to write to /var/data/secrets".to_string());
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::Internal);
assert!(!status.message().contains("/var/data"));
}
#[test]
fn test_io_error_sanitization() {
let err = ServerError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"file /etc/passwd not found",
));
let status: Status = err.into();
assert_eq!(status.code(), tonic::Code::Internal);
assert!(!status.message().contains("/etc/passwd"));
}
#[test]
fn test_sanitize_internal_error() {
let status = sanitize_internal_error("secret data");
assert_eq!(status.code(), tonic::Code::Internal);
assert!(!status.message().contains("secret"));
}
#[test]
fn test_sanitize_error_internal() {
let status = sanitize_error("sensitive info", tonic::Code::Internal);
assert_eq!(status.code(), tonic::Code::Internal);
assert!(!status.message().contains("sensitive"));
}
#[test]
fn test_sanitize_error_unavailable() {
let status = sanitize_error("connection to db failed", tonic::Code::Unavailable);
assert_eq!(status.code(), tonic::Code::Unavailable);
assert!(!status.message().contains("db"));
}
#[test]
fn test_sanitize_error_other_codes() {
let status = sanitize_error("invalid input", tonic::Code::InvalidArgument);
assert_eq!(status.code(), tonic::Code::InvalidArgument);
assert_eq!(status.message(), "invalid input");
}
#[test]
fn test_router_error_to_server_error() {
let router_err = query_router::RouterError::ParseError("bad query".to_string());
let server_err: ServerError = router_err.into();
assert!(matches!(server_err, ServerError::Query(_)));
assert!(server_err.to_string().contains("bad query"));
}
#[test]
fn test_blob_error_to_server_error() {
let blob_err = tensor_blob::BlobError::NotFound("key123".to_string());
let server_err: ServerError = blob_err.into();
assert!(matches!(server_err, ServerError::Blob(_)));
assert!(server_err.to_string().contains("key123"));
}
#[test]
fn test_blob_error_to_status_conversion() {
let blob_err = tensor_blob::BlobError::NotFound("x".to_string());
let server_err: ServerError = blob_err.into();
let status: Status = server_err.into();
assert_eq!(status.code(), tonic::Code::Internal);
}
}