tachyon_web/http/
error.rs1use bytes::Bytes;
4use hyper::{Response, StatusCode};
5
6use crate::http::response::IntoResponse;
7
8pub type Result<T, E = Error> = std::result::Result<T, E>;
10
11#[derive(Debug, Clone)]
13pub enum Error {
14 Rejection {
16 status: StatusCode,
18 message: String,
20 },
21 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 #[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}