1#![doc = include_str!("../README.md")]
2#![doc(
3 test(attr(deny(warnings))),
4 html_favicon_url = "https://raw.githubusercontent.com/helsing-ai/twurst/main/docs/img/twurst.png",
5 html_logo_url = "https://raw.githubusercontent.com/helsing-ai/twurst/main/docs/img/twurst.png"
6)]
7#![cfg_attr(docsrs, feature(doc_auto_cfg))]
8
9use std::collections::HashMap;
10use std::error::Error;
11use std::fmt;
12use std::sync::Arc;
13
14#[derive(Clone, Debug)]
29#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
30pub struct TwirpError {
31 code: TwirpErrorCode,
33 msg: String,
35 #[cfg_attr(
37 feature = "serde",
38 serde(default, skip_serializing_if = "HashMap::is_empty")
39 )]
40 meta: HashMap<String, String>,
41 #[cfg_attr(feature = "serde", serde(default, skip))]
42 source: Option<Arc<dyn Error + Send + Sync>>,
43}
44
45impl TwirpError {
46 #[inline]
47 pub fn code(&self) -> TwirpErrorCode {
48 self.code
49 }
50
51 #[inline]
52 pub fn message(&self) -> &str {
53 &self.msg
54 }
55
56 #[inline]
57 pub fn into_message(self) -> String {
58 self.msg
59 }
60
61 #[inline]
63 pub fn meta(&self, key: &str) -> Option<&str> {
64 self.meta.get(key).map(|s| s.as_str())
65 }
66
67 #[inline]
69 pub fn meta_iter(&self) -> impl Iterator<Item = (&str, &str)> {
70 self.meta.iter().map(|(k, v)| (k.as_str(), v.as_str()))
71 }
72
73 #[inline]
74 pub fn new(code: TwirpErrorCode, msg: impl Into<String>) -> Self {
75 Self {
76 code,
77 msg: msg.into(),
78 meta: HashMap::new(),
79 source: None,
80 }
81 }
82
83 #[inline]
84 pub fn wrap(
85 code: TwirpErrorCode,
86 msg: impl Into<String>,
87 e: impl Error + Send + Sync + 'static,
88 ) -> Self {
89 Self {
90 code,
91 msg: msg.into(),
92 meta: HashMap::new(),
93 source: Some(Arc::new(e)),
94 }
95 }
96
97 #[inline]
99 pub fn with_meta(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
100 self.meta.insert(key.into(), value.into());
101 self
102 }
103
104 #[inline]
105 pub fn aborted(msg: impl Into<String>) -> Self {
106 Self::new(TwirpErrorCode::Aborted, msg)
107 }
108
109 #[inline]
110 pub fn already_exists(msg: impl Into<String>) -> Self {
111 Self::new(TwirpErrorCode::AlreadyExists, msg)
112 }
113
114 #[inline]
115 pub fn canceled(msg: impl Into<String>) -> Self {
116 Self::new(TwirpErrorCode::Canceled, msg)
117 }
118
119 #[inline]
120 pub fn dataloss(msg: impl Into<String>) -> Self {
121 Self::new(TwirpErrorCode::Dataloss, msg)
122 }
123
124 #[inline]
125 pub fn invalid_argument(msg: impl Into<String>) -> Self {
126 Self::new(TwirpErrorCode::InvalidArgument, msg)
127 }
128
129 #[inline]
130 pub fn internal(msg: impl Into<String>) -> Self {
131 Self::new(TwirpErrorCode::Internal, msg)
132 }
133
134 #[inline]
135 pub fn deadline_exceeded(msg: impl Into<String>) -> Self {
136 Self::new(TwirpErrorCode::DeadlineExceeded, msg)
137 }
138
139 #[inline]
140 pub fn failed_precondition(msg: impl Into<String>) -> Self {
141 Self::new(TwirpErrorCode::FailedPrecondition, msg)
142 }
143
144 #[inline]
145 pub fn malformed(msg: impl Into<String>) -> Self {
146 Self::new(TwirpErrorCode::Malformed, msg)
147 }
148
149 #[inline]
150 pub fn not_found(msg: impl Into<String>) -> Self {
151 Self::new(TwirpErrorCode::NotFound, msg)
152 }
153
154 #[inline]
155 pub fn out_of_range(msg: impl Into<String>) -> Self {
156 Self::new(TwirpErrorCode::OutOfRange, msg)
157 }
158
159 #[inline]
160 pub fn permission_denied(msg: impl Into<String>) -> Self {
161 Self::new(TwirpErrorCode::PermissionDenied, msg)
162 }
163
164 #[inline]
165 pub fn required_argument(msg: impl Into<String>) -> Self {
166 Self::invalid_argument(msg)
167 }
168
169 #[inline]
170 pub fn resource_exhausted(msg: impl Into<String>) -> Self {
171 Self::new(TwirpErrorCode::ResourceExhausted, msg)
172 }
173
174 #[inline]
175 pub fn unauthenticated(msg: impl Into<String>) -> Self {
176 Self::new(TwirpErrorCode::Unauthenticated, msg)
177 }
178
179 #[inline]
180 pub fn unavailable(msg: impl Into<String>) -> Self {
181 Self::new(TwirpErrorCode::Unavailable, msg)
182 }
183
184 #[inline]
185 pub fn unimplemented(msg: impl Into<String>) -> Self {
186 Self::new(TwirpErrorCode::Unimplemented, msg)
187 }
188}
189
190impl fmt::Display for TwirpError {
191 #[inline]
192 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193 write!(f, "Twirp {:?} error: {}", self.code, self.msg)
194 }
195}
196
197impl Error for TwirpError {
198 #[inline]
199 fn source(&self) -> Option<&(dyn Error + 'static)> {
200 Some(self.source.as_ref()?)
201 }
202}
203
204impl PartialEq for TwirpError {
205 #[inline]
206 fn eq(&self, other: &Self) -> bool {
207 self.code == other.code && self.msg == other.msg && self.meta == other.meta
208 }
209}
210
211impl Eq for TwirpError {}
212
213#[derive(Clone, Copy, Debug, PartialEq, Eq)]
215#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
216#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
217pub enum TwirpErrorCode {
218 Canceled,
220 Unknown,
222 InvalidArgument,
224 Malformed,
226 DeadlineExceeded,
228 NotFound,
230 BadRoute,
232 AlreadyExists,
234 PermissionDenied,
236 Unauthenticated,
238 ResourceExhausted,
240 FailedPrecondition,
242 Aborted,
244 OutOfRange,
246 Unimplemented,
248 Internal,
250 Unavailable,
252 Dataloss,
254}
255
256#[cfg(feature = "http")]
258impl From<TwirpErrorCode> for http::StatusCode {
259 #[inline]
260 fn from(code: TwirpErrorCode) -> Self {
261 match code {
262 TwirpErrorCode::Canceled => Self::REQUEST_TIMEOUT,
263 TwirpErrorCode::Unknown => Self::INTERNAL_SERVER_ERROR,
264 TwirpErrorCode::InvalidArgument => Self::BAD_REQUEST,
265 TwirpErrorCode::Malformed => Self::BAD_REQUEST,
266 TwirpErrorCode::DeadlineExceeded => Self::REQUEST_TIMEOUT,
267 TwirpErrorCode::NotFound => Self::NOT_FOUND,
268 TwirpErrorCode::BadRoute => Self::NOT_FOUND,
269 TwirpErrorCode::AlreadyExists => Self::CONFLICT,
270 TwirpErrorCode::PermissionDenied => Self::FORBIDDEN,
271 TwirpErrorCode::Unauthenticated => Self::UNAUTHORIZED,
272 TwirpErrorCode::ResourceExhausted => Self::TOO_MANY_REQUESTS,
273 TwirpErrorCode::FailedPrecondition => Self::PRECONDITION_FAILED,
274 TwirpErrorCode::Aborted => Self::CONFLICT,
275 TwirpErrorCode::OutOfRange => Self::BAD_REQUEST,
276 TwirpErrorCode::Unimplemented => Self::NOT_IMPLEMENTED,
277 TwirpErrorCode::Internal => Self::INTERNAL_SERVER_ERROR,
278 TwirpErrorCode::Unavailable => Self::SERVICE_UNAVAILABLE,
279 TwirpErrorCode::Dataloss => Self::SERVICE_UNAVAILABLE,
280 }
281 }
282}
283
284#[cfg(feature = "http")]
285impl<B: From<String>> From<TwirpError> for http::Response<B> {
286 fn from(error: TwirpError) -> Self {
287 let json = serde_json::to_string(&error).unwrap();
288 http::Response::builder()
289 .status(error.code)
290 .header(http::header::CONTENT_TYPE, "application/json")
291 .extension(error)
292 .body(json.into())
293 .unwrap()
294 }
295}
296
297#[cfg(feature = "http")]
298impl<B: AsRef<[u8]>> From<http::Response<B>> for TwirpError {
299 fn from(response: http::Response<B>) -> Self {
300 if let Some(error) = response.extensions().get::<Self>() {
301 return error.clone();
303 }
304 let status = response.status();
306 let body = response.into_body();
307 if let Ok(error) = serde_json::from_slice::<TwirpError>(body.as_ref()) {
308 return error;
310 }
311 let code = if status == http::StatusCode::REQUEST_TIMEOUT {
313 TwirpErrorCode::DeadlineExceeded
314 } else if status == http::StatusCode::FORBIDDEN {
315 TwirpErrorCode::PermissionDenied
316 } else if status == http::StatusCode::UNAUTHORIZED {
317 TwirpErrorCode::Unauthenticated
318 } else if status == http::StatusCode::TOO_MANY_REQUESTS {
319 TwirpErrorCode::ResourceExhausted
320 } else if status == http::StatusCode::PRECONDITION_FAILED {
321 TwirpErrorCode::FailedPrecondition
322 } else if status == http::StatusCode::NOT_IMPLEMENTED {
323 TwirpErrorCode::Unimplemented
324 } else if status == http::StatusCode::TOO_MANY_REQUESTS
325 || status == http::StatusCode::BAD_GATEWAY
326 || status == http::StatusCode::SERVICE_UNAVAILABLE
327 || status == http::StatusCode::GATEWAY_TIMEOUT
328 {
329 TwirpErrorCode::Unavailable
330 } else if status == http::StatusCode::NOT_FOUND {
331 TwirpErrorCode::NotFound
332 } else if status.is_server_error() {
333 TwirpErrorCode::Internal
334 } else if status.is_client_error() {
335 TwirpErrorCode::Malformed
336 } else {
337 TwirpErrorCode::Unknown
338 };
339 TwirpError::new(code, String::from_utf8_lossy(body.as_ref()))
340 }
341}
342
343#[cfg(feature = "axum-08")]
344impl axum_core_05::response::IntoResponse for TwirpError {
345 #[inline]
346 fn into_response(self) -> axum_core_05::response::Response {
347 self.into()
348 }
349}
350
351#[cfg(feature = "tonic-014")]
352impl From<TwirpErrorCode> for tonic_014::Code {
353 #[inline]
354 fn from(code: TwirpErrorCode) -> Self {
355 match code {
356 TwirpErrorCode::Canceled => Self::Cancelled,
357 TwirpErrorCode::Unknown => Self::Unknown,
358 TwirpErrorCode::InvalidArgument => Self::InvalidArgument,
359 TwirpErrorCode::Malformed => Self::InvalidArgument,
360 TwirpErrorCode::DeadlineExceeded => Self::DeadlineExceeded,
361 TwirpErrorCode::NotFound => Self::NotFound,
362 TwirpErrorCode::BadRoute => Self::NotFound,
363 TwirpErrorCode::AlreadyExists => Self::AlreadyExists,
364 TwirpErrorCode::PermissionDenied => Self::PermissionDenied,
365 TwirpErrorCode::Unauthenticated => Self::Unauthenticated,
366 TwirpErrorCode::ResourceExhausted => Self::ResourceExhausted,
367 TwirpErrorCode::FailedPrecondition => Self::FailedPrecondition,
368 TwirpErrorCode::Aborted => Self::Aborted,
369 TwirpErrorCode::OutOfRange => Self::OutOfRange,
370 TwirpErrorCode::Unimplemented => Self::Unimplemented,
371 TwirpErrorCode::Internal => Self::Internal,
372 TwirpErrorCode::Unavailable => Self::Unavailable,
373 TwirpErrorCode::Dataloss => Self::DataLoss,
374 }
375 }
376}
377
378#[cfg(feature = "tonic-014")]
379impl From<TwirpError> for tonic_014::Status {
380 #[inline]
381 fn from(error: TwirpError) -> Self {
382 if let Some(source) = &error.source {
383 if let Some(status) = source.downcast_ref::<tonic_014::Status>() {
384 if status.code() == error.code().into() && status.message() == error.message() {
385 return status.clone();
387 }
388 }
389 }
390 Self::new(error.code().into(), error.into_message())
391 }
392}
393
394#[cfg(feature = "tonic-014")]
395impl From<tonic_014::Code> for TwirpErrorCode {
396 #[inline]
397 fn from(code: tonic_014::Code) -> TwirpErrorCode {
398 match code {
399 tonic_014::Code::Cancelled => Self::Canceled,
400 tonic_014::Code::Unknown => Self::Unknown,
401 tonic_014::Code::InvalidArgument => Self::InvalidArgument,
402 tonic_014::Code::DeadlineExceeded => Self::DeadlineExceeded,
403 tonic_014::Code::NotFound => Self::NotFound,
404 tonic_014::Code::AlreadyExists => Self::AlreadyExists,
405 tonic_014::Code::PermissionDenied => Self::PermissionDenied,
406 tonic_014::Code::Unauthenticated => Self::Unauthenticated,
407 tonic_014::Code::ResourceExhausted => Self::ResourceExhausted,
408 tonic_014::Code::FailedPrecondition => Self::FailedPrecondition,
409 tonic_014::Code::Aborted => Self::Aborted,
410 tonic_014::Code::OutOfRange => Self::OutOfRange,
411 tonic_014::Code::Unimplemented => Self::Unimplemented,
412 tonic_014::Code::Internal => Self::Internal,
413 tonic_014::Code::Unavailable => Self::Unavailable,
414 tonic_014::Code::DataLoss => Self::Dataloss,
415 tonic_014::Code::Ok => Self::Unknown,
416 }
417 }
418}
419
420#[cfg(feature = "tonic-014")]
421impl From<tonic_014::Status> for TwirpError {
422 #[inline]
423 fn from(status: tonic_014::Status) -> TwirpError {
424 Self::wrap(status.code().into(), status.message().to_string(), status)
425 }
426}
427
428#[cfg(test)]
429mod tests {
430 use super::*;
431 #[cfg(feature = "http")]
432 use std::error::Error;
433
434 #[test]
435 fn test_accessors() {
436 let error = TwirpError::invalid_argument("foo is wrong").with_meta("foo", "bar");
437 assert_eq!(error.code(), TwirpErrorCode::InvalidArgument);
438 assert_eq!(error.message(), "foo is wrong");
439 assert_eq!(error.meta("foo"), Some("bar"));
440 }
441
442 #[cfg(feature = "http")]
443 #[test]
444 fn test_to_response() -> Result<(), Box<dyn Error>> {
445 let object =
446 TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog");
447 let response = http::Response::<Vec<u8>>::from(object);
448 assert_eq!(response.status(), http::StatusCode::FORBIDDEN);
449 assert_eq!(
450 response.headers().get(http::header::CONTENT_TYPE),
451 Some(&http::HeaderValue::from_static("application/json"))
452 );
453 assert_eq!(
454 response.into_body(), b"{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}"
455 );
456 Ok(())
457 }
458
459 #[cfg(feature = "http")]
460 #[test]
461 fn test_from_valid_response() -> Result<(), Box<dyn Error>> {
462 let response = http::Response::builder()
463 .header(http::header::CONTENT_TYPE, "application/json")
464 .body("{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}")?;
465 assert_eq!(
466 TwirpError::from(response),
467 TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog")
468 );
469 Ok(())
470 }
471
472 #[cfg(feature = "http")]
473 #[test]
474 fn test_from_plain_response() -> Result<(), Box<dyn Error>> {
475 let response = http::Response::builder()
476 .status(http::StatusCode::FORBIDDEN)
477 .body("Thou shall not pass")?;
478 assert_eq!(
479 TwirpError::from(response),
480 TwirpError::permission_denied("Thou shall not pass")
481 );
482 Ok(())
483 }
484
485 #[cfg(feature = "tonic-014")]
486 #[test]
487 fn test_from_tonic_014_status_simple() {
488 assert_eq!(
489 TwirpError::from(tonic_014::Status::not_found("Not found")),
490 TwirpError::not_found("Not found")
491 );
492 }
493
494 #[cfg(feature = "tonic-014")]
495 #[test]
496 fn test_to_tonic_014_status_simple() {
497 let error = TwirpError::not_found("Not found");
498 let status = tonic_014::Status::from(error);
499 assert_eq!(status.code(), tonic_014::Code::NotFound);
500 assert_eq!(status.message(), "Not found");
501 }
502
503 #[cfg(feature = "tonic-014")]
504 #[test]
505 fn test_from_to_tonic_014_status_roundtrip() {
506 let status = tonic_014::Status::with_details(
507 tonic_014::Code::NotFound,
508 "Not found",
509 b"some_dummy_details".to_vec().into(),
510 );
511 let new_status = tonic_014::Status::from(TwirpError::from(status.clone()));
512 assert_eq!(status.code(), new_status.code());
513 assert_eq!(status.message(), new_status.message());
514 assert_eq!(status.details(), new_status.details());
515 }
516}