http_whatever/lib.rs
1//!
2//! A Thread-safe version of [`snafu::Whatever`], which also allows for structured message
3//! strings giving HTTP status code and application domain qualifiers, and allows
4//! an Error to be turned into an [`http::Response`].
5//!
6//! I fully admit that this flies in the face of "type-oriented" error handling, but
7//! I really do feel that is overkill for most HTTP applications where one error (or
8//! one error chain) is the most you will get out of any request/response cycle, and
9//! the goals are simply:
10//!
11//! * Tell the user what went wrong with a standard HTTP status and message, and
12//! * Log the error (chain) for further investigation if necessary
13//!
14//! To that end, this allows you to use the "whatever..." context features from
15//! [`snafu`] while still categorizing your errors and avoiding the boilerplate
16//! of creating error HTTP responses from those errors.
17//!
18//! The message string is comprised of three colon-separated fields, with the first
19//! two being optional:
20//!
21//! * The HTTP status code - the default is `500`
22//! * An arbitrary string denoting the 'domain' of the application that emitted the error.
23//! The significance of this is application-specific and no formatting rules are enforced
24//! for it (except that it cannot contain a colon). The default is "unknown", which is applied
25//! when the field is missing or when it contains the empty string.
26//! * The message
27//!
28//! # Examples
29//!
30//! ## Basic use ala `snafu::Whatever`.
31//!
32//! ```
33//! use http_whatever::prelude::*;
34//! fn parse_uint(uint_as_str: &str) -> HttpResult<usize> {
35//! uint_as_str.parse::<usize>().whatever_context("400:RequestContent:Bad value")
36//! }
37//! ```
38//!
39//! ## Using the macro
40//! ```
41//! use http_whatever::prelude::*;
42//! fn parse_uint(uint_as_str: &str) -> HttpResult<usize> {
43//! uint_as_str.parse().whatever_context(http_err!(400,uint_as_str,"Bad input"))
44//! }
45//! ```
46//!
47use core::fmt::Debug;
48use std::error::Error;
49#[cfg(feature = "serde_errors")]
50use std::fmt::Display;
51
52use http::{header::CONTENT_TYPE, Response, StatusCode};
53use snafu::{whatever, Backtrace, Snafu};
54
55pub type HttpResult<A> = std::result::Result<A, HttpWhatever>;
56
57#[cfg(feature = "serde_errors")]
58use serde::{ser::Error as SerError,de::Error as DeserError};
59
60///
61/// A macro to help format the standard message strings used by this
62/// error type.
63///
64/// `http_err!(status<default 500>,domain<default "unknown">,msg)`
65///
66#[macro_export]
67macro_rules! http_err {
68 ($s:expr,$d:expr,$e:expr) => {
69 format!("{}:{}:{}", $s, $d, $e)
70 };
71 ($d:expr,$e:expr) => {
72 format!("500:{}:{}", $d, $e)
73 };
74 ($e:expr) => {
75 format!("500:unknown:{}", $e)
76 };
77}
78
79///
80/// An almost-drop-in replacement for [`snafu::Whatever`] with the following benefits:
81///
82/// * Conforms to the async magic incantation `Send + Sync + 'static` and so is thread-safe
83/// and async-safe
84/// * Can be transformed into an [`http::Response`] using information from the error to complete
85/// the response
86/// * A public `new` constructor that facilitates better ergonomics in certain error situations.
87/// * A public `parts` method to retrieve the three parts of the error.
88///
89/// Otherwise it is exactly the same as [`snafu::Whatever`] and can be used in exactly the same
90/// way.
91///
92/// (_almost-drop-in_ because, obviously, you have to use `HttpWhatever` as your error type).
93///
94#[derive(Debug, Snafu)]
95#[snafu(whatever)]
96#[snafu(display("{}", self.display()))]
97pub struct HttpWhatever {
98 #[snafu(source(from(Box<dyn std::error::Error + Send + Sync>, Some)))]
99 #[snafu(provide(false))]
100 source: Option<Box<dyn std::error::Error + Send + Sync>>,
101 message: String,
102 backtrace: Backtrace,
103}
104
105impl HttpWhatever {
106 ///
107 /// Return the three parts of the message as a 3-element tuple.
108 ///
109 /// The three parts are
110 ///
111 /// * The `message` as a string slice
112 /// * the `domain` as a string slice
113 /// * the HTTP status code as a [`http::StatusCode`]
114 ///
115 /// This method is useful if you wish to construct a customized response
116 /// from the error, but still want the categorization that this error type
117 /// allows.
118 ///
119 pub fn parts(&self) -> (&str, &str, StatusCode) {
120 let parts: Vec<&str> = self.message.splitn(3, ':').collect::<Vec<&str>>();
121 let mut idx = parts.len();
122
123 let message = if idx == 0 {
124 "<unknown>"
125 } else {
126 idx -= 1;
127 parts[idx]
128 };
129 let domain = if idx == 0 {
130 "Internal"
131 } else {
132 idx -= 1;
133 parts[idx]
134 };
135 let status_code = if idx == 0 {
136 StatusCode::INTERNAL_SERVER_ERROR
137 } else {
138 StatusCode::from_bytes(parts[idx - 1].as_bytes())
139 .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
140 };
141
142 (message, domain, status_code)
143 }
144
145 fn display(&self) -> String {
146 let parts = self.parts();
147 format!(
148 "{}: (Domain: {}, HTTP status: {})",
149 parts.0, parts.1, parts.2
150 )
151 }
152
153 ///
154 /// Return a String that provides the `to_string()` output of this error and all nested sources.
155 ///
156 pub fn details(&self) -> String {
157 let mut s = self.to_string();
158 let mut source = self.source();
159 while let Some(e) = source {
160 s.push_str(&format!("\n[{e}]"));
161 source = e.source();
162 }
163 s
164 }
165
166 ///
167 /// Return an [`http::Response<B>`] representation of the error, with
168 /// a body generated from the `default` method of the generic body type.
169 ///
170 /// # Panics
171 /// Technically the function will panic if, internally, it cannot build
172 /// the response, but since the parts of the response are already verified,
173 /// and the `Body` is only created with `B::default()`, that isn't actually
174 /// possible.
175 pub fn as_http_response<B>(&self) -> Response<B>
176 where
177 B: Default,
178 {
179 let parts = self.parts();
180 Response::builder()
181 .status(parts.2)
182 .body(B::default())
183 .expect("Response::build should succeed")
184 }
185
186 ///
187 /// Return an [`http::Response<B>`] representation of the error, with
188 /// a string body generated from the `into` method of the generic body
189 /// type.
190 ///
191 /// The string in the response body will be of the format
192 ///
193 /// `<message> (application domain: <domain>)`
194 ///
195 /// The `content-type` header of the response will be `text/plain`.
196 ///
197 /// # Panics
198 /// Technically the function will panic if, internally, it cannot build
199 /// the response, but since the parts of the response are already verified,
200 /// and the `Body` is only created with `B::from(String)`, that isn't actually
201 /// possible.
202 pub fn as_http_string_response<B>(&self) -> Response<B>
203 where
204 B: From<String>,
205 {
206 let parts = self.parts();
207 let body_str = format!("{} (application domain: {})", parts.0, parts.1);
208 let body: B = body_str.into();
209 Response::builder()
210 .status(parts.2)
211 .header(CONTENT_TYPE, "text/plain")
212 .body(body)
213 .expect("Response::build should succeed")
214 }
215
216 ///
217 /// Return an [`http::Response<B>`] representation of the error, with
218 /// a JSON body generated from the `into` method.
219 ///
220 /// The string in the response body will be of the format
221 ///
222 /// `{"message":"<message>","domain":"<domain>"}`
223 ///
224 /// The `content-type` header of the response will be `application/json`.
225 ///
226 /// # Panics
227 /// Technically the function will panic if, internally, it cannot build
228 /// the response, but since the parts of the response are already verified,
229 /// and the `Body` is only created with `B::from(String)`, that isn't actually
230 /// possible.
231 pub fn as_http_json_response<B>(&self) -> Response<B>
232 where
233 B: From<String>,
234 {
235 let parts = self.parts();
236 let body_str = format!("{{\"message\":\"{}\",\"domain\":\"{}\"}}", parts.0, parts.1);
237 let body: B = body_str.into();
238 Response::builder()
239 .status(parts.2)
240 .header(CONTENT_TYPE, "application/json")
241 .body(body)
242 .expect("Response::build should succeed")
243 }
244
245 ///
246 /// Create a new `HttpWhatever` from the input string.
247 ///
248 /// The input string should conform to the structure documented in the
249 /// crate documentation.
250 ///
251 pub fn new(message: impl std::fmt::Display) -> Self {
252 let err_gen = |message| -> HttpResult<()> { whatever!("{message}") };
253 err_gen(message).unwrap_err()
254 }
255}
256
257#[cfg(feature = "serde_errors")]
258impl SerError for HttpWhatever {
259 fn custom<T>(msg:T) -> Self where T:Display {
260 HttpWhatever::new(msg)
261 }
262}
263
264#[cfg(feature = "serde_errors")]
265impl DeserError for HttpWhatever {
266 fn custom<T>(msg:T) -> Self where T:Display {
267 HttpWhatever::new(msg)
268 }
269}
270
271///
272/// A prelude of the main items required to use this type effectively.
273///
274/// This includes the important items from the [`snafu`] prelude, so _you_ do not
275/// have to include the [`snafu`] prelude.
276///
277pub mod prelude {
278 pub use crate::http_err;
279 pub use crate::HttpResult;
280 pub use crate::HttpWhatever;
281 pub use snafu::{ensure, OptionExt as _, ResultExt as _};
282 pub use snafu::{ensure_whatever, whatever};
283}
284
285#[cfg(test)]
286mod tests {
287 use crate::prelude::*;
288 use http::{header::CONTENT_TYPE, Response, StatusCode};
289 use std::num::ParseIntError;
290
291 fn parse_usize(strint: &str) -> Result<usize, ParseIntError> {
292 strint.parse()
293 }
294
295 #[test]
296 fn basic_test() {
297 let result: HttpWhatever = parse_usize("certainly not a usize")
298 .whatever_context("400:Input:That was NOT a usize!")
299 .unwrap_err();
300
301 let parts = result.parts();
302 assert_eq!(parts.0, "That was NOT a usize!");
303 assert_eq!(parts.1, "Input");
304 assert_eq!(parts.2, StatusCode::BAD_REQUEST);
305 }
306
307 #[test]
308 fn basic_details() {
309 let result: HttpWhatever = parse_usize("certainly not a usize")
310 .whatever_context("400:Input:That was NOT a usize!")
311 .unwrap_err();
312
313 assert_eq!(result.details(), "That was NOT a usize!: (Domain: Input, HTTP status: 400 Bad Request)\n[invalid digit found in string]");
314 }
315
316 #[test]
317 fn test_macro() {
318 let result: HttpWhatever = parse_usize("certainly not a usize")
319 .whatever_context(http_err!(400, "Input", "That was NOT a usize!"))
320 .unwrap_err();
321
322 let parts = result.parts();
323 assert_eq!(parts.0, "That was NOT a usize!");
324 assert_eq!(parts.1, "Input");
325 assert_eq!(parts.2, StatusCode::BAD_REQUEST);
326 }
327
328 #[test]
329 fn test_new() {
330 let result: HttpWhatever =
331 HttpWhatever::new(&http_err!(403, "Input", "That was NOT a usize!"));
332
333 let parts = result.parts();
334 assert_eq!(parts.0, "That was NOT a usize!");
335 assert_eq!(parts.1, "Input");
336 assert_eq!(parts.2, StatusCode::FORBIDDEN);
337 }
338
339 #[test]
340 fn test_response() {
341 let result: HttpWhatever =
342 HttpWhatever::new(&http_err!(403, "Input", "That was NOT a usize!"));
343 let http1: Response<String> = result.as_http_response();
344 let http2: Response<String> = result.as_http_string_response();
345 let http3: Response<String> = result.as_http_json_response();
346
347 assert_eq!(http1.body(), "");
348 assert_eq!(http1.status(), StatusCode::FORBIDDEN);
349 assert_eq!(
350 http2.body(),
351 "That was NOT a usize! (application domain: Input)"
352 );
353 assert_eq!(
354 http2.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(),
355 "text/plain"
356 );
357 assert_eq!(
358 http3.body(),
359 "{\"message\":\"That was NOT a usize!\",\"domain\":\"Input\"}"
360 );
361 assert_eq!(
362 http3.headers().get(CONTENT_TYPE).unwrap().to_str().unwrap(),
363 "application/json"
364 );
365 }
366}