1use dioxus::prelude::ServerFnError;
7
8#[derive(Debug)]
10pub enum AppError {
11 Unauthorized(&'static str),
13 Forbidden(&'static str),
15 NotFound(&'static str),
17 BadRequest(String),
19 DbConn(String),
21 Query(String),
23 Transaction(String),
25 Internal(&'static str),
27}
28
29#[cfg(feature = "server")]
30impl AppError {
31 pub fn db_conn(e: impl std::error::Error) -> Self {
39 tracing::error!(
40 "DB connection failed: {}",
41 crate::db::format_with_sources(&e)
42 );
43 AppError::DbConn("connection error".to_string())
44 }
45
46 pub fn query(e: impl std::error::Error) -> Self {
48 tracing::error!("Query failed: {}", crate::db::format_with_sources(&e));
49 AppError::Query("query error".to_string())
50 }
51
52 pub fn tx(e: impl std::error::Error) -> Self {
54 tracing::error!("Transaction failed: {}", crate::db::format_with_sources(&e));
55 AppError::Transaction("transaction error".to_string())
56 }
57}
58
59impl From<AppError> for ServerFnError {
61 fn from(err: AppError) -> ServerFnError {
62 let msg = match &err {
63 AppError::Unauthorized(m) => m.to_string(),
64 AppError::Forbidden(m) => m.to_string(),
65 AppError::NotFound(m) => m.to_string(),
66 AppError::BadRequest(m) => m.to_string(),
68 AppError::DbConn(_) => "服务暂时不可用".to_string(),
69 AppError::Query(_) => "操作失败".to_string(),
70 AppError::Transaction(_) => "操作失败".to_string(),
71 AppError::Internal(m) => m.to_string(),
72 };
73 ServerFnError::new(msg)
74 }
75}
76
77#[cfg(all(test, feature = "server"))]
78mod tests {
79 use super::*;
80
81 #[test]
82 fn unauthorized_message_passthrough() {
83 let err: ServerFnError = AppError::Unauthorized("未登录").into();
84 let msg = err.to_string();
85 assert!(msg.contains("未登录"), "expected '未登录' in: {msg}");
86 }
87
88 #[test]
89 fn db_conn_hides_internal_details() {
90 let src = std::io::Error::other("connection refused on port 5432");
93 let err: ServerFnError = AppError::db_conn(src).into();
94 let msg = err.to_string();
95 assert!(
96 !msg.contains("5432"),
97 "should not leak internal details: {msg}"
98 );
99 assert!(
100 msg.contains("服务暂时不可用"),
101 "expected generic message: {msg}"
102 );
103 }
104
105 #[test]
106 fn query_hides_sql_details() {
107 let src = std::io::Error::other("syntax error at SELECT * FROM");
108 let err: ServerFnError = AppError::query(src).into();
109 let msg = err.to_string();
110 assert!(!msg.contains("SELECT"), "should not leak SQL: {msg}");
111 }
112
113 #[test]
114 fn forbidden_message_passthrough() {
115 let err: ServerFnError = AppError::Forbidden("权限不足").into();
116 let msg = err.to_string();
117 assert!(msg.contains("权限不足"), "expected '权限不足': {msg}");
118 }
119
120 #[test]
121 fn not_found_message_passthrough() {
122 let err: ServerFnError = AppError::NotFound("文章不存在").into();
123 let msg = err.to_string();
124 assert!(msg.contains("文章不存在"), "expected passthrough: {msg}");
125 }
126
127 #[test]
128 fn internal_message_passthrough() {
129 let err: ServerFnError = AppError::Internal("内部错误").into();
131 let msg = err.to_string();
132 assert!(msg.contains("内部错误"), "expected passthrough: {msg}");
133 }
134
135 #[test]
136 fn transaction_hides_sql_details() {
137 let src = std::io::Error::other("deadlock detected on UPDATE posts");
139 let err: ServerFnError = AppError::tx(src).into();
140 let msg = err.to_string();
141 assert!(!msg.contains("UPDATE"), "should not leak SQL: {msg}");
142 assert!(
143 !msg.contains("deadlock"),
144 "should not leak error detail: {msg}"
145 );
146 assert!(msg.contains("操作失败"), "expected generic message: {msg}");
147 }
148
149 #[test]
150 fn db_conn_query_transaction_all_return_generic_message() {
151 let db_conn: ServerFnError = AppError::DbConn("x".into()).into();
153 let query: ServerFnError = AppError::Query("x".into()).into();
154 let tx: ServerFnError = AppError::Transaction("x".into()).into();
155
156 assert!(db_conn.to_string().contains("服务暂时不可用"));
157 assert!(query.to_string().contains("操作失败"));
158 assert!(tx.to_string().contains("操作失败"));
159 }
160}