http_api_problem/
api_error.rs

1//! An error that should be returned from an HTTP API handler.
2//!
3//! It is able to carry typed data via [Extensions] to be used
4//! in middlewares. These values will not become part of any response.
5//!
6//! # Things to know
7//!
8//! [ApiError] can be converted to an [HttpApiProblem] and
9//! also has many conversions to responses of web framewors implemented.
10use std::borrow::Cow;
11use std::collections::HashMap;
12use std::fmt::{self, Display};
13use std::io;
14
15use std::error::Error;
16
17use http::Extensions;
18use serde::Serialize;
19use serde_json::Value;
20
21use super::*;
22pub use http_api_problem_derive::IntoApiError;
23
24pub struct ApiErrorBuilder {
25    /// The suggested status code for the server to be returned to the client
26    pub status: StatusCode,
27
28    /// This is an optional title which can be used to create a valuable output
29    /// for consumers.
30    pub title: Option<String>,
31
32    /// A message that describes the error in a human readable form.
33    ///
34    /// In an [HttpApiProblem] this becomes the `detail` in most cases.
35    pub message: Option<String>,
36
37    /// A URL that points to a detailed description of the error.
38    pub type_url: Option<String>,
39
40    /// A URI reference that identifies the specific
41    /// occurrence of the problem.  It may or may not yield further
42    /// information if dereferenced.
43    pub instance: Option<String>,
44
45    /// Additional JSON encodable information. It is up to the server how and if
46    /// it adds the given information.
47    pub fields: HashMap<String, Value>,
48
49    /// Typed extensions for carrying processable data server side
50    ///
51    /// Can be used e.g. for middlewares
52    ///
53    /// Extensions will not be part of an [HttpApiProblem]
54    pub extensions: Extensions,
55
56    pub source: Option<Box<dyn Error + Send + Sync + 'static>>,
57}
58
59impl ApiErrorBuilder {
60    /// Set the [StatusCode]
61    pub fn status<T: Into<StatusCode>>(mut self, status: T) -> Self {
62        self.status = status.into();
63        self
64    }
65
66    /// Try to set the [StatusCode]
67    ///
68    /// Fails if the `status` argument can not be converted to a [StatusCode]
69    pub fn try_status<T: TryInto<StatusCode>>(self, status: T) -> Result<Self, InvalidStatusCode>
70    where
71        T::Error: Into<InvalidStatusCode>,
72    {
73        let status = status.try_into().map_err(|e| e.into())?;
74        Ok(self.status(status))
75    }
76
77    /// This is an optional title which can be used to create a valuable output
78    /// for consumers.
79    pub fn title<T: Display>(mut self, title: T) -> Self {
80        self.title = Some(title.to_string());
81        self
82    }
83
84    /// A message that describes the error in a human readable form.
85    ///
86    /// In an [HttpApiProblem] this becomes the `detail` in most cases.
87    pub fn message<M: Display>(mut self, message: M) -> Self {
88        self.message = Some(message.to_string());
89        self
90    }
91
92    /// A URL that points to a detailed description of the error.
93    pub fn type_url<U: Display>(mut self, type_url: U) -> Self {
94        self.type_url = Some(type_url.to_string());
95        self
96    }
97
98    /// Sets the `instance`
99    ///
100    /// A URI reference that identifies the specific
101    /// occurrence of the problem.  It may or may not yield further
102    /// information if dereferenced.
103    pub fn instance<T: Display>(mut self, instance: T) -> Self {
104        self.instance = Some(instance.to_string());
105        self
106    }
107
108    /// Adds a serializable field.
109    ///
110    /// If the serialization fails nothing will be added.
111    /// An already present field with the same name will be replaced.
112    pub fn field<T: Into<String>, V: Serialize>(mut self, name: T, value: V) -> Self {
113        if let Ok(value) = serde_json::to_value(value) {
114            self.fields.insert(name.into(), value);
115        }
116
117        self
118    }
119
120    /// Modify the fields values from within a closure
121    pub fn with_fields<F>(mut self, f: F) -> Self
122    where
123        F: FnOnce(HashMap<String, Value>) -> HashMap<String, Value>,
124    {
125        self.fields = f(self.fields);
126
127        self
128    }
129
130    /// Adds an extension value.
131    ///
132    /// Existing values will be overwritten
133    ///
134    /// Extensions will not be part of an [HttpApiProblem]
135    pub fn extension<T: Send + Sync + Clone + 'static>(mut self, val: T) -> Self {
136        let _ = self.extensions.insert(val);
137
138        self
139    }
140
141    /// Modify the extension values from within a closure
142    ///
143    /// Extensions will not be part of an [HttpApiProblem]
144    pub fn with_extensions<F>(mut self, f: F) -> Self
145    where
146        F: FnOnce(Extensions) -> Extensions,
147    {
148        self.extensions = f(self.extensions);
149
150        self
151    }
152
153    pub fn source<E: Error + Send + Sync + 'static>(self, source: E) -> Self {
154        self.source_in_a_box(Box::new(source))
155    }
156
157    pub fn source_in_a_box<E: Into<Box<dyn Error + Send + Sync + 'static>>>(
158        mut self,
159        source: E,
160    ) -> Self {
161        self.source = Some(source.into());
162        self
163    }
164
165    /// Build the [ApiError]
166    pub fn finish(self) -> ApiError {
167        ApiError {
168            status: self.status,
169            title: self.title,
170            message: self.message,
171            type_url: self.type_url,
172            instance: self.instance,
173            fields: self.fields,
174            extensions: self.extensions,
175            source: self.source,
176        }
177    }
178}
179
180/// An error that should be returned from an API handler of a web service.
181///
182/// This should be returned from a handler as an error instead of a response
183/// or [HttpApiProblem]. Allows for logging etc. right before a response is generated.
184///
185/// Advantage over using an [HttpApiProblem] directly are that the [StatusCode] is
186/// mandatory and that this struct can also capture a source error
187/// which the [HttpApiProblem] does not since no error chains
188/// should be transmitted to clients.
189///
190/// # Message on Display and converting to HttpApiProblem
191///
192/// When [Display::fmt] is invoked or when the details field of an [HttpApiProblem]
193/// is filled, the `message` field is used if present. If no `message` is set
194/// but there is a `source` error set, `to_string()` of the source will
195/// be used instead. Otherwise nothing will be displayed or set.
196///
197/// `ApiError` requires the feature `api-error` to be enabled.
198#[derive(Debug)]
199pub struct ApiError {
200    status: StatusCode,
201    title: Option<String>,
202    message: Option<String>,
203    instance: Option<String>,
204    type_url: Option<String>,
205    fields: HashMap<String, Value>,
206    extensions: Extensions,
207    source: Option<Box<dyn Error + Send + Sync + 'static>>,
208}
209
210impl ApiError {
211    /// Get an [ApiErrorBuilder] with the given [StatusCode] preset.
212    pub fn builder<T: Into<StatusCode>>(status: T) -> ApiErrorBuilder {
213        ApiErrorBuilder {
214            status: status.into(),
215            title: None,
216            message: None,
217            type_url: None,
218            instance: None,
219            fields: HashMap::default(),
220            source: None,
221            extensions: Extensions::default(),
222        }
223    }
224
225    /// Try to get an [ApiErrorBuilder] with the given [StatusCode] preset.
226    ///
227    /// Fails if the `status` argument can not be converted to a [StatusCode]
228    pub fn try_builder<S: TryInto<StatusCode>>(
229        status: S,
230    ) -> Result<ApiErrorBuilder, InvalidStatusCode>
231    where
232        S::Error: Into<InvalidStatusCode>,
233    {
234        let status = status.try_into().map_err(|e| e.into())?;
235        Ok(Self::builder(status))
236    }
237
238    /// Create a new instance with the given [StatusCode]
239    pub fn new<T: Into<StatusCode>>(status: T) -> Self {
240        Self {
241            status: status.into(),
242            title: None,
243            message: None,
244            type_url: None,
245            instance: None,
246            fields: HashMap::new(),
247            extensions: Extensions::default(),
248            source: None,
249        }
250    }
251
252    /// Try to create a new instance with the given [StatusCode]
253    ///
254    /// Fails if the `status` argument can not be converted to a [StatusCode]
255    pub fn try_new<S: TryInto<StatusCode>>(status: S) -> Result<Self, InvalidStatusCode>
256    where
257        S::Error: Into<InvalidStatusCode>,
258    {
259        let status = status.try_into().map_err(|e| e.into())?;
260        Ok(Self::new(status))
261    }
262
263    /// Set the [StatusCode].
264    pub fn set_status<T: Into<StatusCode>>(&mut self, status: T) {
265        self.status = status.into();
266    }
267
268    /// Get the [StatusCode].
269    pub fn status(&self) -> StatusCode {
270        self.status
271    }
272
273    /// This is an optional title which can be used to create a valuable output
274    /// for consumers.
275    pub fn set_title<T: Display>(&mut self, title: T) {
276        self.title = Some(title.to_string())
277    }
278
279    /// This is an optional title which can be used to create a valuable output
280    /// for consumers.
281    pub fn title(&self) -> Option<&str> {
282        self.title.as_deref()
283    }
284
285    /// Set a message that describes the error in a human readable form.
286    pub fn set_message<T: Display>(&mut self, message: T) {
287        self.message = Some(message.to_string())
288    }
289
290    /// A message that describes the error in a human readable form.
291    pub fn message(&self) -> Option<&str> {
292        self.message.as_deref()
293    }
294
295    /// Set a URL that points to a detailed description of the error.
296    ///
297    /// If not set it will most probably become `httpstatus.es.com/XXX` when
298    /// the problem response is generated.
299    pub fn set_type_url<T: Display>(&mut self, type_url: T) {
300        self.type_url = Some(type_url.to_string())
301    }
302
303    /// A URL that points to a detailed description of the error.
304    pub fn type_url(&self) -> Option<&str> {
305        self.type_url.as_deref()
306    }
307
308    pub fn set_instance<T: Display>(&mut self, instance: T) {
309        self.instance = Some(instance.to_string())
310    }
311
312    /// A URL that points to a detailed description of the error.
313    pub fn instance(&self) -> Option<&str> {
314        self.instance.as_deref()
315    }
316
317    pub fn set_source<E: Error + Send + Sync + 'static>(&mut self, source: E) {
318        self.set_source_in_a_box(Box::new(source))
319    }
320
321    pub fn set_source_in_a_box<E: Into<Box<dyn Error + Send + Sync + 'static>>>(
322        &mut self,
323        source: E,
324    ) {
325        self.source = Some(source.into());
326    }
327
328    /// Adds a serializable field. If the serialization fails nothing will be
329    /// added. This method returns `true` if the field was added and `false` if
330    /// the field could not be added.
331    ///
332    /// An already present field with the same name will be replaced.
333    pub fn add_field<T: Into<String>, V: Serialize>(&mut self, name: T, value: V) -> bool {
334        self.try_add_field(name, value).is_ok()
335    }
336
337    /// Adds a serializable field. If the serialization fails nothing will be
338    /// added. This fails if a failure occurred while adding the field.
339    ///
340    /// An already present field with the same name will be replaced.
341    pub fn try_add_field<T: Into<String>, V: Serialize>(
342        &mut self,
343        name: T,
344        value: V,
345    ) -> Result<(), Box<dyn Error + 'static>> {
346        let name: String = name.into();
347
348        match name.as_ref() {
349            "type" => return Err("'type' is a reserved field name".into()),
350            "status" => return Err("'status' is a reserved field name".into()),
351            "title" => return Err("'title' is a reserved field name".into()),
352            "detail" => return Err("'detail' is a reserved field name".into()),
353            "instance" => return Err("'instance' is a reserved field name".into()),
354            _ => (),
355        }
356
357        match serde_json::to_value(value) {
358            Ok(value) => {
359                self.fields.insert(name, value);
360                Ok(())
361            }
362            Err(err) => Err(Box::new(err)),
363        }
364    }
365
366    /// Returns a reference to the serialized fields
367    pub fn fields(&self) -> &HashMap<String, Value> {
368        &self.fields
369    }
370
371    /// Returns a mutable reference to the serialized fields
372    pub fn fields_mut(&mut self) -> &mut HashMap<String, Value> {
373        &mut self.fields
374    }
375
376    /// Get a reference to the extensions
377    ///
378    /// Extensions will not be part of an [HttpApiProblem]
379    pub fn extensions(&self) -> &Extensions {
380        &self.extensions
381    }
382
383    /// Get a mutable reference to the extensions
384    ///
385    /// Extensions will not be part of an [HttpApiProblem]
386    pub fn extensions_mut(&mut self) -> &mut Extensions {
387        &mut self.extensions
388    }
389
390    /// Creates an [HttpApiProblem] from this.
391    ///
392    /// Note: If the status is [StatusCode]::UNAUTHORIZED fields will
393    /// **not** be put into the problem.
394    pub fn to_http_api_problem(&self) -> HttpApiProblem {
395        let mut problem = HttpApiProblem::with_title_and_type(self.status);
396
397        problem.title.clone_from(&self.title);
398
399        if let Some(message) = self.detail_message() {
400            problem.detail = Some(message.into())
401        }
402
403        problem.type_url.clone_from(&self.type_url);
404        problem.instance.clone_from(&self.instance);
405
406        if self.status != StatusCode::UNAUTHORIZED {
407            for (key, value) in self.fields.iter() {
408                problem.set_value(key.to_string(), value);
409            }
410        }
411
412        problem
413    }
414
415    /// Turns this into an [HttpApiProblem].
416    ///
417    /// Note: If the status is [StatusCode]::UNAUTHORIZED fields will
418    /// **not** be put into the problem.
419    pub fn into_http_api_problem(self) -> HttpApiProblem {
420        let mut problem = HttpApiProblem::with_title_and_type(self.status);
421
422        if let Some(title) = self.title.as_ref() {
423            problem.title = Some(title.to_owned());
424        }
425
426        if let Some(message) = self.detail_message() {
427            problem.detail = Some(message.into())
428        }
429
430        if let Some(type_url) = self.type_url.as_ref() {
431            problem.type_url = Some(type_url.to_owned())
432        }
433
434        if let Some(instance) = self.instance.as_ref() {
435            problem.instance = Some(instance.to_owned())
436        }
437
438        if self.status != StatusCode::UNAUTHORIZED {
439            for (key, value) in self.fields.iter() {
440                problem.set_value(key.to_string(), value);
441            }
442        }
443
444        problem
445    }
446
447    /// If there is a message it will be the message otherwise the source error stringified
448    ///
449    /// If none is present, `None` is returned
450    pub fn detail_message(&self) -> Option<Cow<str>> {
451        if let Some(message) = self.message.as_ref() {
452            return Some(Cow::Borrowed(message));
453        }
454
455        if let Some(source) = self.source() {
456            return Some(Cow::Owned(source.to_string()));
457        }
458
459        None
460    }
461
462    /// Creates a [hyper] response containing a problem JSON.
463    ///
464    /// Requires the `hyper` feature
465    #[cfg(feature = "hyper")]
466    pub fn into_hyper_response(self) -> hyper::Response<String> {
467        let problem = self.into_http_api_problem();
468        problem.to_hyper_response()
469    }
470
471    /// Creates an axum [Response](axum_core::response::response) containing a problem JSON.
472    ///
473    /// Requires the `axum` feature
474    #[cfg(feature = "axum")]
475    pub fn into_axum_response(self) -> axum_core::response::Response {
476        let problem = self.into_http_api_problem();
477        problem.to_axum_response()
478    }
479
480    /// Creates a `actix-web` response containing a problem JSON.
481    ///
482    /// Requires the `actix.web` feature
483    #[cfg(feature = "actix-web")]
484    pub fn into_actix_web_response(self) -> actix_web::HttpResponse {
485        let problem = self.into_http_api_problem();
486        problem.into()
487    }
488
489    /// Creates a [salvo] response containing a problem JSON.
490    ///
491    /// Requires the `salvo` feature
492    #[cfg(feature = "salvo")]
493    pub fn into_salvo_response(self) -> salvo::Response {
494        let problem = self.into_http_api_problem();
495        problem.to_salvo_response()
496    }
497
498    /// Creates a [tide] response containing a problem JSON.
499    ///
500    /// Requires the `tide` feature
501    #[cfg(feature = "tide")]
502    pub fn into_tide_response(self) -> tide::Response {
503        let problem = self.into_http_api_problem();
504        problem.to_tide_response()
505    }
506}
507
508impl Error for ApiError {
509    fn source(&self) -> Option<&(dyn Error + 'static)> {
510        self.source.as_ref().map(|e| &**e as _)
511    }
512}
513
514impl Display for ApiError {
515    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
516        write!(f, "{}", self.status)?;
517
518        match (self.title.as_ref(), self.detail_message()) {
519            (Some(title), Some(detail)) => return write!(f, " - {} - {}", title, detail),
520            (Some(title), None) => return write!(f, " - {}", title),
521            (None, Some(detail)) => return write!(f, " - {}", detail),
522            (None, None) => (),
523        }
524
525        if let Some(type_url) = self.type_url.as_ref() {
526            return write!(f, " of type {}", type_url);
527        }
528
529        if let Some(instance) = self.instance.as_ref() {
530            return write!(f, " on {}", instance);
531        }
532
533        Ok(())
534    }
535}
536
537impl From<StatusCode> for ApiError {
538    fn from(s: StatusCode) -> Self {
539        Self::new(s)
540    }
541}
542
543impl From<ApiErrorBuilder> for ApiError {
544    fn from(builder: ApiErrorBuilder) -> Self {
545        builder.finish()
546    }
547}
548
549impl From<ApiError> for HttpApiProblem {
550    fn from(error: ApiError) -> Self {
551        error.into_http_api_problem()
552    }
553}
554
555impl From<io::Error> for ApiError {
556    fn from(error: io::Error) -> Self {
557        ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
558            .title("An IO error occurred")
559            .source(error)
560            .finish()
561    }
562}
563
564impl From<std::convert::Infallible> for ApiError {
565    fn from(error: std::convert::Infallible) -> Self {
566        match error {}
567    }
568}
569
570pub trait IntoApiError {
571    fn into_api_error(self) -> ApiError;
572}
573
574impl<T: IntoApiError> From<T> for ApiError {
575    fn from(t: T) -> ApiError {
576        t.into_api_error()
577    }
578}
579
580#[cfg(feature = "hyper")]
581impl From<hyper::Error> for ApiError {
582    fn from(error: hyper::Error) -> Self {
583        ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
584            .source(error)
585            .finish()
586    }
587}
588
589#[cfg(feature = "hyper")]
590impl From<ApiError> for hyper::Response<String> {
591    fn from(error: ApiError) -> hyper::Response<String> {
592        error.into_hyper_response()
593    }
594}
595
596#[cfg(feature = "axum")]
597impl From<ApiError> for axum_core::response::Response {
598    fn from(error: ApiError) -> axum_core::response::Response {
599        error.into_axum_response()
600    }
601}
602
603#[cfg(feature = "axum")]
604impl axum_core::response::IntoResponse for ApiError {
605    fn into_response(self) -> axum_core::response::Response {
606        self.into()
607    }
608}
609
610#[cfg(feature = "actix-web")]
611impl From<actix::prelude::MailboxError> for ApiError {
612    fn from(error: actix::prelude::MailboxError) -> Self {
613        ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
614            .source(error)
615            .finish()
616    }
617}
618
619#[cfg(feature = "actix-web")]
620impl From<ApiError> for actix_web::HttpResponse {
621    fn from(error: ApiError) -> Self {
622        error.into_actix_web_response()
623    }
624}
625
626#[cfg(feature = "actix-web")]
627impl actix_web::error::ResponseError for ApiError {
628    fn error_response(&self) -> actix_web::HttpResponse {
629        let json = self.to_http_api_problem().json_bytes();
630        let actix_status = actix_web::http::StatusCode::from_u16(self.status.as_u16())
631            .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
632
633        actix_web::HttpResponse::build(actix_status)
634            .append_header((
635                actix_web::http::header::CONTENT_TYPE,
636                PROBLEM_JSON_MEDIA_TYPE,
637            ))
638            .body(json)
639    }
640}
641
642#[cfg(feature = "warp")]
643impl warp::reject::Reject for ApiError {}
644
645#[cfg(feature = "salvo")]
646impl From<salvo::Error> for ApiError {
647    fn from(error: salvo::Error) -> Self {
648        ApiError::builder(StatusCode::INTERNAL_SERVER_ERROR)
649            .source(error)
650            .finish()
651    }
652}
653
654#[cfg(feature = "salvo")]
655impl From<ApiError> for salvo::Response {
656    fn from(error: ApiError) -> salvo::Response {
657        error.into_salvo_response()
658    }
659}
660
661#[cfg(feature = "tide")]
662impl From<tide::Error> for ApiError {
663    fn from(error: tide::Error) -> Self {
664        // tide also has its version of status which should always be
665        // convertible without an error.
666        let status: StatusCode = u16::from(error.status())
667            .try_into()
668            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
669        ApiError::builder(status)
670            .source_in_a_box(error.into_inner())
671            .finish()
672    }
673}
674
675#[cfg(feature = "tide")]
676impl From<ApiError> for tide::Response {
677    fn from(error: ApiError) -> tide::Response {
678        error.into_tide_response()
679    }
680}