http_api_problem/
lib.rs

1//! # HTTP-API-PROBLEM
2//!
3//! [![crates.io](https://img.shields.io/crates/v/http-api-problem.svg)](https://crates.io/crates/http-api-problem)
4//! [![docs.rs](https://docs.rs/http-api-problem/badge.svg)](https://docs.rs/http-api-problem)
5//! [![downloads](https://img.shields.io/crates/d/http-api-problem.svg)](https://crates.io/crates/http-api-problem)
6//! ![CI](https://github.com/chridou/http-api-problem/workflows/CI/badge.svg)
7//! [![license-mit](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/chridou/http-api-problem/blob/master/LICENSE-MIT)
8//! [![license-apache](http://img.shields.io/badge/license-APACHE-blue.svg)](https://github.com/chridou/http-api-problem/blob/master/LICENSE-APACHE)
9//!
10//! A library to create HTTP response content for APIs based on
11//! [RFC7807](https://tools.ietf.org/html/rfc7807).
12//!
13//! ## Usage
14//!
15//! Get the latest version for your `Cargo.toml` from
16//! [crates.io](https://crates.io/crates/http-api-problem).
17//!
18//! Add this to your crate root:
19//!
20//! ```rust
21//! use http_api_problem;
22//! ```
23//!
24//!  ## serde
25//!
26//! [HttpApiProblem] implements [Serialize] and [Deserialize] for
27//! [HttpApiProblem].
28//!
29//! ## Examples
30//!
31//! ```rust
32//! use http_api_problem::*;
33//!
34//! let p = HttpApiProblem::new(StatusCode::UNPROCESSABLE_ENTITY)
35//!     .title("You do not have enough credit.")
36//!     .detail("Your current balance is 30, but that costs 50.")
37//!     .type_url("https://example.com/probs/out-of-credit")
38//!     .instance("/account/12345/msgs/abc");
39//!
40//! assert_eq!(Some(StatusCode::UNPROCESSABLE_ENTITY), p.status);
41//! assert_eq!(Some("You do not have enough credit."), p.title.as_deref());
42//! assert_eq!(Some("Your current balance is 30, but that costs 50."), p.detail.as_deref());
43//! assert_eq!(Some("https://example.com/probs/out-of-credit"), p.type_url.as_deref());
44//! assert_eq!(Some("/account/12345/msgs/abc"), p.instance.as_deref());
45//! ```
46//!
47//! There is also `TryFrom<u16>` implemented for [StatusCode]:
48//!
49//! ```rust
50//! use http_api_problem::*;
51//!
52//! let p = HttpApiProblem::try_new(422).unwrap()
53//!     .title("You do not have enough credit.")
54//!     .detail("Your current balance is 30, but that costs 50.")
55//!     .type_url("https://example.com/probs/out-of-credit")
56//!     .instance("/account/12345/msgs/abc");
57//!
58//! assert_eq!(Some(StatusCode::UNPROCESSABLE_ENTITY), p.status);
59//! assert_eq!(Some("You do not have enough credit."), p.title.as_deref());
60//! assert_eq!(Some("Your current balance is 30, but that costs 50."), p.detail.as_deref());
61//! assert_eq!(Some("https://example.com/probs/out-of-credit"), p.type_url.as_deref());
62//! assert_eq!(Some("/account/12345/msgs/abc"), p.instance.as_deref());
63//! ```
64//!
65//! ## Status Codes
66//!
67//! The specification does not require the [HttpApiProblem] to contain a
68//! status code. Nevertheless this crate supports creating responses
69//! for web frameworks. Responses require a status code. If no status code
70//! was set on the [HttpApiProblem] `500 - Internal Server Error` will be
71//! used as a fallback. This can be easily avoided by only using those constructor
72//! functions which require a [StatusCode].
73//!
74//! ## Features
75//!
76//! ### JsonSchema
77//!
78//! The feature `json-schema` enables a derived implementation for
79//! JsonSchema, via `schemars`.
80//!
81//! ### Web Frameworks
82//!
83//! There are multiple features to integrate with web frameworks:
84//!
85//! * `axum`
86//! * `warp`
87//! * `hyper`
88//! * `actix-web`
89//! * `salvo`
90//! * `tide`
91//! * `rocket (v0.5.0-rc1)`
92//!
93//! These mainly convert the `HttpApiProblem` to response types of
94//! the frameworks and implement traits to integrate with the frameworks
95//! error handling.
96//!
97//! Additionally, the feature `rocket-okapi` (which implies the features
98//! `rocket` and `json-schema`) implements `rocket_okapi`'s `OpenApiResponder`
99//! for the json schema generated by the `json-schema` feature.
100//!
101//! ### ApiError
102//!
103//! The feature `api-error` enables a structure which can be
104//! return from "api handlers" that generate responses and can be
105//! converted into an `HttpApiProblem`.
106//!
107//! ## License
108//!
109//! `http-api-problem` is primarily distributed under the terms of both the MIT
110//! license and the Apache License (Version 2.0).
111//!
112//! Copyright (c) 2017 Christian Douven.
113use std::convert::TryInto;
114use std::error::Error;
115use std::fmt;
116
117use serde::{de::DeserializeOwned, Deserialize, Serialize};
118use serde_json::Value;
119use std::collections::HashMap;
120
121#[cfg(feature = "api-error")]
122mod api_error;
123#[cfg(feature = "api-error")]
124pub use api_error::*;
125
126#[cfg(feature = "json-schema")]
127use schemars::JsonSchema;
128
129#[cfg(feature = "actix-web")]
130use actix_web_crate as actix_web;
131
132#[cfg(feature = "axum")]
133use axum_core;
134
135pub use http::status::{InvalidStatusCode, StatusCode};
136
137/// The recommended media type when serialized to JSON
138///
139/// "application/problem+json"
140pub static PROBLEM_JSON_MEDIA_TYPE: &str = "application/problem+json";
141
142/// Description of a problem that can be returned by an HTTP API
143/// based on [RFC7807](https://tools.ietf.org/html/rfc7807)
144///
145/// # Example
146///
147/// ```javascript
148/// {
149///    "type": "https://example.com/probs/out-of-credit",
150///    "title": "You do not have enough credit.",
151///    "detail": "Your current balance is 30, but that costs 50.",
152///    "instance": "/account/12345/msgs/abc",
153/// }
154/// ```
155///
156/// # Purpose
157///
158/// The purpose of [HttpApiProblem] is to generate a meaningful response
159/// for clients. It is not intended to be used as a replacement
160/// for a proper `Error` struct within applications.
161///
162/// For a struct which can be returned by HTTP handlers use [ApiError] which
163/// can be enabled with the feature toggle `api-error`. [ApiError] can be directly
164/// converted into [HttpApiProblem].
165///
166/// # Status Codes and Responses
167///
168/// Prefer to use one of the constructors which
169/// ensure that a [StatusCode] is set. If no [StatusCode] is
170/// set and a transformation to a response of a web framework
171/// is made a [StatusCode] becomes mandatory which in this case will
172/// default to `500`.
173///
174/// When receiving an [HttpApiProblem] there might be an invalid
175/// [StatusCode] contained. In this case the `status` field will be empty.
176/// This is a trade off so that the recipient does not have to deal with
177/// another error and can still have access to the remaining fields of the
178/// struct.
179#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
180#[cfg_attr(feature = "json-schema", derive(JsonSchema))]
181#[cfg_attr(
182    feature = "json-schema",
183    schemars(
184        description = "Description of a problem that can be returned by an HTTP API based on [RFC7807](https://tools.ietf.org/html/rfc7807)"
185    )
186)]
187pub struct HttpApiProblem {
188    /// A URI reference [RFC3986](https://tools.ietf.org/html/rfc3986) that identifies the
189    /// problem type.  This specification encourages that, when
190    /// dereferenced, it provide human-readable documentation for the
191    /// problem type (e.g., using HTML [W3C.REC-html5-20141028]).  When
192    /// this member is not present, its value is assumed to be
193    /// "about:blank".
194    #[serde(rename = "type")]
195    #[serde(skip_serializing_if = "Option::is_none")]
196    #[cfg_attr(
197        feature = "json-schema",
198        schemars(
199            description = "A [RFC3986 URI reference](https://tools.ietf.org/html/rfc3986) that identifies the problem type. When dereferenced, it may provide human-readable documentation for the problem type."
200        )
201    )]
202    pub type_url: Option<String>,
203
204    /// The HTTP status code [RFC7231, Section 6](https://tools.ietf.org/html/rfc7231#section-6)
205    /// generated by the origin server for this occurrence of the problem.
206    #[serde(default)]
207    #[serde(with = "custom_http_status_serialization")]
208    #[cfg_attr(feature = "json-schema", schemars(with = "u16"))]
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub status: Option<StatusCode>,
211
212    /// A short, human-readable summary of the problem
213    /// type. It SHOULD NOT change from occurrence to occurrence of the
214    /// problem, except for purposes of localization (e.g., using
215    /// proactive content negotiation;
216    /// see [RFC7231, Section 3.4](https://tools.ietf.org/html/rfc7231#section-3.4).
217    #[cfg_attr(
218        feature = "json-schema",
219        schemars(
220            description = "A short, human-readable summary of the problem type. It SHOULD NOT change from occurrence to occurrence of the problem."
221        )
222    )]
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub title: Option<String>,
225
226    /// A human-readable explanation specific to this
227    /// occurrence of the problem.
228    #[serde(skip_serializing_if = "Option::is_none")]
229    pub detail: Option<String>,
230
231    /// A URI reference that identifies the specific
232    /// occurrence of the problem.  It may or may not yield further
233    /// information if dereferenced.
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub instance: Option<String>,
236
237    /// Additional fields that must be JSON values
238    ///
239    /// These values get serialized into the JSON
240    /// on top level.
241    #[serde(flatten)]
242    additional_fields: HashMap<String, serde_json::Value>,
243}
244
245impl HttpApiProblem {
246    /// Creates a new instance with the given [StatusCode].
247    ///
248    /// #Example
249    ///
250    /// ```rust
251    /// use http_api_problem::*;
252    ///
253    /// let p = HttpApiProblem::new(StatusCode::INTERNAL_SERVER_ERROR);
254    ///
255    /// assert_eq!(Some(StatusCode::INTERNAL_SERVER_ERROR), p.status);
256    /// assert_eq!(None, p.title);
257    /// assert_eq!(None, p.detail);
258    /// assert_eq!(None, p.type_url);
259    /// assert_eq!(None, p.instance);
260    /// ```
261    pub fn new<T: Into<StatusCode>>(status: T) -> Self {
262        Self::empty().status(status)
263    }
264
265    /// Creates a new instance with the given [StatusCode].
266    ///
267    /// Fails if the argument can not be converted into a [StatusCode].
268    ///
269    /// #Example
270    ///
271    /// ```rust
272    /// use http_api_problem::*;
273    ///
274    /// let p = HttpApiProblem::try_new(500).unwrap();
275    ///
276    /// assert_eq!(Some(StatusCode::INTERNAL_SERVER_ERROR), p.status);
277    /// assert_eq!(None, p.title);
278    /// assert_eq!(None, p.detail);
279    /// assert_eq!(None, p.type_url);
280    /// assert_eq!(None, p.instance);
281    /// ```
282    pub fn try_new<T: TryInto<StatusCode>>(status: T) -> Result<Self, InvalidStatusCode>
283    where
284        T::Error: Into<InvalidStatusCode>,
285    {
286        let status = status.try_into().map_err(|e| e.into())?;
287        Ok(Self::new(status))
288    }
289
290    /// Creates a new instance with `title` derived from a [StatusCode].
291    ///
292    /// #Example
293    ///
294    /// ```rust
295    /// use http_api_problem::*;
296    ///
297    /// let p = HttpApiProblem::with_title(StatusCode::NOT_FOUND);
298    ///
299    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
300    /// assert_eq!(Some("Not Found"), p.title.as_deref());
301    /// assert_eq!(None, p.detail);
302    /// assert_eq!(None, p.type_url);
303    /// assert_eq!(None, p.instance);
304    /// ```
305    pub fn with_title<T: Into<StatusCode>>(status: T) -> Self {
306        let status = status.into();
307        Self::new(status).title(
308            status
309                .canonical_reason()
310                .unwrap_or("<unknown status code>")
311                .to_string(),
312        )
313    }
314
315    /// Creates a new instance with `title` derived from a [StatusCode].
316    ///
317    /// Fails if the argument can not be converted into a [StatusCode].
318    ///
319    /// #Example
320    ///
321    /// ```rust
322    /// use http_api_problem::*;
323    ///
324    /// let p = HttpApiProblem::try_with_title(404).unwrap();
325    ///
326    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
327    /// assert_eq!(Some("Not Found"), p.title.as_deref());
328    /// assert_eq!(None, p.detail);
329    /// assert_eq!(None, p.type_url);
330    /// assert_eq!(None, p.instance);
331    /// ```
332    pub fn try_with_title<T: TryInto<StatusCode>>(status: T) -> Result<Self, InvalidStatusCode>
333    where
334        T::Error: Into<InvalidStatusCode>,
335    {
336        let status = status.try_into().map_err(|e| e.into())?;
337        Ok(Self::with_title(status))
338    }
339
340    /// Creates a new instance with the `title` and `type_url` derived from the
341    /// [StatusCode].
342    ///
343    /// #Example
344    ///
345    /// ```rust
346    /// use http_api_problem::*;
347    ///
348    /// let p = HttpApiProblem::with_title_and_type(StatusCode::SERVICE_UNAVAILABLE);
349    ///
350    /// assert_eq!(Some(StatusCode::SERVICE_UNAVAILABLE), p.status);
351    /// assert_eq!(Some("Service Unavailable"), p.title.as_deref());
352    /// assert_eq!(None, p.detail);
353    /// assert_eq!(Some("https://httpstatuses.com/503".to_string()), p.type_url);
354    /// assert_eq!(None, p.instance);
355    /// ```
356    pub fn with_title_and_type<T: Into<StatusCode>>(status: T) -> Self {
357        let status = status.into();
358        Self::with_title(status).type_url(format!("https://httpstatuses.com/{}", status.as_u16()))
359    }
360
361    /// Creates a new instance with the `title` and `type_url` derived from the
362    /// [StatusCode].
363    ///
364    /// Fails if the argument can not be converted into a [StatusCode].
365    ///
366    /// #Example
367    ///
368    /// ```rust
369    /// use http_api_problem::*;
370    ///
371    /// let p = HttpApiProblem::try_with_title_and_type(503).unwrap();
372    ///
373    /// assert_eq!(Some(StatusCode::SERVICE_UNAVAILABLE), p.status);
374    /// assert_eq!(Some("Service Unavailable"), p.title.as_deref());
375    /// assert_eq!(None, p.detail);
376    /// assert_eq!(Some("https://httpstatuses.com/503".to_string()), p.type_url);
377    /// assert_eq!(None, p.instance);
378    /// ```
379    pub fn try_with_title_and_type<T: TryInto<StatusCode>>(
380        status: T,
381    ) -> Result<Self, InvalidStatusCode>
382    where
383        T::Error: Into<InvalidStatusCode>,
384    {
385        let status = status.try_into().map_err(|e| e.into())?;
386
387        Ok(Self::with_title_and_type(status))
388    }
389
390    /// Creates a new instance without any field set.
391    ///
392    /// Prefer to use one of the other constructors which
393    /// ensure that a [StatusCode] is set. If no [StatusCode] is
394    /// set and a transformation to a response of a web framework
395    /// is made a [StatusCode] becomes mandatory which in this case will
396    /// default to `500`.
397    pub fn empty() -> Self {
398        HttpApiProblem {
399            type_url: None,
400            status: None,
401            title: None,
402            detail: None,
403            instance: None,
404            additional_fields: Default::default(),
405        }
406    }
407
408    /// Sets the `status`
409    ///
410    /// #Example
411    ///
412    /// ```rust
413    /// use http_api_problem::*;
414    ///
415    /// let p = HttpApiProblem::new(StatusCode::NOT_FOUND).title("Error");
416    ///
417    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
418    /// assert_eq!(Some("Error"), p.title.as_deref());
419    /// assert_eq!(None, p.detail);
420    /// assert_eq!(None, p.type_url);
421    /// assert_eq!(None, p.instance);
422    /// ```
423    pub fn status<T: Into<StatusCode>>(mut self, status: T) -> Self {
424        self.status = Some(status.into());
425        self
426    }
427
428    /// Sets the `type_url`
429    ///
430    /// #Example
431    ///
432    /// ```rust
433    /// use http_api_problem::*;
434    ///
435    /// let p = HttpApiProblem::new(StatusCode::NOT_FOUND).type_url("http://example.com/my/real_error");
436    ///
437    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
438    /// assert_eq!(None, p.title);
439    /// assert_eq!(None, p.detail);
440    /// assert_eq!(Some("http://example.com/my/real_error".to_string()), p.type_url);
441    /// assert_eq!(None, p.instance);
442    /// ```
443    pub fn type_url<T: Into<String>>(mut self, type_url: T) -> Self {
444        self.type_url = Some(type_url.into());
445        self
446    }
447
448    /// Tries to set the `status`
449    ///
450    /// Fails if the argument can not be converted into a [StatusCode].
451    ///
452    /// #Example
453    ///
454    /// ```rust
455    /// use http_api_problem::*;
456    ///
457    /// let p = HttpApiProblem::try_new(404).unwrap().title("Error");
458    ///
459    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
460    /// assert_eq!(Some("Error"), p.title.as_deref());
461    /// assert_eq!(None, p.detail);
462    /// assert_eq!(None, p.type_url);
463    /// assert_eq!(None, p.instance);
464    /// ```
465    pub fn try_status<T: TryInto<StatusCode>>(
466        mut self,
467        status: T,
468    ) -> Result<Self, InvalidStatusCode>
469    where
470        T::Error: Into<InvalidStatusCode>,
471    {
472        self.status = Some(status.try_into().map_err(|e| e.into())?);
473        Ok(self)
474    }
475
476    /// Sets the `title`
477    ///
478    /// #Example
479    ///
480    /// ```rust
481    /// use http_api_problem::*;
482    ///
483    /// let p = HttpApiProblem::new(StatusCode::NOT_FOUND).title("Another Error");
484    ///
485    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
486    /// assert_eq!(Some("Another Error"), p.title.as_deref());
487    /// assert_eq!(None, p.detail);
488    /// assert_eq!(None, p.type_url);
489    /// assert_eq!(None, p.instance);
490    /// ```
491    pub fn title<T: Into<String>>(mut self, title: T) -> Self {
492        self.title = Some(title.into());
493        self
494    }
495
496    /// Sets the `detail`
497    ///
498    /// #Example
499    ///
500    /// ```rust
501    /// use http_api_problem::*;
502    ///
503    /// let p = HttpApiProblem::new(StatusCode::NOT_FOUND).detail("a detailed description");
504    ///
505    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
506    /// assert_eq!(None, p.title);
507    /// assert_eq!(Some("a detailed description".to_string()), p.detail);
508    /// assert_eq!(None, p.type_url);
509    /// assert_eq!(None, p.instance);
510    /// ```
511    pub fn detail<T: Into<String>>(mut self, detail: T) -> HttpApiProblem {
512        self.detail = Some(detail.into());
513        self
514    }
515
516    /// Sets the `instance`
517    ///
518    /// #Example
519    ///
520    /// ```rust
521    /// use http_api_problem::*;
522    ///
523    /// let p = HttpApiProblem::new(StatusCode::NOT_FOUND).instance("/account/1234/withdraw");
524    ///
525    /// assert_eq!(Some(StatusCode::NOT_FOUND), p.status);
526    /// assert_eq!(None, p.title);
527    /// assert_eq!(None, p.detail);
528    /// assert_eq!(None, p.type_url);
529    /// assert_eq!(Some("/account/1234/withdraw".to_string()), p.instance);
530    /// ```
531    pub fn instance<T: Into<String>>(mut self, instance: T) -> HttpApiProblem {
532        self.instance = Some(instance.into());
533        self
534    }
535
536    /// Add a value that must be serializable.
537    ///
538    /// The key must not be one of the field names of this struct.
539    ///
540    /// These values get serialized into the JSON
541    /// on top level.
542    pub fn try_value<K, V>(
543        mut self,
544        key: K,
545        value: &V,
546    ) -> Result<Self, Box<dyn Error + Send + Sync + 'static>>
547    where
548        V: Serialize,
549        K: Into<String>,
550    {
551        self.try_set_value(key, value)?;
552        Ok(self)
553    }
554
555    /// Add a value that must be serializable.
556    ///
557    /// The key must not be one of the field names of this struct.
558    /// If the key is a field name or the value is not serializable nothing happens.
559    ///
560    /// These values get serialized into the JSON
561    /// on top level.
562    pub fn value<K, V>(mut self, key: K, value: &V) -> Self
563    where
564        V: Serialize,
565        K: Into<String>,
566    {
567        self.set_value(key, value);
568        self
569    }
570
571    /// Add a value that must be serializable.
572    ///
573    /// The key must not be one of the field names of this struct.
574    /// If the key is a field name or the value is not serializable nothing happens.
575    ///
576    /// These values get serialized into the JSON
577    /// on top level.
578    pub fn set_value<K, V>(&mut self, key: K, value: &V)
579    where
580        V: Serialize,
581        K: Into<String>,
582    {
583        let _ = self.try_set_value(key, value);
584    }
585
586    /// Returns the deserialized field for the given key.
587    ///
588    /// If the key does not exist or the field is not deserializable to
589    /// the target type `None` is returned
590    pub fn get_value<K, V>(&self, key: &str) -> Option<V>
591    where
592        V: DeserializeOwned,
593    {
594        self.json_value(key)
595            .and_then(|v| serde_json::from_value(v.clone()).ok())
596    }
597
598    pub fn try_set_value<K, V>(
599        &mut self,
600        key: K,
601        value: &V,
602    ) -> Result<(), Box<dyn Error + Send + Sync + 'static>>
603    where
604        V: Serialize,
605        K: Into<String>,
606    {
607        let key: String = key.into();
608        match key.as_ref() {
609            "type" => return Err("'type' is a reserved field name".into()),
610            "status" => return Err("'status' is a reserved field name".into()),
611            "title" => return Err("'title' is a reserved field name".into()),
612            "detail" => return Err("'detail' is a reserved field name".into()),
613            "instance" => return Err("'instance' is a reserved field name".into()),
614            "additional_fields" => {
615                return Err("'additional_fields' is a reserved field name".into());
616            }
617            _ => (),
618        }
619        let serialized = serde_json::to_value(value).map_err(|err| err.to_string())?;
620        self.additional_fields.insert(key, serialized);
621        Ok(())
622    }
623
624    /// Returns a reference to the serialized fields
625    ///
626    /// If the key does not exist or the field is not deserializable to
627    /// the target type `None` is returned
628    pub fn additional_fields(&self) -> &HashMap<String, Value> {
629        &self.additional_fields
630    }
631
632    /// Returns a mutable reference to the serialized fields
633    ///
634    /// If the key does not exist or the field is not deserializable to
635    /// the target type `None` is returned
636    pub fn additional_fields_mut(&mut self) -> &mut HashMap<String, Value> {
637        &mut self.additional_fields
638    }
639
640    pub fn keys<K, V>(&self) -> impl Iterator<Item = &String>
641    where
642        V: DeserializeOwned,
643    {
644        self.additional_fields.keys()
645    }
646
647    /// Returns the `serde_json::Value` for the given key if the key exists.
648    pub fn json_value(&self, key: &str) -> Option<&serde_json::Value> {
649        self.additional_fields.get(key)
650    }
651
652    /// Serialize to a JSON `Vec<u8>`
653    pub fn json_bytes(&self) -> Vec<u8> {
654        serde_json::to_vec(self).unwrap()
655    }
656
657    /// Serialize to a JSON `String`
658    pub fn json_string(&self) -> String {
659        serde_json::to_string_pretty(self).unwrap()
660    }
661
662    /// Creates a [hyper] response.
663    ///
664    /// If status is `None` `500 - Internal Server Error` is the
665    /// default.
666    ///
667    /// Requires the `hyper` feature
668    #[cfg(feature = "hyper")]
669    pub fn to_hyper_response(&self) -> hyper::Response<String> {
670        use hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE};
671        use hyper::*;
672
673        let json = self.json_string();
674        let length = json.len() as u64;
675
676        let (mut parts, body) = Response::new(json).into_parts();
677
678        parts.headers.insert(
679            CONTENT_TYPE,
680            HeaderValue::from_static(PROBLEM_JSON_MEDIA_TYPE),
681        );
682        parts.headers.insert(
683            CONTENT_LENGTH,
684            HeaderValue::from_str(&length.to_string()).unwrap(),
685        );
686        parts.status = self
687            .status_or_internal_server_error()
688            .as_u16()
689            .try_into()
690            .unwrap_or(hyper::StatusCode::INTERNAL_SERVER_ERROR);
691
692        Response::from_parts(parts, body)
693    }
694
695    /// Creates an axum [Response](axum_core::response::Response).
696    ///
697    /// If status is `None` `500 - Internal Server Error` is the
698    /// default.
699    ///
700    /// Requires the `axum` feature
701    #[cfg(feature = "axum")]
702    pub fn to_axum_response(&self) -> axum_core::response::Response {
703        use axum_core::response::IntoResponse;
704        use http::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE};
705
706        let json = self.json_bytes();
707        let length = json.len() as u64;
708
709        let status = self.status_or_internal_server_error();
710
711        let mut response = (status, json).into_response();
712
713        *response.status_mut() = self.status_or_internal_server_error();
714
715        response.headers_mut().insert(
716            CONTENT_TYPE,
717            HeaderValue::from_static(PROBLEM_JSON_MEDIA_TYPE),
718        );
719        response.headers_mut().insert(
720            CONTENT_LENGTH,
721            HeaderValue::from_str(&length.to_string()).unwrap(),
722        );
723
724        response
725    }
726
727    /// Creates an `actix` response.
728    ///
729    /// If status is `None` or not convertible
730    /// to an actix status `500 - Internal Server Error` is the
731    /// default.
732    ///
733    /// Requires the `actix-web` feature
734    #[cfg(feature = "actix-web")]
735    pub fn to_actix_response(&self) -> actix_web::HttpResponse {
736        let effective_status = self.status_or_internal_server_error();
737        let actix_status = actix_web::http::StatusCode::from_u16(effective_status.as_u16())
738            .unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
739
740        let json = self.json_bytes();
741
742        actix_web::HttpResponse::build(actix_status)
743            .append_header((
744                actix_web::http::header::CONTENT_TYPE,
745                PROBLEM_JSON_MEDIA_TYPE,
746            ))
747            .body(json)
748    }
749
750    /// Creates a `rocket` response.
751    ///
752    /// If status is `None` `500 - Internal Server Error` is the
753    /// default.
754    ///
755    /// Requires the `rocket` feature
756    #[cfg(feature = "rocket")]
757    pub fn to_rocket_response(&self) -> rocket::Response<'static> {
758        use rocket::http::ContentType;
759        use rocket::http::Status;
760        use rocket::Response;
761        use std::io::Cursor;
762
763        let content_type: ContentType = PROBLEM_JSON_MEDIA_TYPE.parse().unwrap();
764        let json = self.json_bytes();
765        let response = Response::build()
766            .status(Status {
767                code: self.status_code_or_internal_server_error(),
768            })
769            .sized_body(json.len(), Cursor::new(json))
770            .header(content_type)
771            .finalize();
772
773        response
774    }
775
776    /// Creates a [salvo] response.
777    ///
778    /// If status is `None` `500 - Internal Server Error` is the
779    /// default.
780    ///
781    /// Requires the `salvo` feature
782    #[cfg(feature = "salvo")]
783    pub fn to_salvo_response(&self) -> salvo::Response {
784        use salvo::hyper::header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE};
785        use salvo::hyper::*;
786
787        let json = self.json_string();
788        let length = json.len() as u64;
789
790        let (mut parts, body) = Response::new(json).into_parts();
791
792        parts.headers.insert(
793            CONTENT_TYPE,
794            HeaderValue::from_static(PROBLEM_JSON_MEDIA_TYPE),
795        );
796        parts.headers.insert(
797            CONTENT_LENGTH,
798            HeaderValue::from_str(&length.to_string()).unwrap(),
799        );
800        parts.status = self
801            .status_or_internal_server_error()
802            .as_u16()
803            .try_into()
804            .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
805
806        Response::from_parts(parts, body).into()
807    }
808
809    /// Creates a [tide] response.
810    ///
811    /// If status is `None` `500 - Internal Server Error` is the
812    /// default.
813    ///
814    /// Requires the `tide` feature
815    #[cfg(feature = "tide")]
816    pub fn to_tide_response(&self) -> tide::Response {
817        let json = self.json_bytes();
818        let length = json.len() as u64;
819
820        tide::Response::builder(self.status_code_or_internal_server_error())
821            .body(json)
822            .header("Content-Length", length.to_string())
823            .content_type(PROBLEM_JSON_MEDIA_TYPE)
824            .build()
825    }
826
827    #[allow(dead_code)]
828    fn status_or_internal_server_error(&self) -> StatusCode {
829        self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
830    }
831
832    #[allow(dead_code)]
833    fn status_code_or_internal_server_error(&self) -> u16 {
834        self.status_or_internal_server_error().as_u16()
835    }
836
837    // Deprecations
838
839    #[deprecated(since = "0.50.0", note = "please use `with_title` instead")]
840    pub fn with_title_from_status<T: Into<StatusCode>>(status: T) -> Self {
841        Self::with_title(status)
842    }
843    #[deprecated(since = "0.50.0", note = "please use `with_title_and_type` instead")]
844    pub fn with_title_and_type_from_status<T: Into<StatusCode>>(status: T) -> Self {
845        Self::with_title_and_type(status)
846    }
847    #[deprecated(since = "0.50.0", note = "please use `status` instead")]
848    pub fn set_status<T: Into<StatusCode>>(self, status: T) -> Self {
849        self.status(status)
850    }
851    #[deprecated(since = "0.50.0", note = "please use `title` instead")]
852    pub fn set_title<T: Into<String>>(self, title: T) -> Self {
853        self.title(title)
854    }
855    #[deprecated(since = "0.50.0", note = "please use `detail` instead")]
856    pub fn set_detail<T: Into<String>>(self, detail: T) -> Self {
857        self.detail(detail)
858    }
859    #[deprecated(since = "0.50.0", note = "please use `type_url` instead")]
860    pub fn set_type_url<T: Into<String>>(self, type_url: T) -> Self {
861        self.type_url(type_url)
862    }
863    #[deprecated(since = "0.50.0", note = "please use `instance` instead")]
864    pub fn set_instance<T: Into<String>>(self, instance: T) -> Self {
865        self.instance(instance)
866    }
867}
868
869impl fmt::Display for HttpApiProblem {
870    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
871        if let Some(status) = self.status {
872            write!(f, "{}", status)?;
873        } else {
874            write!(f, "<no status>")?;
875        }
876
877        match (self.title.as_ref(), self.detail.as_ref()) {
878            (Some(title), Some(detail)) => return write!(f, " - {} - {}", title, detail),
879            (Some(title), None) => return write!(f, " - {}", title),
880            (None, Some(detail)) => return write!(f, " - {}", detail),
881            (None, None) => (),
882        }
883
884        if let Some(type_url) = self.type_url.as_ref() {
885            return write!(f, " - {}", type_url);
886        }
887
888        Ok(())
889    }
890}
891
892impl Error for HttpApiProblem {
893    fn source(&self) -> Option<&(dyn Error + 'static)> {
894        None
895    }
896}
897
898impl From<StatusCode> for HttpApiProblem {
899    fn from(status: StatusCode) -> HttpApiProblem {
900        HttpApiProblem::new(status)
901    }
902}
903
904impl From<std::convert::Infallible> for HttpApiProblem {
905    fn from(error: std::convert::Infallible) -> HttpApiProblem {
906        match error {}
907    }
908}
909
910/// Creates an [hyper::Response] from something that can become an
911/// `HttpApiProblem`.
912///
913/// If status is `None` `500 - Internal Server Error` is the
914/// default.
915#[cfg(feature = "hyper")]
916pub fn into_hyper_response<T: Into<HttpApiProblem>>(what: T) -> hyper::Response<String> {
917    let problem: HttpApiProblem = what.into();
918    problem.to_hyper_response()
919}
920
921#[cfg(feature = "hyper")]
922impl From<HttpApiProblem> for hyper::Response<String> {
923    fn from(problem: HttpApiProblem) -> hyper::Response<String> {
924        problem.to_hyper_response()
925    }
926}
927
928/// Creates an axum [Response](axum_core::response::Response) from something that can become an
929/// `HttpApiProblem`.
930///
931/// If status is `None` `500 - Internal Server Error` is the
932/// default.
933///
934/// Requires the `axum` feature
935#[cfg(feature = "axum")]
936pub fn into_axum_response<T: Into<HttpApiProblem>>(what: T) -> axum_core::response::Response {
937    let problem: HttpApiProblem = what.into();
938    problem.to_axum_response()
939}
940
941#[cfg(feature = "axum")]
942impl From<HttpApiProblem> for axum_core::response::Response {
943    fn from(problem: HttpApiProblem) -> axum_core::response::Response {
944        problem.to_axum_response()
945    }
946}
947
948#[cfg(feature = "axum")]
949impl axum_core::response::IntoResponse for HttpApiProblem {
950    fn into_response(self) -> axum_core::response::Response {
951        self.into()
952    }
953}
954
955// Creates an `actix::HttpResponse` from something that can become an
956/// `HttpApiProblem`.
957///
958/// If status is `None` `500 - Internal Server Error` is the
959/// default.
960#[cfg(feature = "actix-web")]
961pub fn into_actix_response<T: Into<HttpApiProblem>>(what: T) -> actix_web::HttpResponse {
962    let problem: HttpApiProblem = what.into();
963    problem.to_actix_response()
964}
965
966#[cfg(feature = "actix-web")]
967impl From<HttpApiProblem> for actix_web::HttpResponse {
968    fn from(problem: HttpApiProblem) -> actix_web::HttpResponse {
969        problem.to_actix_response()
970    }
971}
972
973/// Creates an `rocket::Response` from something that can become an
974/// `HttpApiProblem`.
975///
976/// If status is `None` `500 - Internal Server Error` is the
977/// default.
978#[cfg(feature = "rocket")]
979pub fn into_rocket_response<T: Into<HttpApiProblem>>(what: T) -> ::rocket::Response<'static> {
980    let problem: HttpApiProblem = what.into();
981    problem.to_rocket_response()
982}
983
984#[cfg(feature = "rocket")]
985impl From<HttpApiProblem> for ::rocket::Response<'static> {
986    fn from(problem: HttpApiProblem) -> ::rocket::Response<'static> {
987        problem.to_rocket_response()
988    }
989}
990
991#[cfg(feature = "rocket")]
992impl<'r> ::rocket::response::Responder<'r, 'static> for HttpApiProblem {
993    fn respond_to(self, _request: &::rocket::Request) -> ::rocket::response::Result<'static> {
994        Ok(self.into())
995    }
996}
997
998#[cfg(feature = "rocket-okapi")]
999impl rocket_okapi::response::OpenApiResponderInner for HttpApiProblem {
1000    fn responses(
1001        gen: &mut rocket_okapi::gen::OpenApiGenerator,
1002    ) -> rocket_okapi::Result<rocket_okapi::okapi::openapi3::Responses> {
1003        let mut responses = rocket_okapi::okapi::openapi3::Responses::default();
1004        let schema = gen.json_schema::<HttpApiProblem>();
1005        rocket_okapi::util::add_default_response_schema(
1006            &mut responses,
1007            PROBLEM_JSON_MEDIA_TYPE,
1008            schema,
1009        );
1010        Ok(responses)
1011    }
1012}
1013
1014#[cfg(feature = "warp")]
1015impl warp::reject::Reject for HttpApiProblem {}
1016
1017/// Creates a [salvo::Response] from something that can become an
1018/// `HttpApiProblem`.
1019///
1020/// If status is `None` `500 - Internal Server Error` is the
1021/// default.
1022#[cfg(feature = "salvo")]
1023pub fn into_salvo_response<T: Into<HttpApiProblem>>(what: T) -> salvo::Response {
1024    let problem: HttpApiProblem = what.into();
1025    problem.to_salvo_response()
1026}
1027
1028#[cfg(feature = "salvo")]
1029impl From<HttpApiProblem> for salvo::Response {
1030    fn from(problem: HttpApiProblem) -> salvo::Response {
1031        problem.to_salvo_response()
1032    }
1033}
1034
1035/// Creates a [tide::Response] from something that can become an
1036/// `HttpApiProblem`.
1037///
1038/// If status is `None` `500 - Internal Server Error` is the
1039/// default.
1040#[cfg(feature = "tide")]
1041pub fn into_tide_response<T: Into<HttpApiProblem>>(what: T) -> tide::Response {
1042    let problem: HttpApiProblem = what.into();
1043    problem.to_tide_response()
1044}
1045
1046#[cfg(feature = "tide")]
1047impl From<HttpApiProblem> for tide::Response {
1048    fn from(problem: HttpApiProblem) -> tide::Response {
1049        problem.to_tide_response()
1050    }
1051}
1052
1053mod custom_http_status_serialization {
1054    use http::StatusCode;
1055    use serde::{Deserialize, Deserializer, Serializer};
1056    use std::convert::TryFrom;
1057
1058    pub fn serialize<S>(status: &Option<StatusCode>, s: S) -> Result<S::Ok, S::Error>
1059    where
1060        S: Serializer,
1061    {
1062        if let Some(ref status_code) = *status {
1063            return s.serialize_u16(status_code.as_u16());
1064        }
1065        s.serialize_none()
1066    }
1067
1068    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<StatusCode>, D::Error>
1069    where
1070        D: Deserializer<'de>,
1071    {
1072        let s: Option<u16> = Option::deserialize(deserializer)?;
1073        if let Some(numeric_status_code) = s {
1074            // If the status code numeral is invalid we simply return None.
1075            // This is a trade off to guarantee that the client can still
1076            // have access to the rest of the problem struct instead of
1077            // having to deal with an error caused by trying to deserialize an invalid status
1078            // code. Additionally the received response still contains a status code.
1079            let status_code = StatusCode::try_from(numeric_status_code).ok();
1080            return Ok(status_code);
1081        }
1082
1083        Ok(None)
1084    }
1085}
1086
1087#[cfg(test)]
1088mod test;