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-012")]
352impl From<TwirpErrorCode> for tonic_012::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-012")]
379impl From<TwirpError> for tonic_012::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_012::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-012")]
395impl From<tonic_012::Code> for TwirpErrorCode {
396 #[inline]
397 fn from(code: tonic_012::Code) -> TwirpErrorCode {
398 match code {
399 tonic_012::Code::Cancelled => Self::Canceled,
400 tonic_012::Code::Unknown => Self::Unknown,
401 tonic_012::Code::InvalidArgument => Self::InvalidArgument,
402 tonic_012::Code::DeadlineExceeded => Self::DeadlineExceeded,
403 tonic_012::Code::NotFound => Self::NotFound,
404 tonic_012::Code::AlreadyExists => Self::AlreadyExists,
405 tonic_012::Code::PermissionDenied => Self::PermissionDenied,
406 tonic_012::Code::Unauthenticated => Self::Unauthenticated,
407 tonic_012::Code::ResourceExhausted => Self::ResourceExhausted,
408 tonic_012::Code::FailedPrecondition => Self::FailedPrecondition,
409 tonic_012::Code::Aborted => Self::Aborted,
410 tonic_012::Code::OutOfRange => Self::OutOfRange,
411 tonic_012::Code::Unimplemented => Self::Unimplemented,
412 tonic_012::Code::Internal => Self::Internal,
413 tonic_012::Code::Unavailable => Self::Unavailable,
414 tonic_012::Code::DataLoss => Self::Dataloss,
415 tonic_012::Code::Ok => Self::Unknown,
416 }
417 }
418}
419
420#[cfg(feature = "tonic-012")]
421impl From<tonic_012::Status> for TwirpError {
422 #[inline]
423 fn from(status: tonic_012::Status) -> TwirpError {
424 Self::wrap(status.code().into(), status.message().to_string(), status)
425 }
426}
427
428#[cfg(feature = "tonic-013")]
429impl From<TwirpErrorCode> for tonic_013::Code {
430 #[inline]
431 fn from(code: TwirpErrorCode) -> Self {
432 match code {
433 TwirpErrorCode::Canceled => Self::Cancelled,
434 TwirpErrorCode::Unknown => Self::Unknown,
435 TwirpErrorCode::InvalidArgument => Self::InvalidArgument,
436 TwirpErrorCode::Malformed => Self::InvalidArgument,
437 TwirpErrorCode::DeadlineExceeded => Self::DeadlineExceeded,
438 TwirpErrorCode::NotFound => Self::NotFound,
439 TwirpErrorCode::BadRoute => Self::NotFound,
440 TwirpErrorCode::AlreadyExists => Self::AlreadyExists,
441 TwirpErrorCode::PermissionDenied => Self::PermissionDenied,
442 TwirpErrorCode::Unauthenticated => Self::Unauthenticated,
443 TwirpErrorCode::ResourceExhausted => Self::ResourceExhausted,
444 TwirpErrorCode::FailedPrecondition => Self::FailedPrecondition,
445 TwirpErrorCode::Aborted => Self::Aborted,
446 TwirpErrorCode::OutOfRange => Self::OutOfRange,
447 TwirpErrorCode::Unimplemented => Self::Unimplemented,
448 TwirpErrorCode::Internal => Self::Internal,
449 TwirpErrorCode::Unavailable => Self::Unavailable,
450 TwirpErrorCode::Dataloss => Self::DataLoss,
451 }
452 }
453}
454
455#[cfg(feature = "tonic-013")]
456impl From<TwirpError> for tonic_013::Status {
457 #[inline]
458 fn from(error: TwirpError) -> Self {
459 if let Some(source) = &error.source {
460 if let Some(status) = source.downcast_ref::<tonic_013::Status>() {
461 if status.code() == error.code().into() && status.message() == error.message() {
462 return status.clone();
464 }
465 }
466 }
467 Self::new(error.code().into(), error.into_message())
468 }
469}
470
471#[cfg(feature = "tonic-013")]
472impl From<tonic_013::Code> for TwirpErrorCode {
473 #[inline]
474 fn from(code: tonic_013::Code) -> TwirpErrorCode {
475 match code {
476 tonic_013::Code::Cancelled => Self::Canceled,
477 tonic_013::Code::Unknown => Self::Unknown,
478 tonic_013::Code::InvalidArgument => Self::InvalidArgument,
479 tonic_013::Code::DeadlineExceeded => Self::DeadlineExceeded,
480 tonic_013::Code::NotFound => Self::NotFound,
481 tonic_013::Code::AlreadyExists => Self::AlreadyExists,
482 tonic_013::Code::PermissionDenied => Self::PermissionDenied,
483 tonic_013::Code::Unauthenticated => Self::Unauthenticated,
484 tonic_013::Code::ResourceExhausted => Self::ResourceExhausted,
485 tonic_013::Code::FailedPrecondition => Self::FailedPrecondition,
486 tonic_013::Code::Aborted => Self::Aborted,
487 tonic_013::Code::OutOfRange => Self::OutOfRange,
488 tonic_013::Code::Unimplemented => Self::Unimplemented,
489 tonic_013::Code::Internal => Self::Internal,
490 tonic_013::Code::Unavailable => Self::Unavailable,
491 tonic_013::Code::DataLoss => Self::Dataloss,
492 tonic_013::Code::Ok => Self::Unknown,
493 }
494 }
495}
496
497#[cfg(feature = "tonic-013")]
498impl From<tonic_013::Status> for TwirpError {
499 #[inline]
500 fn from(status: tonic_013::Status) -> TwirpError {
501 Self::wrap(status.code().into(), status.message().to_string(), status)
502 }
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 #[cfg(feature = "http")]
509 use std::error::Error;
510
511 #[test]
512 fn test_accessors() {
513 let error = TwirpError::invalid_argument("foo is wrong").with_meta("foo", "bar");
514 assert_eq!(error.code(), TwirpErrorCode::InvalidArgument);
515 assert_eq!(error.message(), "foo is wrong");
516 assert_eq!(error.meta("foo"), Some("bar"));
517 }
518
519 #[cfg(feature = "http")]
520 #[test]
521 fn test_to_response() -> Result<(), Box<dyn Error>> {
522 let object =
523 TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog");
524 let response = http::Response::<Vec<u8>>::from(object);
525 assert_eq!(response.status(), http::StatusCode::FORBIDDEN);
526 assert_eq!(
527 response.headers().get(http::header::CONTENT_TYPE),
528 Some(&http::HeaderValue::from_static("application/json"))
529 );
530 assert_eq!(
531 response.into_body(), b"{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}"
532 );
533 Ok(())
534 }
535
536 #[cfg(feature = "http")]
537 #[test]
538 fn test_from_valid_response() -> Result<(), Box<dyn Error>> {
539 let response = http::Response::builder()
540 .header(http::header::CONTENT_TYPE, "application/json")
541 .body("{\"code\":\"permission_denied\",\"msg\":\"Thou shall not pass\",\"meta\":{\"target\":\"Balrog\"}}")?;
542 assert_eq!(
543 TwirpError::from(response),
544 TwirpError::permission_denied("Thou shall not pass").with_meta("target", "Balrog")
545 );
546 Ok(())
547 }
548
549 #[cfg(feature = "http")]
550 #[test]
551 fn test_from_plain_response() -> Result<(), Box<dyn Error>> {
552 let response = http::Response::builder()
553 .status(http::StatusCode::FORBIDDEN)
554 .body("Thou shall not pass")?;
555 assert_eq!(
556 TwirpError::from(response),
557 TwirpError::permission_denied("Thou shall not pass")
558 );
559 Ok(())
560 }
561
562 #[cfg(feature = "tonic-012")]
563 #[test]
564 fn test_from_tonic_012_status_simple() {
565 assert_eq!(
566 TwirpError::from(tonic_012::Status::not_found("Not found")),
567 TwirpError::not_found("Not found")
568 );
569 }
570
571 #[cfg(feature = "tonic-012")]
572 #[test]
573 fn test_to_tonic_012_status_simple() {
574 let error = TwirpError::not_found("Not found");
575 let status = tonic_012::Status::from(error);
576 assert_eq!(status.code(), tonic_012::Code::NotFound);
577 assert_eq!(status.message(), "Not found");
578 }
579
580 #[cfg(feature = "tonic-012")]
581 #[test]
582 fn test_from_to_tonic_012_status_roundtrip() {
583 let status = tonic_012::Status::with_details(
584 tonic_012::Code::NotFound,
585 "Not found",
586 b"some_dummy_details".to_vec().into(),
587 );
588 let new_status = tonic_012::Status::from(TwirpError::from(status.clone()));
589 assert_eq!(status.code(), new_status.code());
590 assert_eq!(status.message(), new_status.message());
591 assert_eq!(status.details(), new_status.details());
592 }
593
594 #[cfg(feature = "tonic-013")]
595 #[test]
596 fn test_from_tonic_013_status_simple() {
597 assert_eq!(
598 TwirpError::from(tonic_013::Status::not_found("Not found")),
599 TwirpError::not_found("Not found")
600 );
601 }
602
603 #[cfg(feature = "tonic-013")]
604 #[test]
605 fn test_to_tonic_013_status_simple() {
606 let error = TwirpError::not_found("Not found");
607 let status = tonic_013::Status::from(error);
608 assert_eq!(status.code(), tonic_013::Code::NotFound);
609 assert_eq!(status.message(), "Not found");
610 }
611
612 #[cfg(feature = "tonic-013")]
613 #[test]
614 fn test_from_to_tonic_013_status_roundtrip() {
615 let status = tonic_013::Status::with_details(
616 tonic_013::Code::NotFound,
617 "Not found",
618 b"some_dummy_details".to_vec().into(),
619 );
620 let new_status = tonic_013::Status::from(TwirpError::from(status.clone()));
621 assert_eq!(status.code(), new_status.code());
622 assert_eq!(status.message(), new_status.message());
623 assert_eq!(status.details(), new_status.details());
624 }
625}