1use std::fmt;
8
9use leptos::prelude::ServerFnError;
10use leptos::server_fn::{
11 codec::JsonEncoding,
12 error::{FromServerFnError, ServerFnErrorErr},
13};
14use serde::de::{self, Deserializer, IgnoredAny, MapAccess, Visitor};
15use serde::{Deserialize, Serialize};
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
25#[serde(tag = "code", content = "message", rename_all = "snake_case")]
26pub enum ApiError {
27 Unauthorized(String),
28 Forbidden(String),
29 NotFound(String),
30 BadRequest(String),
31 Unavailable,
32 Internal(String),
33 Framework(ServerFnError),
34}
35
36impl std::fmt::Display for ApiError {
37 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38 match self {
39 Self::Unauthorized(message)
40 | Self::Forbidden(message)
41 | Self::NotFound(message)
42 | Self::BadRequest(message)
43 | Self::Internal(message) => f.write_str(message),
44 Self::Unavailable | Self::Framework(_) => f.write_str("服务暂时不可用"),
45 }
46 }
47}
48
49impl std::error::Error for ApiError {}
50
51impl FromServerFnError for ApiError {
52 type Encoder = JsonEncoding;
53
54 fn from_server_fn_error(value: ServerFnErrorErr) -> Self {
55 Self::Framework(ServerFnError::from_server_fn_error(value))
56 }
57}
58
59const CODES: &[&str] = &[
60 "unauthorized",
61 "forbidden",
62 "not_found",
63 "bad_request",
64 "unavailable",
65 "internal",
66 "framework",
67];
68
69const FRAMEWORK_VARIANTS: &[&str] = &[
70 "Registration",
71 "Request",
72 "Response",
73 "ServerError",
74 "MiddlewareError",
75 "Deserialization",
76 "Serialization",
77 "Args",
78 "MissingArg",
79];
80
81enum Message {
83 Text(String),
84 Tagged(String, String),
85 Null,
86}
87
88impl<'de> Deserialize<'de> for Message {
89 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
90 struct MessageVisitor;
91
92 impl<'de> Visitor<'de> for MessageVisitor {
93 type Value = Message;
94
95 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
96 f.write_str("错误消息文本或 {变体: 文本}")
97 }
98
99 fn visit_str<E: de::Error>(self, v: &str) -> Result<Message, E> {
100 Ok(Message::Text(v.to_owned()))
101 }
102
103 fn visit_string<E: de::Error>(self, v: String) -> Result<Message, E> {
104 Ok(Message::Text(v))
105 }
106
107 fn visit_unit<E: de::Error>(self) -> Result<Message, E> {
108 Ok(Message::Null)
109 }
110
111 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Message, A::Error> {
112 let Some((name, text)) = map.next_entry::<String, String>()? else {
113 return Err(de::Error::invalid_length(0, &self));
114 };
115 if map.next_key::<IgnoredAny>()?.is_some() {
116 return Err(de::Error::invalid_length(2, &self));
117 }
118 Ok(Message::Tagged(name, text))
119 }
120 }
121
122 d.deserialize_any(MessageVisitor)
123 }
124}
125
126impl<'de> Deserialize<'de> for ApiError {
127 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
128 struct ApiErrorVisitor;
129
130 impl<'de> Visitor<'de> for ApiErrorVisitor {
131 type Value = ApiError;
132
133 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134 f.write_str("带 code 与 message 的错误对象")
135 }
136
137 fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<ApiError, A::Error> {
138 let mut code: Option<String> = None;
139 let mut message: Option<Message> = None;
140 while let Some(key) = map.next_key::<String>()? {
141 match key.as_str() {
142 "code" => {
143 if code.is_some() {
144 return Err(de::Error::duplicate_field("code"));
145 }
146 code = Some(map.next_value()?);
147 }
148 "message" => {
149 if message.is_some() {
150 return Err(de::Error::duplicate_field("message"));
151 }
152 message = Some(map.next_value()?);
153 }
154 _ => {
155 map.next_value::<IgnoredAny>()?;
156 }
157 }
158 }
159 let code = code.ok_or_else(|| de::Error::missing_field("code"))?;
160 api_error_from_parts(&code, message)
161 }
162 }
163
164 d.deserialize_struct("ApiError", &["code", "message"], ApiErrorVisitor)
165 }
166}
167
168fn api_error_from_parts<E: de::Error>(code: &str, message: Option<Message>) -> Result<ApiError, E> {
170 let text = |message: Option<Message>| match message {
171 Some(Message::Text(text)) => Ok(text),
172 None => Err(E::missing_field("message")),
173 Some(_) => Err(E::custom("message 应为字符串")),
174 };
175 Ok(match code {
176 "unauthorized" => ApiError::Unauthorized(text(message)?),
177 "forbidden" => ApiError::Forbidden(text(message)?),
178 "not_found" => ApiError::NotFound(text(message)?),
179 "bad_request" => ApiError::BadRequest(text(message)?),
180 "internal" => ApiError::Internal(text(message)?),
181 "unavailable" => match message {
182 None | Some(Message::Null) => ApiError::Unavailable,
183 Some(_) => return Err(E::custom("unavailable 不带 message")),
184 },
185 "framework" => match message {
186 Some(Message::Tagged(name, text)) => ApiError::Framework(framework_error(&name, text)?),
187 None => return Err(E::missing_field("message")),
188 Some(_) => return Err(E::custom("framework 的 message 应为 {变体: 文本}")),
189 },
190 _ => return Err(E::unknown_variant(code, CODES)),
191 })
192}
193
194fn framework_error<E: de::Error>(name: &str, text: String) -> Result<ServerFnError, E> {
196 Ok(match name {
197 "Registration" => ServerFnError::Registration(text),
198 "Request" => ServerFnError::Request(text),
199 "Response" => ServerFnError::Response(text),
200 "ServerError" => ServerFnError::ServerError(text),
201 "MiddlewareError" => ServerFnError::MiddlewareError(text),
202 "Deserialization" => ServerFnError::Deserialization(text),
203 "Serialization" => ServerFnError::Serialization(text),
204 "Args" => ServerFnError::Args(text),
205 "MissingArg" => ServerFnError::MissingArg(text),
206 _ => return Err(E::unknown_variant(name, FRAMEWORK_VARIANTS)),
207 })
208}
209
210#[derive(Debug)]
212pub enum AppError {
213 Unauthorized(&'static str),
215 Forbidden(&'static str),
217 NotFound(&'static str),
219 BadRequest(String),
221 DbConn(String),
222 Query(String),
223 Transaction(String),
224 Internal(&'static str),
226}
227
228#[cfg(feature = "server")]
229impl AppError {
230 pub fn db_conn(e: impl std::error::Error) -> Self {
232 tracing::error!(
233 "DB connection failed: {}",
234 crate::db::format_with_sources(&e)
235 );
236 AppError::DbConn("connection error".to_string())
237 }
238
239 pub fn query(e: impl std::error::Error) -> Self {
241 tracing::error!("Query failed: {}", crate::db::format_with_sources(&e));
242 AppError::Query("query error".to_string())
243 }
244
245 pub fn tx(e: impl std::error::Error) -> Self {
247 tracing::error!("Transaction failed: {}", crate::db::format_with_sources(&e));
248 AppError::Transaction("transaction error".to_string())
249 }
250}
251
252impl From<AppError> for ApiError {
253 fn from(err: AppError) -> Self {
254 match err {
255 AppError::Unauthorized(message) => Self::Unauthorized(message.into()),
256 AppError::Forbidden(message) => Self::Forbidden(message.into()),
257 AppError::NotFound(message) => Self::NotFound(message.into()),
258 AppError::BadRequest(message) => Self::BadRequest(message),
259 AppError::DbConn(_) => Self::Unavailable,
260 AppError::Query(_) | AppError::Transaction(_) => Self::Internal("操作失败".into()),
261 AppError::Internal(message) => Self::Internal(message.into()),
262 }
263 }
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 #[test]
271 fn business_categories_survive_server_function_encoding() {
272 for error in [
274 ApiError::from(AppError::Unauthorized("not_found:相同消息")),
275 ApiError::from(AppError::Forbidden("not_found:相同消息")),
276 ApiError::from(AppError::NotFound("not_found:相同消息")),
277 ApiError::from(AppError::BadRequest("not_found:相同消息".into())),
278 ApiError::from(AppError::Internal("not_found:相同消息")),
279 ApiError::Unavailable,
280 ] {
281 assert_eq!(ApiError::de(error.ser()), error);
282 }
283 }
284
285 #[test]
286 fn database_details_never_cross_the_wire() {
287 for (internal, expected) in [
288 (
289 AppError::DbConn("postgres://admin:secret@localhost:5432/private".into()),
290 ApiError::Unavailable,
291 ),
292 (
293 AppError::Query("SELECT password FROM users".into()),
294 ApiError::Internal("操作失败".into()),
295 ),
296 (
297 AppError::Transaction("deadlock detected on UPDATE posts".into()),
298 ApiError::Internal("操作失败".into()),
299 ),
300 ] {
301 let error = ApiError::from(internal);
302 assert_eq!(error, expected);
303 assert_eq!(ApiError::de(error.ser()), expected);
304 }
305 }
306
307 #[test]
308 fn malformed_responses_remain_framework_errors() {
309 assert!(matches!(
310 ApiError::de("not_found:代理错误页".into()),
311 ApiError::Framework(ServerFnError::Deserialization(_))
312 ));
313 let error = ApiError::from_server_fn_error(ServerFnErrorErr::Request("not_found".into()));
314 assert!(matches!(
315 ApiError::de(error.ser()),
316 ApiError::Framework(ServerFnError::Request(_))
317 ));
318 }
319
320 #[test]
321 fn wire_format_decodes_independent_of_field_order() {
322 let not_found = ApiError::NotFound("文章不存在".into());
323 assert_eq!(
324 ApiError::de(r#"{"code":"not_found","message":"文章不存在"}"#.into()),
325 not_found
326 );
327 assert_eq!(
328 ApiError::de(r#"{"message":"文章不存在","code":"not_found"}"#.into()),
329 not_found
330 );
331 assert_eq!(
332 ApiError::de(r#"{"code":"unavailable"}"#.into()),
333 ApiError::Unavailable
334 );
335 assert_eq!(
336 ApiError::de(r#"{"code":"unavailable","message":null}"#.into()),
337 ApiError::Unavailable
338 );
339 assert_eq!(
340 ApiError::de(r#"{"code":"framework","message":{"Request":"x"}}"#.into()),
341 ApiError::Framework(ServerFnError::Request("x".into()))
342 );
343 assert!(matches!(
344 ApiError::de(r#"{"code":"teapot","message":"x"}"#.into()),
345 ApiError::Framework(ServerFnError::Deserialization(_))
346 ));
347 }
348
349 #[test]
350 fn framework_variants_round_trip() {
351 let variants: [fn(String) -> ServerFnError; 9] = [
352 ServerFnError::Registration,
353 ServerFnError::Request,
354 ServerFnError::Response,
355 ServerFnError::ServerError,
356 ServerFnError::MiddlewareError,
357 ServerFnError::Deserialization,
358 ServerFnError::Serialization,
359 ServerFnError::Args,
360 ServerFnError::MissingArg,
361 ];
362 for variant in variants {
363 let error = ApiError::Framework(variant("m".into()));
364 assert_eq!(ApiError::de(error.ser()), error);
365 }
366 }
367}