Skip to main content

tachyon_web/http/
error.rs

1//! Error types for the Tachyon-Web framework.
2
3use bytes::Bytes;
4use hyper::{Response, StatusCode};
5
6use crate::http::response::IntoResponse;
7
8/// A specialized Result type for Tachyon-Web operations.
9pub type Result<T, E = Error> = std::result::Result<T, E>;
10
11/// Represents errors that can occur during request processing, routing, or extraction.
12#[derive(Debug, Clone)]
13pub enum Error {
14    /// A client error resulting in an HTTP status code and a descriptive message.
15    Rejection {
16        /// The HTTP status code to return.
17        status: StatusCode,
18        /// The descriptive error message.
19        message: String,
20    },
21    /// An internal server error.
22    Internal(String),
23}
24
25impl std::fmt::Display for Error {
26    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27        match self {
28            Self::Rejection { status, message } => write!(f, "Rejection ({status}): {message}"),
29            Self::Internal(msg) => write!(f, "Internal Error: {msg}"),
30        }
31    }
32}
33
34impl std::error::Error for Error {}
35
36impl From<std::io::Error> for Error {
37    fn from(e: std::io::Error) -> Self {
38        Self::Internal(e.to_string())
39    }
40}
41
42impl From<std::convert::Infallible> for Error {
43    fn from(e: std::convert::Infallible) -> Self {
44        match e {}
45    }
46}
47
48impl From<hyper::Error> for Error {
49    fn from(e: hyper::Error) -> Self {
50        Self::Internal(e.to_string())
51    }
52}
53
54impl IntoResponse for Error {
55    fn into_response(self) -> Response<crate::http::response::Body> {
56        match self {
57            Self::Rejection { status, message } => Response::builder()
58                .status(status)
59                .header("content-type", "text/plain; charset=utf-8")
60                .body(crate::http::response::Body::full(Bytes::from(message)))
61                .unwrap_or_else(|_| Response::new(crate::http::response::Body::empty())),
62            Self::Internal(msg) => {
63                tracing::error!("internal error: {msg}");
64                Response::builder()
65                    .status(StatusCode::INTERNAL_SERVER_ERROR)
66                    .header("content-type", "text/plain; charset=utf-8")
67                    .body(crate::http::response::Body::full(Bytes::from_static(
68                        b"Internal Server Error",
69                    )))
70                    .unwrap_or_else(|_| Response::new(crate::http::response::Body::empty()))
71            }
72        }
73    }
74}
75
76#[cfg(test)]
77mod tests {
78    use super::*;
79
80    #[test]
81    fn test_error_display() {
82        let err1 = Error::Rejection {
83            status: StatusCode::BAD_REQUEST,
84            message: "bad".to_string(),
85        };
86        assert_eq!(err1.to_string(), "Rejection (400 Bad Request): bad");
87
88        let err2 = Error::Internal("oops".to_string());
89        assert_eq!(err2.to_string(), "Internal Error: oops");
90    }
91
92    #[test]
93    fn test_error_into_response() {
94        let err1 = Error::Rejection {
95            status: StatusCode::NOT_FOUND,
96            message: "not found".to_string(),
97        };
98        let resp1 = err1.into_response();
99        assert_eq!(resp1.status(), StatusCode::NOT_FOUND);
100
101        let err2 = Error::Internal("failure".to_string());
102        let resp2 = err2.into_response();
103        assert_eq!(resp2.status(), StatusCode::INTERNAL_SERVER_ERROR);
104    }
105
106    #[test]
107    fn io_error_converts_to_internal() {
108        let io_err = std::io::Error::other("disk on fire");
109        let err: Error = io_err.into();
110        assert!(matches!(err, Error::Internal(msg) if msg.contains("disk on fire")));
111    }
112
113    /// `hyper::Error` has no public constructor, so the only way to get a real one is to
114    /// actually drive a connection into a parse error — a malformed request line over an
115    /// in-memory duplex pipe, no real socket needed.
116    #[cfg(feature = "http1")]
117    #[tokio::test]
118    async fn hyper_error_converts_to_internal() {
119        use tokio::io::AsyncWriteExt;
120
121        let (mut client_io, server_io) = tokio::io::duplex(1024);
122        let svc = hyper::service::service_fn(|_req: hyper::Request<hyper::body::Incoming>| async {
123            Ok::<_, std::io::Error>(Response::new(crate::http::response::Body::empty()))
124        });
125        let server = tokio::spawn(async move {
126            hyper::server::conn::http1::Builder::new()
127                .serve_connection(hyper_util::rt::TokioIo::new(server_io), svc)
128                .await
129        });
130
131        client_io
132            .write_all(b"not a valid http request at all\r\n\r\n")
133            .await
134            .expect("write garbage");
135        client_io.shutdown().await.expect("shutdown write half");
136
137        let hyper_err = server
138            .await
139            .expect("server task join")
140            .expect_err("malformed request line must fail to parse");
141        let err: Error = hyper_err.into();
142        assert!(matches!(err, Error::Internal(_)));
143    }
144}