http_api_problem/lib.rs
1//! # HTTP-API-PROBLEM
2//!
3//! [](https://crates.io/crates/http-api-problem)
4//! [](https://docs.rs/http-api-problem)
5//! [](https://crates.io/crates/http-api-problem)
6//! 
7//! [](https://github.com/chridou/http-api-problem/blob/master/LICENSE-MIT)
8//! [](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;