Files
zclaw_openfang/crates/zclaw-saas/src/error.rs
iven bf728c34f3
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
fix: saasStore require() bug + health check pool formula + DEV error details
- saasStore.ts: replace require('./chat/conversationStore') with await import()
  to fix ReferenceError in Vite ESM environment (P1)
- main.rs: fix health check pool usage formula from max_connections - num_idle
  to pool.size() - num_idle, preventing false "degraded" status (P1)
- error.rs: show detailed error messages in ZCLAW_SAAS_DEV=true mode
- Update bug tracker with BUG-003 through BUG-007
2026-04-09 22:23:05 +08:00

152 lines
4.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! SaaS 错误类型
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use serde_json::json;
/// SaaS 服务错误类型
#[derive(Debug, thiserror::Error)]
pub enum SaasError {
#[error("未找到: {0}")]
NotFound(String),
#[error("权限不足: {0}")]
Forbidden(String),
#[error("未认证")]
Unauthorized,
#[error("无效输入: {0}")]
InvalidInput(String),
#[error("认证失败: {0}")]
AuthError(String),
#[error("用户已存在: {0}")]
AlreadyExists(String),
#[error("序列化错误: {0}")]
Serialization(#[from] serde_json::Error),
#[error("IO 错误: {0}")]
Io(#[from] std::io::Error),
#[error("数据库错误: {0}")]
Database(#[from] sqlx::Error),
#[error("配置错误: {0}")]
Config(#[from] toml::de::Error),
#[error("JWT 错误: {0}")]
Jwt(#[from] jsonwebtoken::errors::Error),
#[error("密码哈希错误: {0}")]
PasswordHash(String),
#[error("TOTP 错误: {0}")]
Totp(String),
#[error("加密错误: {0}")]
Encryption(String),
#[error("中转错误: {0}")]
Relay(String),
#[error("通用错误: {0}")]
General(#[from] anyhow::Error),
#[error("速率限制: {0}")]
RateLimited(String),
#[error("内部错误: {0}")]
Internal(String),
}
impl SaasError {
/// 将 sqlx::Error 中的 unique violation 映射为 AlreadyExists (409)
/// 其他 DB 错误保持为 Database (500)。
pub fn from_sqlx_unique(e: sqlx::Error, context: &str) -> Self {
if let sqlx::Error::Database(ref db_err) = e {
// PostgreSQL unique_violation = "23505"
if db_err.code().map(|c| c == "23505").unwrap_or(false) {
return Self::AlreadyExists(format!("{}已存在", context));
}
}
Self::Database(e)
}
/// 获取 HTTP 状态码
pub fn status_code(&self) -> StatusCode {
match self {
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::Forbidden(_) => StatusCode::FORBIDDEN,
Self::Unauthorized => StatusCode::UNAUTHORIZED,
Self::InvalidInput(_) => StatusCode::BAD_REQUEST,
Self::AlreadyExists(_) => StatusCode::CONFLICT,
Self::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS,
Self::Database(_) | Self::Internal(_) | Self::Io(_) | Self::Serialization(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::AuthError(_) => StatusCode::UNAUTHORIZED,
Self::Jwt(_) | Self::PasswordHash(_) | Self::Encryption(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
Self::Totp(_) => StatusCode::BAD_REQUEST,
Self::Config(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Relay(_) => StatusCode::BAD_GATEWAY,
Self::General(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
/// 获取错误代码
pub fn error_code(&self) -> &str {
match self {
Self::NotFound(_) => "NOT_FOUND",
Self::Forbidden(_) => "FORBIDDEN",
Self::Unauthorized => "UNAUTHORIZED",
Self::InvalidInput(_) => "INVALID_INPUT",
Self::AlreadyExists(_) => "ALREADY_EXISTS",
Self::RateLimited(_) => "RATE_LIMITED",
Self::Database(_) => "DATABASE_ERROR",
Self::Io(_) => "IO_ERROR",
Self::Serialization(_) => "SERIALIZATION_ERROR",
Self::Internal(_) => "INTERNAL_ERROR",
Self::AuthError(_) => "AUTH_ERROR",
Self::Jwt(_) => "JWT_ERROR",
Self::PasswordHash(_) => "PASSWORD_HASH_ERROR",
Self::Totp(_) => "TOTP_ERROR",
Self::Encryption(_) => "ENCRYPTION_ERROR",
Self::Config(_) => "CONFIG_ERROR",
Self::Relay(_) => "RELAY_ERROR",
Self::General(_) => "GENERAL_ERROR",
}
}
}
/// 实现 Axum 响应
impl IntoResponse for SaasError {
fn into_response(self) -> Response {
let status = self.status_code();
let (error_code, message) = match &self {
// 500 错误不泄露内部细节给客户端 (开发模式除外)
Self::Database(_) | Self::Internal(_) | Self::Io(_)
| Self::Jwt(_) | Self::Config(_) => {
tracing::error!("内部错误 [{}]: {}", self.error_code(), self);
if std::env::var("ZCLAW_SAAS_DEV").as_deref() == Ok("true") {
(self.error_code().to_string(), format!("[DEV] {}", self))
} else {
(self.error_code().to_string(), "服务内部错误".to_string())
}
}
_ => (self.error_code().to_string(), self.to_string()),
};
let body = json!({
"error": error_code,
"message": message,
});
(status, axum::Json(body)).into_response()
}
}
/// Result 类型别名
pub type SaasResult<T> = std::result::Result<T, SaasError>;