Skip to main content

trillium_api/
error.rs

1#[cfg(any(feature = "serde_json", feature = "sonic-rs"))]
2use crate::ApiConnExt;
3use serde::{Deserialize, Serialize};
4use std::fmt::Display;
5use trillium::{Conn, Handler, Status};
6
7/// A serde-serializable error
8#[derive(Serialize, Deserialize, Debug, Clone, thiserror::Error)]
9#[serde(tag = "type", rename_all = "snake_case")]
10pub enum Error {
11    /// An error occurred in parsing the provided body content
12    #[error("Parse error at {path}: {message}")]
13    ParseError {
14        /// the path of the parse error, as provided by [`serde_path_to_error`]
15        path: String,
16        /// the contents of the error
17        message: String,
18    },
19    /// A transmission error occurred in the connection to the http
20    /// client
21    #[error("I/O error type {kind}: {message}")]
22    IoError {
23        /// stringified [`std::io::ErrorKind`]
24        kind: String,
25        /// stringified [`std::io::Error`]
26        message: String,
27    },
28    /// The client provided a content type that this library does not
29    /// yet support
30    #[error("Unsupported mime type: {mime_type}")]
31    UnsupportedMimeType {
32        /// the unsupported mime type
33        mime_type: String,
34    },
35    /// The client did not provide a content-type
36    #[error("Missing content type")]
37    MissingContentType,
38    /// Miscellaneous other errors -- please open an issue on
39    /// trillium-api if you find yourself parsing the contents of
40    /// this.
41    #[error("{message}")]
42    Other {
43        /// A stringified error
44        message: String,
45    },
46
47    #[error("No negotiated mime type")]
48    /// we were unable to find a content type that matches the Accept
49    /// header. Please open an issue if you'd like an additional
50    /// format to be supported
51    FailureToNegotiateContent,
52}
53
54#[cfg(feature = "serde_json")]
55#[cfg_attr(docsrs, doc(cfg(feature = "serde_json")))]
56impl From<serde_json::Error> for Error {
57    fn from(value: serde_json::Error) -> Self {
58        Self::ParseError {
59            path: format!("{}:{}", value.line(), value.column()),
60            message: value.to_string(),
61        }
62    }
63}
64
65#[cfg(feature = "sonic-rs")]
66#[cfg_attr(docsrs, doc(cfg(feature = "sonic-rs")))]
67impl From<sonic_rs::Error> for Error {
68    fn from(value: sonic_rs::Error) -> Self {
69        Self::ParseError {
70            path: format!("{}:{}", value.line(), value.column()),
71            message: value.to_string(),
72        }
73    }
74}
75
76impl From<trillium::Error> for Error {
77    fn from(error: trillium::Error) -> Self {
78        match error {
79            trillium::Error::Io(e) => Self::IoError {
80                kind: e.kind().to_string(),
81                message: e.to_string(),
82            },
83
84            other => Self::Other {
85                message: other.to_string(),
86            },
87        }
88    }
89}
90
91impl<E: Display> From<serde_path_to_error::Error<E>> for Error {
92    fn from(e: serde_path_to_error::Error<E>) -> Self {
93        Error::ParseError {
94            path: e.path().to_string(),
95            message: e.to_string(),
96        }
97    }
98}
99
100#[cfg(feature = "forms")]
101#[cfg_attr(docsrs, doc(cfg(feature = "forms")))]
102impl From<serde_urlencoded::ser::Error> for Error {
103    fn from(value: serde_urlencoded::ser::Error) -> Self {
104        Error::Other {
105            message: value.to_string(),
106        }
107    }
108}
109#[cfg(feature = "forms")]
110impl From<serde_urlencoded::de::Error> for Error {
111    fn from(value: serde_urlencoded::de::Error) -> Self {
112        Error::ParseError {
113            path: "".into(),
114            message: value.to_string(),
115        }
116    }
117}
118
119impl Handler for Error {
120    async fn run(&self, conn: Conn) -> Conn {
121        conn.with_state(self.clone()).halt()
122    }
123
124    #[cfg(any(feature = "serde_json", feature = "sonic-rs"))]
125    async fn before_send(&self, mut conn: Conn) -> Conn {
126        if let Some(error) = conn.take_state::<Self>() {
127            conn.with_json(&crate::json!({ "error": &error }))
128                .with_status(&error)
129        } else {
130            conn
131        }
132    }
133
134    #[cfg(not(any(feature = "serde_json", feature = "sonic-rs")))]
135    async fn before_send(&self, mut conn: Conn) -> Conn {
136        if let Some(error) = conn.take_state::<Self>() {
137            conn.with_body(error.to_string()).with_status(&error)
138        } else {
139            conn
140        }
141    }
142}
143
144impl From<&Error> for Status {
145    fn from(value: &Error) -> Self {
146        match value {
147            Error::ParseError { .. } => Status::UnprocessableEntity,
148            Error::UnsupportedMimeType { .. } | Error::MissingContentType => {
149                Status::UnsupportedMediaType
150            }
151            Error::FailureToNegotiateContent => Status::NotAcceptable,
152            Error::IoError { .. } => Status::BadRequest,
153            _ => Status::InternalServerError,
154        }
155    }
156}
157
158impl crate::FromConn for Error {
159    async fn from_conn(conn: &mut Conn) -> Option<Self> {
160        conn.take_state()
161    }
162}