trillium_api/
error.rs

1use crate::ApiConnExt;
2use serde::{Deserialize, Serialize};
3use serde_json::json;
4use std::fmt::Display;
5use trillium::{async_trait, 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
54impl From<serde_json::Error> for Error {
55    fn from(value: serde_json::Error) -> Self {
56        Self::ParseError {
57            path: format!("{}:{}", value.line(), value.column()),
58            message: value.to_string(),
59        }
60    }
61}
62
63impl From<trillium::Error> for Error {
64    fn from(error: trillium::Error) -> Self {
65        match error {
66            trillium::Error::Io(e) => Self::IoError {
67                kind: e.kind().to_string(),
68                message: e.to_string(),
69            },
70
71            other => Self::Other {
72                message: other.to_string(),
73            },
74        }
75    }
76}
77
78impl<E: Display> From<serde_path_to_error::Error<E>> for Error {
79    fn from(e: serde_path_to_error::Error<E>) -> Self {
80        Error::ParseError {
81            path: e.path().to_string(),
82            message: e.to_string(),
83        }
84    }
85}
86
87#[cfg(feature = "forms")]
88impl From<serde_urlencoded::ser::Error> for Error {
89    fn from(value: serde_urlencoded::ser::Error) -> Self {
90        Error::Other {
91            message: value.to_string(),
92        }
93    }
94}
95#[cfg(feature = "forms")]
96impl From<serde_urlencoded::de::Error> for Error {
97    fn from(value: serde_urlencoded::de::Error) -> Self {
98        Error::ParseError {
99            path: "".into(),
100            message: value.to_string(),
101        }
102    }
103}
104
105#[async_trait]
106impl Handler for Error {
107    async fn run(&self, conn: Conn) -> Conn {
108        conn.with_state(self.clone()).halt()
109    }
110
111    async fn before_send(&self, mut conn: Conn) -> Conn {
112        if let Some(error) = conn.take_state::<Self>() {
113            conn.with_json(&json!({ "error": &error }))
114                .with_status(&error)
115        } else {
116            conn
117        }
118    }
119}
120
121impl From<&Error> for Status {
122    fn from(value: &Error) -> Self {
123        match value {
124            Error::ParseError { .. } => Status::UnprocessableEntity,
125            Error::UnsupportedMimeType { .. } | Error::MissingContentType => {
126                Status::UnsupportedMediaType
127            }
128            Error::FailureToNegotiateContent => Status::NotAcceptable,
129            Error::IoError { .. } => Status::BadRequest,
130            _ => Status::InternalServerError,
131        }
132    }
133}
134
135#[async_trait]
136impl crate::FromConn for Error {
137    async fn from_conn(conn: &mut Conn) -> Option<Self> {
138        conn.take_state()
139    }
140}