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}