1use leptos::prelude::ServerFnError;
7
8use crate::api::error::ApiError;
9
10const GENERIC_REASON: &str = "操作失败";
12
13const UNAVAILABLE_REASON: &str = "服务暂时不可用,请稍后再试";
15
16pub fn error_reason(err: &ApiError) -> Option<String> {
18 match err {
19 ApiError::Unauthorized(message)
20 | ApiError::Forbidden(message)
21 | ApiError::NotFound(message)
22 | ApiError::BadRequest(message)
23 | ApiError::Internal(message) => {
24 let message = message.trim();
25 (!message.is_empty() && message != GENERIC_REASON).then(|| message.to_string())
26 }
27 ApiError::Unavailable => Some("服务暂时不可用".to_string()),
28 ApiError::Framework(ServerFnError::Request(_)) => {
29 Some("网络连接中断,请检查网络后重试".to_string())
30 }
31 ApiError::Framework(
34 ServerFnError::Deserialization(message) | ServerFnError::Response(message),
35 ) => {
36 let reason = ["413", "429", "502", "503", "504"]
37 .iter()
38 .find(|code| message.contains(**code))
39 .and_then(|code| code.parse().ok().and_then(status_reason))
40 .unwrap_or(UNAVAILABLE_REASON);
41 Some(reason.to_string())
42 }
43 _ => None,
44 }
45}
46
47fn status_reason(code: u16) -> Option<&'static str> {
48 match code {
49 413 => Some("内容超过服务器允许的大小"),
50 429 => Some("请求过于频繁,请稍后再试"),
51 502..=504 => Some(UNAVAILABLE_REASON),
52 _ => None,
53 }
54}
55
56pub fn failure_message(action: &str, err: &ApiError) -> String {
58 match error_reason(err) {
59 Some(reason) => format!("{action}:{reason}"),
60 None => format!("{action},请稍后再试"),
61 }
62}
63
64pub fn is_not_found(err: &ApiError) -> bool {
66 matches!(err, ApiError::NotFound(_))
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn business_reason_and_generic_failure_are_presented_separately() {
75 let error = ApiError::BadRequest("标题不能为空".into());
76 assert_eq!(
77 failure_message("保存失败", &error),
78 "保存失败:标题不能为空"
79 );
80 let missing = ApiError::NotFound("文章不存在".into());
81 assert_eq!(
82 failure_message("加载失败", &missing),
83 "加载失败:文章不存在"
84 );
85 for message in ["操作失败", " "] {
86 let error = ApiError::Internal(message.into());
87 assert_eq!(failure_message("删除失败", &error), "删除失败,请稍后再试");
88 }
89 }
90
91 #[test]
92 fn unparseable_client_failures_never_leak_internal_text() {
93 let err = ApiError::Framework(ServerFnError::Deserialization(
94 "expected value at line 1".to_string(),
95 ));
96 let text = failure_message("发布失败", &err);
97 assert_eq!(text, "发布失败:服务暂时不可用,请稍后再试");
98 assert!(!text.contains("error"));
99 assert!(!text.contains("line 1"));
100 }
101
102 #[test]
103 fn non_json_error_pages_are_described_by_status() {
104 let too_large = ApiError::Framework(ServerFnError::Deserialization(
105 "Invalid format: missing delimiter in \"<html><title>413 Request Entity Too Large</title>\""
106 .to_string(),
107 ));
108 assert_eq!(
109 error_reason(&too_large).as_deref(),
110 Some("内容超过服务器允许的大小")
111 );
112 let proxy = ApiError::Framework(ServerFnError::Response(
113 "<h1>502 Bad Gateway</h1>".to_string(),
114 ));
115 assert_eq!(
116 failure_message("登录失败", &proxy),
117 "登录失败:服务暂时不可用,请稍后再试"
118 );
119 let busy = ApiError::Framework(ServerFnError::Deserialization(
120 "<h1>429 Too Many Requests</h1>".to_string(),
121 ));
122 assert_eq!(
123 failure_message("登录失败", &busy),
124 "登录失败:请求过于频繁,请稍后再试"
125 );
126 let unknown = ApiError::Framework(ServerFnError::Deserialization(
127 "<p>418 teapot</p>".to_string(),
128 ));
129 assert_eq!(
130 failure_message("登录失败", &unknown),
131 "登录失败:服务暂时不可用,请稍后再试"
132 );
133 }
134
135 #[test]
136 fn request_errors_read_as_network_problems() {
137 let err = ApiError::Framework(ServerFnError::Request("connection reset".to_string()));
138 assert_eq!(
139 failure_message("回复失败", &err),
140 "回复失败:网络连接中断,请检查网络后重试"
141 );
142 }
143
144 #[test]
145 fn other_variants_have_no_reason() {
146 let err = ApiError::Framework(ServerFnError::Args("missing field".to_string()));
147 assert_eq!(error_reason(&err), None);
148 assert_eq!(failure_message("保存失败", &err), "保存失败,请稍后再试");
149 }
150
151 #[test]
152 fn not_found_requires_the_typed_category() {
153 assert!(is_not_found(&ApiError::NotFound("文章不存在".into())));
154 for error in [
155 ApiError::BadRequest("not_found:文章不存在".into()),
156 ApiError::Internal("not_found".into()),
157 ApiError::Framework(ServerFnError::Request("not_found:x".into())),
158 ApiError::Framework(ServerFnError::ServerError("not_found".into())),
159 ] {
160 assert!(!is_not_found(&error));
161 }
162 }
163}