http_problem/
lib.rs

1//! # HTTP Problem-based Error Handling Library
2//!
3//! This crate provides a general mechanism for error handling based on the
4//! [RFC 7807] problem entity with the [`Problem`] type.
5//!
6//! Users can find many pre-defined errors at the [`http`] and [`sql`] modules.
7//!
8//! The workflow for error handling with this library is as follow:
9//!
10//! 1. Use the predefined errors/functions or define a new one with the
11//!    [`define_custom_type!`] macro to returns errors in functions that
12//!    return [`Result<T, Problem>`] (an alias is provided in the library).
13//!     * You can also use the extensions traits [`ResultExt`],
14//!       [`ProblemResultExt`], [`OptionExt`] to handle common cases.
15//! 2. Catch any desired error with [`ProblemResultExt::catch_err`].
16//!
17//! [RFC 7807]: https://tools.ietf.org/html/rfc7807
18//! [`Problem`]: crate::Problem
19//! [`define_custom_type!`]: crate::define_custom_type
20//! [`http`]: crate::http
21//! [`sql`]: crate::sql
22//! [`Result<T, Problem>`]: crate::Result
23//! [`ResultExt`]: crate::ext::ResultExt
24//! [`ProblemResultExt`]: crate::ext::ProblemResultExt
25//! [`OptionExt`]: crate::ext::OptionExt
26//! [`ProblemResultExt::catch_err`]: crate::ext::ProblemResultExt::catch_err
27use std::{borrow::Cow, collections::HashMap, panic::Location};
28
29use backtrace::Backtrace;
30use eyre::EyreContext;
31use parking_lot::Once;
32use serde::ser::SerializeMap;
33
34mod macros;
35
36#[cfg(feature = "actix")]
37mod actix;
38#[cfg(feature = "axum")]
39mod axum;
40mod commons;
41mod custom;
42pub(crate) use self::custom::*;
43mod ext;
44/// HTTP related well-known errors.
45pub mod http;
46/// Definitions for global error reporting.
47pub mod reporter;
48use self::reporter::Report;
49#[cfg(feature = "sql")]
50/// SQL related well-known errors.
51pub mod sql;
52
53/// Prelude imports for this crate.
54pub mod prelude {
55    #[cfg(feature = "actix")]
56    pub use super::actix::*;
57    #[cfg(feature = "axum")]
58    pub use super::axum::*;
59    #[cfg(feature = "sql")]
60    pub use super::sql::*;
61    pub use super::{commons::*, custom::*, ext::*, http, Problem, Result};
62}
63
64pub(crate) fn blank_type_uri() -> custom::Uri {
65    custom::Uri::from_static("about:blank")
66}
67
68/// An alias for a static `Cow<str>`.
69pub type CowStr = Cow<'static, str>;
70
71/// Convenience alias for functions that can error ouy with [`Problem`].
72pub type Result<T, E = Problem> = std::result::Result<T, E>;
73
74fn install() {
75    static HOOK_INSTALLED: Once = Once::new();
76
77    HOOK_INSTALLED.call_once(|| {
78        eyre::set_hook(Box::new(crate::reporter::capture_handler))
79            .expect("Failed to set error hook, maybe install was already called?");
80    })
81}
82
83/// A [RFC 7807] Problem Error.
84///
85/// # Error Cause
86///
87/// This type provides methods to access the inner error cause. Although we
88/// store it, we DO NOT send it when serializing the problem, as it would
89/// leak implementation details.
90///
91/// # Backtraces
92///
93/// Many implementations of the RFC add automatic backtrace to the problem.
94/// This is NOT done by this type and MUST NOT be added manually, as exposing
95/// the backtrace to the caller will expose implementation details and CAN
96/// be source of vulnerabilities.
97///
98/// # Custom Problem Types
99///
100/// When an HTTP API needs to define a response that indicates an error
101/// condition, it might be appropriate to do so by defining a new problem type.
102///
103/// New problem type definitions MUST document:
104///
105/// 1. a type URI (typically, with the "http" or "https" scheme),
106/// 2. a title that appropriately describes it (think short), and
107/// 3. the HTTP status code for it to be used with.
108///
109/// A problem type definition MAY specify additional members on the problem
110/// details object. For example, an extension might use typed links [RFC 5988]
111/// to another resource that can be used by machines to resolve the problem.
112///
113/// Avoid defining custom problem types, preferring to use standardized HTTP
114/// status whenever possible. Custom types should only be defined if no
115/// HTTP status code can properly encode the occurred problem. As an example:
116///
117/// ```ignore
118/// {
119///     "type": "https://example.com/probs/out-of-credit",
120///     "status": 403,
121///     "title": "You do not have enough credit",
122///     "detail": "Your current balance is 30, but that costs 50",
123///     "balance": 30,
124///     "accounts": ["/account/12345", "/account/67890"]
125/// }
126/// ```
127///
128/// When adding a new problem type, we suggest that the type reference should
129/// also be added to the main API gateway page.
130///
131/// # Error Instances
132///
133/// We currently do not track error instances (the `instance` field defined
134/// in the RFC). This may change in the future.
135///
136/// [RFC 7807]: https://tools.ietf.org/html/rfc7807
137/// [RFC 5988]: https://tools.ietf.org/html/rfc5988
138#[derive(Default)]
139pub struct Problem {
140    inner: Box<ProblemInner>,
141}
142
143#[derive(Debug)]
144struct ProblemInner {
145    r#type: Uri,
146    title: CowStr,
147    status: StatusCode,
148    details: CowStr,
149    cause: eyre::Report,
150    extensions: Extensions,
151}
152
153impl Default for ProblemInner {
154    fn default() -> Self {
155        Self {
156            r#type: blank_type_uri(),
157            title: Cow::Borrowed(""),
158            status: StatusCode::default(),
159            details: Cow::Borrowed(""),
160            cause: eyre::Report::msg(""),
161            extensions: Extensions::default(),
162        }
163    }
164}
165
166impl ProblemInner {
167    fn report(&self) -> &Report {
168        self.cause
169            .handler()
170            .downcast_ref::<Report>()
171            .expect("Problem used without installation")
172    }
173}
174
175impl serde::Serialize for Problem {
176    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
177    where
178        S: serde::Serializer,
179    {
180        let mut map = serializer.serialize_map(None)?;
181
182        map.serialize_entry(&"status", &self.status().as_u16())?;
183
184        if !matches!(self.type_().scheme_str(), None | Some("about")) {
185            map.serialize_entry(&"type", &format_args!("{}", self.type_()))?;
186        }
187
188        map.serialize_entry(&"title", &self.title())?;
189        map.serialize_entry(&"detail", &self.details())?;
190
191        for (k, v) in &self.extensions().inner {
192            map.serialize_entry(k, v)?;
193        }
194
195        map.end()
196    }
197}
198
199impl std::fmt::Debug for Problem {
200    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
201        self.inner.report().debug(self.cause(), f)
202    }
203}
204
205impl std::fmt::Display for Problem {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        use eyre::EyreHandler;
208
209        writeln!(
210            f,
211            "{} - {}: {}",
212            self.status(),
213            self.title(),
214            self.details()
215        )?;
216        self.inner.report().display(&*self.inner.cause, f)?;
217
218        Ok(())
219    }
220}
221
222impl std::error::Error for Problem {
223    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
224        Some(self.cause())
225    }
226}
227
228impl Problem {
229    pub(crate) fn report_as_error(&self) {
230        if let Some(reporter) = self::reporter::global_reporter() {
231            if reporter.should_report_error(self) {
232                reporter.report_error(self);
233            }
234        }
235    }
236}
237
238/// [`Problem`] constructors.
239impl Problem {
240    /// Create a custom problem from the given type.
241    ///
242    /// See the type's documentation for more information about custom types.
243    #[track_caller]
244    pub fn custom(status: StatusCode, r#type: Uri) -> Self {
245        let mut problem = Self::from_status(status);
246        problem.inner.r#type = r#type;
247        problem
248    }
249
250    /// Create a new problem for the given status code.
251    #[track_caller]
252    pub fn from_status(status: StatusCode) -> Self {
253        install();
254
255        let title = status.canonical_reason().unwrap();
256        Self {
257            inner: Box::new(ProblemInner {
258                title: title.into(),
259                cause: eyre::Report::msg(title),
260                status,
261                ..ProblemInner::default()
262            }),
263        }
264    }
265
266    /// Sets the title for this problem.
267    ///
268    /// **OBS**: HTTP Status only problems MUST NOT have their title changed.
269    ///
270    /// This method doesn't protect against this, be sure to follow the spec.
271    #[must_use]
272    pub fn with_title(mut self, title: impl Into<CowStr>) -> Self {
273        self.inner.title = title.into();
274        self
275    }
276
277    /// Sets the detail for this problem.
278    #[must_use]
279    pub fn with_detail(mut self, detail: impl Into<CowStr>) -> Self {
280        self.inner.details = detail.into();
281        self
282    }
283
284    /// Sets the error cause for this problem.
285    #[must_use]
286    #[track_caller]
287    pub fn with_cause<E>(mut self, cause: E) -> Self
288    where
289        E: std::error::Error + Send + Sync + 'static,
290    {
291        self.inner.cause = eyre::Report::new(cause);
292        self
293    }
294
295    /// Add a new extension value for the problem.
296    ///
297    /// The `telemetry` extension is reserved for internal use and the `cause`
298    /// extension is reserved for future use.
299    ///
300    /// # Panics
301    ///
302    /// Panics if `field == "cause"` or if the serialization of `value` fails.
303    #[must_use]
304    pub fn with_extension<E, V>(mut self, extension: E, value: V) -> Self
305    where
306        E: Into<CowStr>,
307        V: serde::Serialize,
308    {
309        let extension = extension.into();
310        match extension.as_ref() {
311            "type" | "status" | "details" | "cause" | "" => {
312                panic!("Invalid extension received: {extension}")
313            }
314            _ => self.inner.extensions.insert(extension, value),
315        }
316
317        self
318    }
319}
320
321/// Getters
322impl Problem {
323    /// A URI reference ([RFC 3986]) that identifies the problem type.
324    ///
325    /// The specification encourages that, when dereferenced, it provide
326    /// human-readable documentation for the problem type. When this
327    /// member is not present, its value is assumed to be `about:blank`.
328    ///
329    /// [RFC 3986]: https://tools.ietf.org/html/rfc3986
330    pub const fn type_(&self) -> &Uri {
331        &self.inner.r#type
332    }
333
334    /// A short, human-readable summary of the problem type.
335    ///
336    /// It SHOULD NOT change from occurrence to occurrence of the problem.
337    pub fn title(&self) -> &str {
338        &self.inner.title
339    }
340
341    /// The HTTP status code generated by the origin server for this
342    /// occurrence of the problem.
343    pub const fn status(&self) -> StatusCode {
344        self.inner.status
345    }
346
347    /// A human-readable explanation specific to this occurrence of the
348    /// problem.
349    pub fn details(&self) -> &str {
350        &self.inner.details
351    }
352
353    /// Extra members of the problem containing additional information
354    /// about the specific occurrence.
355    pub const fn extensions(&self) -> &Extensions {
356        &self.inner.extensions
357    }
358
359    /// Extra members of the problem containing additional information
360    /// about the specific occurrence.
361    pub fn extensions_mut(&mut self) -> &mut Extensions {
362        &mut self.inner.extensions
363    }
364
365    /// The internal cause of this problem.
366    pub fn cause(&self) -> &(dyn std::error::Error + 'static) {
367        &*self.inner.cause
368    }
369}
370
371/// Error handling methods.
372impl Problem {
373    /// Get the [`Report`] of this instance.
374    #[must_use]
375    pub fn report(&self) -> &Report {
376        self.inner.report()
377    }
378
379    /// Get the backtrace for this Error.
380    pub fn backtrace(&self) -> Backtrace {
381        (*self.inner.report().backtrace()).clone()
382    }
383
384    /// Location where this instance was created.
385    pub fn location(&self) -> &'static Location<'static> {
386        self.inner.report().location()
387    }
388
389    /// Returns true if `E` is the type of the cause of this problem.
390    ///
391    /// Useful to a failed result is caused by a specific error type.
392    pub fn is<E>(&self) -> bool
393    where
394        E: std::error::Error + Send + Sync + 'static,
395    {
396        self.inner.cause.is::<E>()
397    }
398
399    /// Attempts to downcast the problem to a concrete type.
400    ///
401    /// # Errors
402    ///
403    /// Returns the original problem if the underlying cause is not of the
404    /// specified type.
405    pub fn downcast<E>(mut self) -> Result<E, Self>
406    where
407        E: std::error::Error + Send + Sync + 'static,
408    {
409        match self.inner.cause.downcast() {
410            Ok(err) => Ok(err),
411            Err(cause) => {
412                self.inner.cause = cause;
413                Err(self)
414            }
415        }
416    }
417
418    /// Attempt to downcast the problem to a concrete type by reference.
419    pub fn downcast_ref<E>(&self) -> Option<&E>
420    where
421        E: std::error::Error + Send + Sync + 'static,
422    {
423        self.inner.cause.downcast_ref()
424    }
425
426    /// Attempts to isolate a specific cause to the `Err` variant.
427    ///
428    /// This is different from a downcast as we don't lose backtrace/source
429    /// location information.
430    ///
431    /// This method is useful when the user wants to handle specific errors
432    /// with `?`.
433    ///
434    /// # Errors
435    ///
436    /// Returns `Err` when `self` is an `E`.
437    pub fn isolate<E>(self) -> Result<Self, Self>
438    where
439        E: std::error::Error + Send + Sync + 'static,
440    {
441        if self.is::<E>() {
442            Err(self)
443        } else {
444            Ok(self)
445        }
446    }
447}
448
449/// Set of extensions of a [`Problem`].
450#[derive(Debug, Clone, Default, serde::Serialize)]
451#[serde(transparent)]
452pub struct Extensions {
453    inner: HashMap<CowStr, serde_json::Value>,
454}
455
456impl Extensions {
457    /// Add an extension into the set.
458    ///
459    /// # Panics
460    ///
461    /// Panics if the serialization of `V` fails.
462    pub fn insert<K, V>(&mut self, key: K, value: V)
463    where
464        K: Into<CowStr>,
465        V: serde::Serialize,
466    {
467        self.inner.insert(key.into(), serde_json::json!(value));
468    }
469
470    /// Number of extensions.
471    pub fn len(&self) -> usize {
472        self.inner.len()
473    }
474
475    /// If we have no extensions.
476    pub fn is_empty(&self) -> bool {
477        self.inner.is_empty()
478    }
479}
480
481impl<'e> IntoIterator for &'e Extensions {
482    type IntoIter = ExtensionsIter<'e>;
483    type Item = (&'e str, &'e serde_json::Value);
484
485    fn into_iter(self) -> Self::IntoIter {
486        ExtensionsIter(self.inner.iter().map(|(k, v)| (&**k, v)))
487    }
488}
489
490use std::{collections::hash_map::Iter, iter::Map};
491
492#[doc(hidden)]
493#[allow(clippy::type_complexity)]
494pub struct ExtensionsIter<'e>(
495    Map<
496        Iter<'e, Cow<'e, str>, serde_json::Value>,
497        for<'a> fn((&'a Cow<'a, str>, &'a serde_json::Value)) -> (&'a str, &'a serde_json::Value),
498    >,
499);
500
501impl<'e> Iterator for ExtensionsIter<'e> {
502    type Item = (&'e str, &'e serde_json::Value);
503
504    fn next(&mut self) -> Option<Self::Item> {
505        self.0.next()
506    }
507}
508
509#[cfg(test)]
510mod tests {
511    use std::error::Error;
512
513    use serde_json::json;
514
515    use super::*;
516
517    #[test]
518    fn test_extensions() {
519        let mut ext = Extensions::default();
520
521        assert!(ext.is_empty());
522        assert_eq!(ext.len(), 0);
523        assert!(ext.into_iter().next().is_none());
524
525        ext.insert("bla", "bla");
526
527        assert_eq!(ext.len(), 1);
528        assert!(!ext.is_empty());
529        assert_eq!(ext.into_iter().next(), Some(("bla", &json!("bla"))));
530
531        assert_eq!(json!(ext), json!({ "bla": "bla" }));
532    }
533
534    #[test]
535    fn test_problem_with_extensions_good() {
536        let mut error = http::failed_precondition();
537
538        for (key, value) in [
539            ("bla", json!("bla")),
540            ("foo", json!(1)),
541            ("bar", json!(1.2)),
542            ("baz", json!([1.2])),
543        ] {
544            error = error.with_extension(key, value);
545        }
546
547        assert_eq!(error.extensions().len(), 4);
548    }
549
550    macro_rules! test_invalid_extension {
551        ($test_fn: ident, $ext: literal) => {
552            #[test]
553            #[should_panic = concat!("Invalid extension received: ", $ext)]
554            fn $test_fn() {
555                let _res = http::failed_precondition().with_extension($ext, json!(1));
556            }
557        };
558    }
559
560    test_invalid_extension!(test_problem_with_extension_type, "type");
561    test_invalid_extension!(test_problem_with_extension_status, "status");
562    test_invalid_extension!(test_problem_with_extension_details, "details");
563    test_invalid_extension!(test_problem_with_extension_cause, "cause");
564    test_invalid_extension!(test_problem_with_extension_empty, "");
565
566    #[test]
567    fn test_problem_getter_type_() {
568        assert_eq!(http::failed_precondition().type_(), "about:blank");
569    }
570
571    #[test]
572    fn test_problem_getter_report() {
573        let err = http::failed_precondition();
574        let report = err.report();
575
576        assert_eq!(err.location(), report.location());
577    }
578
579    #[test]
580    fn test_problem_error_handling() {
581        let err = http::failed_precondition();
582
583        assert!(err.is::<http::PreconditionFailed>());
584        assert!(err.downcast_ref::<http::PreconditionFailed>().is_some());
585        assert!(err.isolate::<http::PreconditionFailed>().is_err());
586
587        let err = http::failed_precondition();
588        assert!(!err.is::<http::NotFound>());
589        assert!(err.downcast_ref::<http::NotFound>().is_none());
590        assert!(err.isolate::<http::NotFound>().is_ok());
591
592        let err = http::failed_precondition();
593        assert!(err.downcast::<http::PreconditionFailed>().is_ok());
594
595        let err = http::failed_precondition();
596        assert!(err.downcast::<http::NotFound>().is_err());
597    }
598
599    #[test]
600    fn test_problem_source() {
601        let err = http::failed_precondition();
602        let source = err.source().unwrap() as *const dyn Error as *const ();
603        let cause = err.cause() as *const dyn Error as *const ();
604
605        assert!(core::ptr::eq(source, cause));
606    }
607
608    #[test]
609    fn test_problem_serialize_no_type() {
610        let err = http::failed_precondition()
611            .with_detail("Failed a precondition")
612            .with_extension("foo", "bar");
613
614        assert_eq!(
615            json!(err),
616            json!({
617                "detail": "Failed a precondition",
618                "foo": "bar",
619                "status": 412,
620                "title": "Precondition Failed",
621            })
622        );
623    }
624
625    #[test]
626    fn test_problem_serialize_type() {
627        let err = Problem::custom(
628            StatusCode::PRECONDITION_FAILED,
629            Uri::from_static("https://my.beautiful.error"),
630        )
631        .with_detail("Failed a precondition")
632        .with_extension("foo", "bar");
633
634        assert_eq!(
635            json!(err),
636            json!({
637                "detail": "Failed a precondition",
638                "foo": "bar",
639                "status": 412,
640                "title": "Precondition Failed",
641                "type": "https://my.beautiful.error/",
642            })
643        );
644    }
645}