impulse_utils/
errors.rs

1//! Implementation of optional private errors for `salvo` and client errors for `reqwest`.
2
3#[cfg(feature = "mresult")]
4use http::StatusCode;
5
6#[cfg(feature = "salvo")]
7#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
8use salvo::oapi::{EndpointOutRegister, ToSchema};
9
10#[cfg(feature = "salvo")]
11#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
12use salvo::{Depot, Request, Response};
13
14#[cfg(feature = "salvo")]
15#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
16use salvo::Writer as ServerResponseWriter;
17
18/// Data structure responsible for server errors.
19#[cfg(feature = "mresult")]
20#[derive(Clone)]
21pub struct ServerError {
22  /// Status code to return.
23  pub status_code: Option<StatusCode>,
24  /// Text to return (and hide error messages that leads to leak vulnerable data).
25  pub public_msg: Option<String>,
26  /// Text that really describes error situation.
27  pub private_msg: Option<Vec<String>>,
28}
29
30#[cfg(feature = "mresult")]
31impl std::fmt::Debug for ServerError {
32  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33    f.write_str(&format!(
34      "Error: `{}` status code\n  Error message: \"{}\"{}",
35      self.status_code.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR).as_str(),
36      self.decide_public_msg(),
37      if let Some(privates) = self.private_msg.as_ref() {
38        format!(
39          "\n{}",
40          privates
41            .iter()
42            .map(|e| format!("    Caused by: {e}"))
43            .collect::<Vec<_>>()
44            .join("\n")
45        )
46      } else {
47        String::new()
48      }
49    ))
50  }
51}
52
53/// Public error message.
54#[derive(Debug, serde::Serialize, serde::Deserialize)]
55#[cfg_attr(
56  all(feature = "salvo", not(any(target_arch = "wasm32", target_arch = "wasm64"))),
57  derive(salvo::oapi::ToSchema)
58)]
59pub struct ErrorResponse {
60  /// Public error message.
61  pub err: String,
62}
63
64#[cfg(feature = "mresult")]
65impl std::fmt::Display for ServerError {
66  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67    f.write_str(&format!(
68      "Error: `{}` status code\n  Error message: \"{}\"{}",
69      self.status_code.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR).as_str(),
70      self.decide_public_msg(),
71      if let Some(privates) = self.private_msg.as_ref() {
72        format!(
73          "\n{}",
74          privates
75            .iter()
76            .map(|e| format!("    Caused by: {e}"))
77            .collect::<Vec<_>>()
78            .join("\n")
79        )
80      } else {
81        String::new()
82      }
83    ))
84  }
85}
86
87#[cfg(feature = "mresult")]
88impl std::error::Error for ServerError {}
89
90impl std::fmt::Display for ErrorResponse {
91  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92    f.write_str(&format!("Error: {}", self.err))
93  }
94}
95
96#[cfg(feature = "mresult")]
97impl std::error::Error for ErrorResponse {}
98
99/// Data structure responsible for client errors.
100#[cfg(feature = "cresult")]
101#[derive(Clone)]
102pub struct ClientError {
103  /// Error message.
104  pub message: String,
105}
106
107#[cfg(feature = "cresult")]
108impl std::fmt::Display for ClientError {
109  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110    f.write_str(self.message.as_str())
111  }
112}
113
114#[cfg(feature = "cresult")]
115impl std::fmt::Debug for ClientError {
116  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117    f.write_str(self.message.as_str())
118  }
119}
120
121#[cfg(feature = "cresult")]
122impl std::error::Error for ClientError {}
123
124#[cfg(all(feature = "salvo", feature = "mresult"))]
125#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
126impl crate::responses::ExplicitServerWrite for ServerError {
127  async fn explicit_write(self, res: &mut Response) {
128    res.status_code(self.status_code.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR));
129    tracing::error!("{}", self);
130
131    res
132      .add_header(salvo::http::header::CONTENT_TYPE, "application/json", true)
133      .ok();
134    res
135      .write_body(
136        sonic_rs::to_string(&ErrorResponse {
137          err: self.decide_public_msg(),
138        })
139        .unwrap_or(r#"{"err":"Unknown server error"}"#.to_string()),
140      )
141      .ok();
142  }
143}
144
145#[cfg(all(feature = "salvo", feature = "mresult"))]
146#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
147#[salvo::async_trait]
148impl ServerResponseWriter for ServerError {
149  /// Method for sending an error message to the client.
150  async fn write(self, _req: &mut Request, _depot: &mut Depot, res: &mut Response) {
151    crate::responses::ExplicitServerWrite::explicit_write(self, res).await
152  }
153}
154
155#[cfg(all(feature = "salvo", feature = "mresult"))]
156#[cfg(not(any(target_arch = "wasm32", target_arch = "wasm64")))]
157impl EndpointOutRegister for ServerError {
158  /// Registers error types for OpenAPI.
159  fn register(components: &mut salvo::oapi::Components, operation: &mut salvo::oapi::Operation) {
160    operation.responses.insert(
161      "400",
162      salvo::oapi::Response::new("Bad request").add_content("application/json", ErrorResponse::to_schema(components)),
163    );
164    operation.responses.insert(
165      "401",
166      salvo::oapi::Response::new("Unauthorized").add_content("application/json", ErrorResponse::to_schema(components)),
167    );
168    operation.responses.insert(
169      "403",
170      salvo::oapi::Response::new("Forbidden").add_content("application/json", ErrorResponse::to_schema(components)),
171    );
172    operation.responses.insert(
173      "404",
174      salvo::oapi::Response::new("Not found").add_content("application/json", ErrorResponse::to_schema(components)),
175    );
176    operation.responses.insert(
177      "405",
178      salvo::oapi::Response::new("Method not allowed")
179        .add_content("application/json", ErrorResponse::to_schema(components)),
180    );
181    operation.responses.insert(
182      "500",
183      salvo::oapi::Response::new("Internal server error")
184        .add_content("application/json", ErrorResponse::to_schema(components)),
185    );
186  }
187}
188
189#[cfg(any(feature = "mresult", all(feature = "reqwest", feature = "cresult")))]
190pub(crate) fn public_msg_from(status_code: &Option<u16>) -> &'static str {
191  match status_code {
192    Some(400) => "Bad request.",
193    Some(401) => "Unauthorized request.",
194    Some(403) => "Access denied.",
195    Some(404) => "Page or method not found.",
196    Some(405) => "Method not allowed.",
197    Some(500) => "Internal server error. Contact the administrator.",
198    _ => "Specific error. Check with the administrator for details.",
199  }
200}
201
202#[cfg(feature = "mresult")]
203impl ServerError {
204  fn decide_public_msg(&self) -> String {
205    if let Some(public_msg) = self.public_msg.as_ref() {
206      public_msg.to_owned()
207    } else {
208      public_msg_from(&self.status_code.as_ref().map(|v| v.as_u16())).to_string()
209    }
210  }
211
212  fn format_error(error: &(dyn std::error::Error + 'static)) -> Vec<String> {
213    let mut result = vec![];
214    let mut current_error: Option<&(dyn std::error::Error + 'static)> = Some(error);
215
216    while let Some(err) = current_error {
217      result.push(err.to_string());
218      current_error = err.source();
219    }
220
221    result
222  }
223
224  /// Makes a new ServerError with actual error.
225  pub fn from_private(err: impl std::error::Error + 'static) -> Self {
226    let err_data = Self::format_error(&err);
227
228    Self {
229      status_code: None,
230      public_msg: None,
231      private_msg: Some(err_data),
232    }
233  }
234
235  /// Makes a new ServerError with actual error provided by plain string.
236  pub fn from_private_str(err: impl Into<String>) -> Self {
237    Self {
238      status_code: None,
239      public_msg: None,
240      private_msg: Some(vec![err.into()]),
241    }
242  }
243
244  /// Adds a private message to the ServerError.
245  pub fn with_private_str(mut self, new_private_msg: impl Into<String>) -> Self {
246    if let Some(privates) = self.private_msg.as_mut() {
247      privates.insert(0, new_private_msg.into());
248    } else {
249      self.private_msg = Some(vec![new_private_msg.into()]);
250    }
251
252    self
253  }
254
255  /// Adds a public message to the ServerError.
256  ///
257  /// If ServerError already have a public message, it goes to private messages stack.
258  pub fn with_public(mut self, new_public_msg: impl Into<String>) -> Self {
259    if let Some(old_public_msg) = self.public_msg.take() {
260      if let Some(privates) = self.private_msg.as_mut() {
261        privates.insert(0, old_public_msg);
262      } else {
263        self.private_msg = Some(vec![old_public_msg]);
264      }
265    }
266
267    self.public_msg = Some(new_public_msg.into());
268    self
269  }
270
271  /// Makes a new Server Error from public message.
272  pub fn from_public(public_msg: impl Into<String>) -> Self {
273    Self {
274      status_code: None,
275      public_msg: Some(public_msg.into()),
276      private_msg: None,
277    }
278  }
279
280  /// Error BAD REQUEST (400).
281  pub fn with_400(mut self) -> Self {
282    self.status_code = Some(StatusCode::BAD_REQUEST);
283    self
284  }
285
286  /// Error UNAUTHORIZED (401).
287  pub fn with_401(mut self) -> Self {
288    self.status_code = Some(StatusCode::UNAUTHORIZED);
289    self
290  }
291
292  /// Error FORBIDDEN (403).
293  pub fn with_403(mut self) -> Self {
294    self.status_code = Some(StatusCode::FORBIDDEN);
295    self
296  }
297
298  /// Error NOT FOUND (404).
299  pub fn with_404(mut self) -> Self {
300    self.status_code = Some(StatusCode::NOT_FOUND);
301    self
302  }
303
304  /// Error METHOD NOT ALLOWED (405).
305  pub fn with_405(mut self) -> Self {
306    self.status_code = Some(StatusCode::METHOD_NOT_ALLOWED);
307    self
308  }
309
310  /// Error INTERNAL SERVER ERROR (500).
311  pub fn with_500(mut self) -> Self {
312    self.status_code = Some(StatusCode::INTERNAL_SERVER_ERROR);
313    self
314  }
315
316  /// Adds any status code you need.
317  pub fn with_code(mut self, code: StatusCode) -> Self {
318    self.status_code = Some(code);
319    self
320  }
321
322  /// Converts the ServerError into Result::Err.
323  pub fn bail<T>(self) -> Result<T, Self> {
324    Err(self)
325  }
326}
327
328#[cfg(feature = "cresult")]
329impl ClientError {
330  /// Converts any `std::error::Error` to `ClientError`.
331  pub fn from<T: std::error::Error>(value: T) -> Self {
332    Self {
333      message: value.to_string(),
334    }
335  }
336
337  /// Constructs `ClientError` from string.
338  #[allow(clippy::should_implement_trait)]
339  pub fn from_str(value: impl Into<String>) -> Self {
340    Self { message: value.into() }
341  }
342
343  /// Adds some context with the string.
344  pub fn context(mut self, value: impl Into<String>) -> Self {
345    self.message = value.into() + "\n  Caused by: " + &self.message;
346    self
347  }
348}
349
350#[cfg(feature = "cresult")]
351impl AsRef<str> for ClientError {
352  fn as_ref(&self) -> &str {
353    self.message.as_str()
354  }
355}