gateway_runtime/
errors.rs1#[allow(unused_imports)]
24use crate::alloc;
25use crate::BoxBody;
26use bytes::Bytes;
27use http::{Response, StatusCode};
28use http_body_util::{BodyExt, Full};
29use tonic::Code;
30
31#[cfg(feature = "std")]
32use thiserror::Error;
33
34#[cfg(feature = "std")]
39#[derive(Debug, Error)]
40pub enum GatewayError {
41 #[error("failed to serialize/deserialize message: {0}")]
43 Encoding(#[source] Box<dyn std::error::Error + Send + Sync>),
44
45 #[error("upstream gRPC error: {0}")]
47 Upstream(#[from] tonic::Status),
48
49 #[error("HTTP protocol error: {0}")]
51 Http(#[from] http::Error),
52
53 #[error("HTTP error {0}: {1}")]
55 Custom(StatusCode, String),
56
57 #[error("Method not allowed")]
59 MethodNotAllowed,
60
61 #[error("Not found")]
63 NotFound,
64}
65
66#[cfg(not(feature = "std"))]
67#[derive(Debug)]
68pub enum GatewayError {
69 Encoding(alloc::string::String),
70 Upstream(tonic::Status),
71 Http(http::Error),
72 Custom(StatusCode, alloc::string::String),
73 MethodNotAllowed,
74 NotFound,
75}
76
77#[cfg(not(feature = "std"))]
78impl core::fmt::Display for GatewayError {
79 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
80 match self {
81 GatewayError::Encoding(e) => {
82 write!(f, "failed to serialize/deserialize message: {}", e)
83 }
84 GatewayError::Upstream(s) => write!(f, "upstream gRPC error: {}", s),
85 GatewayError::Http(e) => write!(f, "HTTP protocol error: {}", e),
86 GatewayError::Custom(c, m) => write!(f, "HTTP error {}: {}", c, m),
87 GatewayError::MethodNotAllowed => write!(f, "Method not allowed"),
88 GatewayError::NotFound => write!(f, "Not found"),
89 }
90 }
91}
92
93pub fn map_code_to_status(code: Code) -> StatusCode {
103 match code {
104 Code::Ok => StatusCode::OK,
105 Code::Cancelled => StatusCode::from_u16(499).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
106 Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
107 Code::InvalidArgument => StatusCode::BAD_REQUEST,
108 Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
109 Code::NotFound => StatusCode::NOT_FOUND,
110 Code::AlreadyExists => StatusCode::CONFLICT,
111 Code::PermissionDenied => StatusCode::FORBIDDEN,
112 Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
113 Code::FailedPrecondition => StatusCode::BAD_REQUEST,
114 Code::Aborted => StatusCode::CONFLICT,
115 Code::OutOfRange => StatusCode::BAD_REQUEST,
116 Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
117 Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
118 Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
119 Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
120 Code::Unauthenticated => StatusCode::UNAUTHORIZED,
121 }
122}
123
124pub fn handle_error(status: tonic::Status) -> Response<BoxBody> {
135 let http_code = map_code_to_status(status.code());
136
137 let body = serde_json::json!({
139 "code": status.code() as i32,
140 "message": status.message(),
141 "details": []
142 });
143
144 let body_bytes = serde_json::to_vec(&body).unwrap_or_default();
145 let full_body =
146 BodyExt::boxed_unsync(Full::new(Bytes::from(body_bytes)).map_err(|never| match never {}));
147
148 Response::builder()
149 .status(http_code)
150 .header("Content-Type", "application/json")
151 .body(full_body)
152 .unwrap_or_else(|_| {
153 Response::new(BodyExt::boxed_unsync(
154 Full::new(Bytes::new()).map_err(|never| match never {}),
155 ))
156 })
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_map_code_to_status() {
165 assert_eq!(map_code_to_status(Code::Ok), StatusCode::OK);
166 assert_eq!(
167 map_code_to_status(Code::InvalidArgument),
168 StatusCode::BAD_REQUEST
169 );
170 assert_eq!(map_code_to_status(Code::NotFound), StatusCode::NOT_FOUND);
171 assert_eq!(
172 map_code_to_status(Code::Unauthenticated),
173 StatusCode::UNAUTHORIZED
174 );
175 assert_eq!(
176 map_code_to_status(Code::PermissionDenied),
177 StatusCode::FORBIDDEN
178 );
179 assert_eq!(
180 map_code_to_status(Code::Unavailable),
181 StatusCode::SERVICE_UNAVAILABLE
182 );
183 assert_eq!(
184 map_code_to_status(Code::Internal),
185 StatusCode::INTERNAL_SERVER_ERROR
186 );
187 }
188
189 #[test]
190 fn test_gateway_error_display() {
191 assert_eq!(GatewayError::NotFound.to_string(), "Not found");
192 assert_eq!(
193 GatewayError::MethodNotAllowed.to_string(),
194 "Method not allowed"
195 );
196 }
197
198 #[test]
199 fn test_gateway_error_http_from() {
200 let res = http::Response::builder().header("bad", "\n").body(()); let err = res.unwrap_err();
203 let ge: GatewayError = err.into();
204 match ge {
205 GatewayError::Http(_) => {}
206 _ => panic!("Expected Http error"),
207 }
208 }
209
210 #[test]
211 fn test_gateway_error_upstream_from() {
212 let status = tonic::Status::new(Code::Internal, "msg");
213 let ge: GatewayError = status.into();
214 matches!(ge, GatewayError::Upstream(_));
215 }
216
217 #[test]
218 fn test_handle_error_ok() {
219 let status = tonic::Status::ok("ok");
221 let resp = handle_error(status);
222 assert_eq!(resp.status(), StatusCode::OK);
223 }
224
225 #[test]
226 fn test_handle_error_json_body() {
227 let status = tonic::Status::new(Code::NotFound, "missing");
228 let resp = handle_error(status);
229 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
230 assert_eq!(
231 resp.headers().get("content-type").unwrap(),
232 "application/json"
233 );
234 }
236
237 #[test]
238 fn test_map_code_cancelled() {
239 let s = map_code_to_status(Code::Cancelled);
241 assert!(s.as_u16() == 499 || s == StatusCode::INTERNAL_SERVER_ERROR);
242 }
243}