#[cfg(feature = "http")]
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
#[cfg(feature = "http")]
use regex::Regex;
#[cfg(feature = "http")]
use serde_json::json;
#[cfg(feature = "http")]
const MAX_ERROR_MESSAGE_LENGTH: usize = 200;
#[cfg(feature = "http")]
static SANITIZE_PATTERNS: std::sync::OnceLock<Vec<(Regex, &'static str)>> =
std::sync::OnceLock::new();
#[cfg(feature = "http")]
fn get_sanitize_patterns() -> &'static Vec<(Regex, &'static str)> {
SANITIZE_PATTERNS.get_or_init(|| {
vec![
(
Regex::new(r#"token \d+"#).expect("sanitize pattern: valid token regex"),
"token [ID]",
),
(
Regex::new(r#"at position \d+"#).expect("sanitize pattern: valid position regex"),
"at position [REDACTED]",
),
(
Regex::new(r#"\.unwrap\(\)"#).expect("sanitize pattern: valid unwrap regex"),
"[INTERNAL_ERROR]",
),
(
Regex::new(r#"expect\([^)]+\)"#).expect("sanitize pattern: valid expect regex"),
"[INTERNAL_ERROR]",
),
]
})
}
#[cfg(feature = "http")]
fn sanitize_error_message(msg: &str) -> String {
let mut sanitized = msg.to_string();
for (pattern, replacement) in get_sanitize_patterns() {
sanitized = pattern.replace_all(&sanitized, *replacement).to_string();
}
if sanitized.len() > MAX_ERROR_MESSAGE_LENGTH {
let mut truncate_at = MAX_ERROR_MESSAGE_LENGTH;
while truncate_at > 0 && !sanitized.is_char_boundary(truncate_at) {
truncate_at -= 1;
}
sanitized.truncate(truncate_at);
sanitized.push_str("...");
}
sanitized
}
#[derive(Debug, Clone)]
pub enum VecboostError {
ConfigError(String),
ModelLoadError(String),
ModelFileCorrupted(String),
ModelIntegrityError(String),
TokenizationError(String),
InferenceError(String),
OutOfMemory(String),
InvalidInput(String),
NotFound(String),
ModelNotLoaded(String),
AuthenticationError(String),
SecurityError(String),
IoError(String),
ValidationError(String),
RateLimitExceeded(String),
DatabaseError(String),
InternalError(String),
}
impl std::fmt::Display for VecboostError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let detail = self.error_detail();
let args = crate::i18n::tr_args(&[("detail", detail)]);
let msg = crate::i18n::tr_with_args(self.error_code(), args);
write!(f, "{}", msg)
}
}
impl std::error::Error for VecboostError {}
impl VecboostError {
pub fn config_error(message: String) -> Self {
VecboostError::ConfigError(message)
}
pub fn model_load_error(message: String) -> Self {
VecboostError::ModelLoadError(message)
}
pub fn model_file_corrupted(message: String) -> Self {
VecboostError::ModelFileCorrupted(message)
}
pub fn model_integrity_error(message: String) -> Self {
VecboostError::ModelIntegrityError(message)
}
pub fn tokenization_error(message: String) -> Self {
VecboostError::TokenizationError(message)
}
pub fn inference_error(message: String) -> Self {
VecboostError::InferenceError(message)
}
pub fn invalid_input(message: String) -> Self {
VecboostError::InvalidInput(message)
}
pub fn not_found(message: String) -> Self {
VecboostError::NotFound(message)
}
pub fn model_not_loaded(message: String) -> Self {
VecboostError::ModelNotLoaded(message)
}
pub fn authentication_error(message: String) -> Self {
VecboostError::AuthenticationError(message)
}
pub fn security_error(message: String) -> Self {
VecboostError::SecurityError(message)
}
pub fn io_error(message: String) -> Self {
VecboostError::IoError(message)
}
pub fn validation_error(message: String) -> Self {
VecboostError::ValidationError(message)
}
pub fn database_error(message: String) -> Self {
VecboostError::DatabaseError(message)
}
pub fn rate_limit_exceeded(message: String) -> Self {
VecboostError::RateLimitExceeded(message)
}
pub fn out_of_memory(message: String) -> Self {
VecboostError::OutOfMemory(message)
}
pub fn internal_error(message: String) -> Self {
VecboostError::InternalError(message)
}
pub fn error_code(&self) -> &'static str {
match self {
VecboostError::ConfigError(_) => "error-config",
VecboostError::ModelLoadError(_) => "error-model-load",
VecboostError::ModelFileCorrupted(_) => "error-model-corrupted",
VecboostError::ModelIntegrityError(_) => "error-model-integrity",
VecboostError::TokenizationError(_) => "error-tokenization",
VecboostError::InferenceError(_) => "error-inference",
VecboostError::OutOfMemory(_) => "error-oom",
VecboostError::InvalidInput(_) => "error-invalid-input",
VecboostError::NotFound(_) => "error-not-found",
VecboostError::ModelNotLoaded(_) => "error-model-not-loaded",
VecboostError::AuthenticationError(_) => "error-authentication",
VecboostError::SecurityError(_) => "error-security",
VecboostError::IoError(_) => "error-io",
VecboostError::ValidationError(_) => "error-validation",
VecboostError::RateLimitExceeded(_) => "error-rate-limit",
VecboostError::DatabaseError(_) => "error-database",
VecboostError::InternalError(_) => "error-internal",
}
}
pub fn error_detail(&self) -> &str {
match self {
VecboostError::ConfigError(s)
| VecboostError::ModelLoadError(s)
| VecboostError::ModelFileCorrupted(s)
| VecboostError::ModelIntegrityError(s)
| VecboostError::TokenizationError(s)
| VecboostError::InferenceError(s)
| VecboostError::OutOfMemory(s)
| VecboostError::InvalidInput(s)
| VecboostError::NotFound(s)
| VecboostError::ModelNotLoaded(s)
| VecboostError::AuthenticationError(s)
| VecboostError::SecurityError(s)
| VecboostError::IoError(s)
| VecboostError::ValidationError(s)
| VecboostError::RateLimitExceeded(s)
| VecboostError::DatabaseError(s)
| VecboostError::InternalError(s) => s.as_str(),
}
}
}
#[cfg(feature = "http")]
impl IntoResponse for VecboostError {
fn into_response(self) -> Response {
let status = match &self {
VecboostError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR,
VecboostError::ModelLoadError(_) => StatusCode::FAILED_DEPENDENCY,
VecboostError::ModelFileCorrupted(_) => StatusCode::FAILED_DEPENDENCY,
VecboostError::ModelIntegrityError(_) => StatusCode::FAILED_DEPENDENCY,
VecboostError::TokenizationError(_) => StatusCode::UNPROCESSABLE_ENTITY,
VecboostError::InferenceError(_) => StatusCode::SERVICE_UNAVAILABLE,
VecboostError::OutOfMemory(_) => StatusCode::INSUFFICIENT_STORAGE,
VecboostError::InvalidInput(_) => StatusCode::BAD_REQUEST,
VecboostError::NotFound(_) => StatusCode::NOT_FOUND,
VecboostError::ModelNotLoaded(_) => StatusCode::FAILED_DEPENDENCY,
VecboostError::AuthenticationError(_) => StatusCode::UNAUTHORIZED,
VecboostError::SecurityError(_) => StatusCode::INTERNAL_SERVER_ERROR,
VecboostError::IoError(_) => StatusCode::INTERNAL_SERVER_ERROR,
VecboostError::ValidationError(_) => StatusCode::BAD_REQUEST,
VecboostError::RateLimitExceeded(_) => StatusCode::TOO_MANY_REQUESTS,
VecboostError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
VecboostError::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
let error_code = self.error_code();
let args = crate::i18n::tr_args(&[("detail", self.error_detail())]);
let translated_message = crate::i18n::tr_with_args(error_code, args);
let sanitized_message = sanitize_error_message(&translated_message);
let body = Json(json!({
"error": sanitized_message,
"code": status.as_u16(),
"error_code": error_code
}));
(status, body).into_response()
}
}
impl From<std::io::Error> for VecboostError {
fn from(e: std::io::Error) -> Self {
VecboostError::IoError(e.to_string())
}
}
#[cfg(feature = "db")]
impl From<dbnexus::sea_orm::DbErr> for VecboostError {
fn from(e: dbnexus::sea_orm::DbErr) -> Self {
VecboostError::DatabaseError(e.to_string())
}
}
#[cfg(feature = "auth")]
impl From<garrison::error::GarrisonError> for VecboostError {
fn from(e: garrison::error::GarrisonError) -> Self {
use garrison::error::GarrisonError as GE;
match e {
GE::NotLogin(msg)
| GE::NotPermission(msg)
| GE::NotRole(msg)
| GE::InvalidToken(msg)
| GE::TokenRevoked(msg)
| GE::ExpiredToken(msg)
| GE::Session(msg) => VecboostError::AuthenticationError(msg),
GE::Config(msg) => VecboostError::ConfigError(msg),
GE::Dao(msg) | GE::Internal(msg) | GE::Annotation(msg) | GE::Context(msg) => {
VecboostError::InternalError(msg)
}
GE::Exception(ex) => VecboostError::AuthenticationError(ex.to_string()),
_ => VecboostError::InternalError(e.to_string()),
}
}
}
impl From<candle_core::Error> for VecboostError {
fn from(e: candle_core::Error) -> Self {
VecboostError::InferenceError(e.to_string())
}
}
impl From<tokio::task::JoinError> for VecboostError {
fn from(e: tokio::task::JoinError) -> Self {
VecboostError::InferenceError(e.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ensure_init() {
crate::i18n::init();
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_unix_path_preserved() {
let msg = "Failed to load /home/user/model/file.safetensors";
let sanitized = sanitize_error_message(msg);
assert!(sanitized.contains("/home/user/model/file.safetensors"));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_windows_path_preserved() {
let msg = r#"Failed to load C:\Users\admin\config.json"#;
let sanitized = sanitize_error_message(msg);
assert!(sanitized.contains(r#"C:\Users\admin\config.json"#));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_token_id() {
let msg = "Invalid token 12345";
let sanitized = sanitize_error_message(msg);
assert!(sanitized.contains("token [ID]"));
assert!(!sanitized.contains("12345"));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_position() {
let msg = "JSON parse error at position 42";
let sanitized = sanitize_error_message(msg);
assert!(sanitized.contains("at position [REDACTED]"));
assert!(!sanitized.contains("42"));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_unwrap() {
let msg = "Error in value.unwrap() at line 42";
let sanitized = sanitize_error_message(msg);
assert!(sanitized.contains("[INTERNAL_ERROR]"));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_expect() {
let msg = "called Result::expect(hello) on an Err value";
let sanitized = sanitize_error_message(msg);
assert!(sanitized.contains("[INTERNAL_ERROR]"));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_truncation() {
let long_msg = "x".repeat(300);
let sanitized = sanitize_error_message(&long_msg);
assert!(sanitized.len() <= MAX_ERROR_MESSAGE_LENGTH + 3);
assert!(sanitized.ends_with("..."));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_short_message() {
let msg = "Simple error";
let sanitized = sanitize_error_message(msg);
assert_eq!(sanitized, "Simple error");
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_empty() {
let sanitized = sanitize_error_message("");
assert_eq!(sanitized, "");
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_config_error() {
let err = VecboostError::ConfigError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_invalid_input() {
let err = VecboostError::InvalidInput("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_not_found() {
let err = VecboostError::NotFound("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_authentication_error() {
let err = VecboostError::AuthenticationError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_rate_limit_exceeded() {
let err = VecboostError::RateLimitExceeded("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_model_load_error() {
let err = VecboostError::ModelLoadError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::FAILED_DEPENDENCY);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_inference_error() {
let err = VecboostError::InferenceError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_validation_error() {
let err = VecboostError::ValidationError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_tokenization_error() {
let err = VecboostError::TokenizationError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_out_of_memory() {
let err = VecboostError::OutOfMemory("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::INSUFFICIENT_STORAGE);
}
#[test]
fn test_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let vecboost_err: VecboostError = io_err.into();
match vecboost_err {
VecboostError::IoError(msg) => assert!(msg.contains("file not found")),
_ => panic!("Expected IoError"),
}
}
#[test]
fn test_error_display() {
ensure_init();
let err = VecboostError::ConfigError("test message".to_string());
let expected = crate::i18n::tr_with_args(
"error-config",
crate::i18n::tr_args(&[("detail", "test message")]),
);
assert_eq!(format!("{}", err), expected);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_model_file_corrupted() {
let err = VecboostError::ModelFileCorrupted("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::FAILED_DEPENDENCY);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_model_integrity_error() {
let err = VecboostError::ModelIntegrityError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::FAILED_DEPENDENCY);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_model_not_loaded() {
let err = VecboostError::ModelNotLoaded("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::FAILED_DEPENDENCY);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_security_error() {
let err = VecboostError::SecurityError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_io_error_variant() {
let err = VecboostError::IoError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_database_error() {
let err = VecboostError::DatabaseError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[cfg(feature = "http")]
#[test]
fn test_into_response_internal_error() {
let err = VecboostError::InternalError("test".to_string());
let response = err.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn test_from_candle_error() {
let candle_err = candle_core::Error::Msg("candle failure".to_string());
let vecboost_err: VecboostError = candle_err.into();
match vecboost_err {
VecboostError::InferenceError(msg) => {
assert!(msg.contains("candle failure"), "got: {}", msg)
}
_ => panic!("Expected InferenceError"),
}
}
#[tokio::test]
async fn test_from_join_error() {
let handle = tokio::spawn(async {
panic!("test panic");
});
let join_err = handle.await.unwrap_err();
let vecboost_err: VecboostError = join_err.into();
match vecboost_err {
VecboostError::InferenceError(_) => {}
other => panic!("Expected InferenceError, got {:?}", other),
}
}
#[cfg(feature = "db")]
#[test]
fn test_from_db_error() {
let db_err = dbnexus::sea_orm::DbErr::RecordNotFound("not found".to_string());
let vecboost_err: VecboostError = db_err.into();
match vecboost_err {
VecboostError::DatabaseError(msg) => {
assert!(msg.contains("not found"), "got: {}", msg)
}
_ => panic!("Expected DatabaseError"),
}
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_multiple_patterns() {
let msg = "Error at position 42 in value.unwrap() for token 12345 at /home/user/model/file.safetensors";
let sanitized = sanitize_error_message(msg);
assert!(sanitized.contains("at position [REDACTED]"));
assert!(sanitized.contains("[INTERNAL_ERROR]"));
assert!(sanitized.contains("token [ID]"));
assert!(sanitized.contains("/home/user/model/file.safetensors"));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_exact_boundary() {
let msg = "x".repeat(MAX_ERROR_MESSAGE_LENGTH);
let sanitized = sanitize_error_message(&msg);
assert_eq!(sanitized.len(), MAX_ERROR_MESSAGE_LENGTH);
assert!(!sanitized.ends_with("..."));
}
#[cfg(feature = "http")]
#[test]
fn test_sanitize_error_message_one_char_over_boundary() {
let msg = "x".repeat(MAX_ERROR_MESSAGE_LENGTH + 1);
let sanitized = sanitize_error_message(&msg);
assert!(sanitized.ends_with("..."));
assert!(sanitized.len() <= MAX_ERROR_MESSAGE_LENGTH + 3);
}
#[test]
fn test_all_error_variants_display() {
ensure_init();
let cases: Vec<(VecboostError, &str, &str)> = vec![
(VecboostError::OutOfMemory("oom".into()), "error-oom", "oom"),
(
VecboostError::DatabaseError("db".into()),
"error-database",
"db",
),
(
VecboostError::InternalError("int".into()),
"error-internal",
"int",
),
(
VecboostError::RateLimitExceeded("rl".into()),
"error-rate-limit",
"rl",
),
];
for (err, code, detail) in cases {
let expected =
crate::i18n::tr_with_args(code, crate::i18n::tr_args(&[("detail", detail)]));
assert_eq!(format!("{}", err), expected, "mismatch for {}", code);
}
}
#[test]
fn test_error_constructors() {
let _ = VecboostError::config_error("cfg".into());
let _ = VecboostError::model_load_error("ml".into());
let _ = VecboostError::model_file_corrupted("mfc".into());
let _ = VecboostError::model_integrity_error("mi".into());
let _ = VecboostError::tokenization_error("tok".into());
let _ = VecboostError::inference_error("inf".into());
let _ = VecboostError::invalid_input("ii".into());
let _ = VecboostError::not_found("nf".into());
let _ = VecboostError::model_not_loaded("mnl".into());
let _ = VecboostError::authentication_error("auth".into());
let _ = VecboostError::security_error("sec".into());
let _ = VecboostError::io_error("io".into());
let _ = VecboostError::validation_error("val".into());
let _ = VecboostError::database_error("db".into());
let _ = VecboostError::rate_limit_exceeded("rl".into());
let _ = VecboostError::out_of_memory("oom".into());
let _ = VecboostError::internal_error("int".into());
}
#[test]
fn test_error_code_all_variants() {
let variants: Vec<VecboostError> = vec![
VecboostError::ConfigError("x".into()),
VecboostError::ModelLoadError("x".into()),
VecboostError::ModelFileCorrupted("x".into()),
VecboostError::ModelIntegrityError("x".into()),
VecboostError::TokenizationError("x".into()),
VecboostError::InferenceError("x".into()),
VecboostError::OutOfMemory("x".into()),
VecboostError::InvalidInput("x".into()),
VecboostError::NotFound("x".into()),
VecboostError::ModelNotLoaded("x".into()),
VecboostError::AuthenticationError("x".into()),
VecboostError::SecurityError("x".into()),
VecboostError::IoError("x".into()),
VecboostError::ValidationError("x".into()),
VecboostError::RateLimitExceeded("x".into()),
VecboostError::DatabaseError("x".into()),
VecboostError::InternalError("x".into()),
];
for v in &variants {
assert!(!v.error_code().is_empty());
assert!(!v.error_detail().is_empty());
}
}
#[test]
fn test_error_is_error_trait() {
let err = VecboostError::ConfigError("test".into());
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_error_display_all_remaining_variants() {
ensure_init();
let remaining: Vec<(VecboostError, &str)> = vec![
(
VecboostError::ModelLoadError("ml".into()),
"error-model-load",
),
(
VecboostError::ModelFileCorrupted("mfc".into()),
"error-model-corrupted",
),
(
VecboostError::ModelIntegrityError("mi".into()),
"error-model-integrity",
),
(
VecboostError::TokenizationError("tok".into()),
"error-tokenization",
),
(
VecboostError::InferenceError("inf".into()),
"error-inference",
),
(
VecboostError::InvalidInput("ii".into()),
"error-invalid-input",
),
(VecboostError::NotFound("nf".into()), "error-not-found"),
(
VecboostError::ModelNotLoaded("mnl".into()),
"error-model-not-loaded",
),
(
VecboostError::AuthenticationError("auth".into()),
"error-authentication",
),
(VecboostError::SecurityError("sec".into()), "error-security"),
(VecboostError::IoError("io".into()), "error-io"),
(
VecboostError::ValidationError("val".into()),
"error-validation",
),
];
for (err, code) in remaining {
let expected = crate::i18n::tr_with_args(
code,
crate::i18n::tr_args(&[("detail", err.error_detail())]),
);
assert_eq!(format!("{}", err), expected, "mismatch for {}", code);
}
}
#[cfg(feature = "auth")]
#[test]
fn test_from_garrison_error_not_login() {
let ge = garrison::error::GarrisonError::NotLogin("not logged in".to_string());
let ve: VecboostError = ge.into();
assert!(matches!(ve, VecboostError::AuthenticationError(_)));
}
#[cfg(feature = "auth")]
#[test]
fn test_from_garrison_error_config() {
let ge = garrison::error::GarrisonError::Config("bad config".to_string());
let ve: VecboostError = ge.into();
assert!(matches!(ve, VecboostError::ConfigError(_)));
}
#[cfg(feature = "auth")]
#[test]
fn test_from_garrison_error_internal() {
let ge = garrison::error::GarrisonError::Internal("internal".to_string());
let ve: VecboostError = ge.into();
assert!(matches!(ve, VecboostError::InternalError(_)));
}
}