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)
64 | AppError::Forbidden(m)
65 | AppError::NotFound(m)
66 | AppError::Internal(m) => m.to_string(),
67 AppError::BadRequest(m) => m.to_string(),
69 AppError::DbConn(_) => "服务暂时不可用".to_string(),
70 AppError::Query(_) | AppError::Transaction(_) => "操作失败".to_string(),
71 };
72 ServerFnError::new(msg)
73 }
74}
75
76#[cfg(all(test, feature = "server"))]
77mod tests {
78 use super::*;
79
80 #[test]
81 fn unauthorized_message_passthrough() {
82 let err: ServerFnError = AppError::Unauthorized("未登录").into();
83 let msg = err.to_string();
84 assert!(msg.contains("未登录"), "expected '未登录' in: {msg}");
85 }
86
87 #[test]
88 fn db_conn_hides_internal_details() {
89 let src = std::io::Error::other("connection refused on port 5432");
92 let err: ServerFnError = AppError::db_conn(src).into();
93 let msg = err.to_string();
94 assert!(
95 !msg.contains("5432"),
96 "should not leak internal details: {msg}"
97 );
98 assert!(
99 msg.contains("服务暂时不可用"),
100 "expected generic message: {msg}"
101 );
102 }
103
104 #[test]
105 fn query_hides_sql_details() {
106 let src = std::io::Error::other("syntax error at SELECT * FROM");
107 let err: ServerFnError = AppError::query(src).into();
108 let msg = err.to_string();
109 assert!(!msg.contains("SELECT"), "should not leak SQL: {msg}");
110 }
111
112 #[test]
113 fn forbidden_message_passthrough() {
114 let err: ServerFnError = AppError::Forbidden("权限不足").into();
115 let msg = err.to_string();
116 assert!(msg.contains("权限不足"), "expected '权限不足': {msg}");
117 }
118
119 #[test]
120 fn not_found_message_passthrough() {
121 let err: ServerFnError = AppError::NotFound("文章不存在").into();
122 let msg = err.to_string();
123 assert!(msg.contains("文章不存在"), "expected passthrough: {msg}");
124 }
125
126 #[test]
127 fn internal_message_passthrough() {
128 let err: ServerFnError = AppError::Internal("内部错误").into();
130 let msg = err.to_string();
131 assert!(msg.contains("内部错误"), "expected passthrough: {msg}");
132 }
133
134 #[test]
135 fn transaction_hides_sql_details() {
136 let src = std::io::Error::other("deadlock detected on UPDATE posts");
138 let err: ServerFnError = AppError::tx(src).into();
139 let msg = err.to_string();
140 assert!(!msg.contains("UPDATE"), "should not leak SQL: {msg}");
141 assert!(
142 !msg.contains("deadlock"),
143 "should not leak error detail: {msg}"
144 );
145 assert!(msg.contains("操作失败"), "expected generic message: {msg}");
146 }
147
148 #[test]
149 fn db_conn_query_transaction_all_return_generic_message() {
150 let db_conn: ServerFnError = AppError::DbConn("x".into()).into();
152 let query: ServerFnError = AppError::Query("x".into()).into();
153 let tx: ServerFnError = AppError::Transaction("x".into()).into();
154
155 assert!(db_conn.to_string().contains("服务暂时不可用"));
156 assert!(query.to_string().contains("操作失败"));
157 assert!(tx.to_string().contains("操作失败"));
158 }
159}