1use std::sync::Arc;
7
8use bytes::Bytes;
9use http::StatusCode;
10use serde::Deserialize;
11use serde::Serialize;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19#[non_exhaustive]
20pub enum ErrorCode {
21 Canceled,
23 Unknown,
25 InvalidArgument,
27 DeadlineExceeded,
29 NotFound,
31 AlreadyExists,
33 PermissionDenied,
35 ResourceExhausted,
37 FailedPrecondition,
39 Aborted,
41 OutOfRange,
43 Unimplemented,
45 Internal,
47 Unavailable,
49 DataLoss,
51 Unauthenticated,
53}
54
55impl ErrorCode {
56 #[inline]
58 pub fn as_str(&self) -> &'static str {
59 match self {
60 Self::Canceled => "canceled",
61 Self::Unknown => "unknown",
62 Self::InvalidArgument => "invalid_argument",
63 Self::DeadlineExceeded => "deadline_exceeded",
64 Self::NotFound => "not_found",
65 Self::AlreadyExists => "already_exists",
66 Self::PermissionDenied => "permission_denied",
67 Self::ResourceExhausted => "resource_exhausted",
68 Self::FailedPrecondition => "failed_precondition",
69 Self::Aborted => "aborted",
70 Self::OutOfRange => "out_of_range",
71 Self::Unimplemented => "unimplemented",
72 Self::Internal => "internal",
73 Self::Unavailable => "unavailable",
74 Self::DataLoss => "data_loss",
75 Self::Unauthenticated => "unauthenticated",
76 }
77 }
78
79 #[inline]
81 pub fn http_status(&self) -> StatusCode {
82 match self {
83 Self::Canceled => {
85 StatusCode::from_u16(499).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
87 }
88 Self::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
89 Self::InvalidArgument => StatusCode::BAD_REQUEST,
90 Self::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
91 Self::NotFound => StatusCode::NOT_FOUND,
92 Self::AlreadyExists => StatusCode::CONFLICT,
93 Self::PermissionDenied => StatusCode::FORBIDDEN,
94 Self::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
95 Self::FailedPrecondition => StatusCode::BAD_REQUEST,
96 Self::Aborted => StatusCode::CONFLICT,
97 Self::OutOfRange => StatusCode::BAD_REQUEST,
98 Self::Unimplemented => StatusCode::NOT_IMPLEMENTED,
99 Self::Internal => StatusCode::INTERNAL_SERVER_ERROR,
100 Self::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
101 Self::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
102 Self::Unauthenticated => StatusCode::UNAUTHORIZED,
103 }
104 }
105}
106
107impl ErrorCode {
108 #[inline]
110 pub fn grpc_code(&self) -> u32 {
111 match self {
112 Self::Canceled => 1,
113 Self::Unknown => 2,
114 Self::InvalidArgument => 3,
115 Self::DeadlineExceeded => 4,
116 Self::NotFound => 5,
117 Self::AlreadyExists => 6,
118 Self::PermissionDenied => 7,
119 Self::ResourceExhausted => 8,
120 Self::FailedPrecondition => 9,
121 Self::Aborted => 10,
122 Self::OutOfRange => 11,
123 Self::Unimplemented => 12,
124 Self::Internal => 13,
125 Self::Unavailable => 14,
126 Self::DataLoss => 15,
127 Self::Unauthenticated => 16,
128 }
129 }
130
131 #[inline]
136 pub fn from_grpc_code(code: u32) -> Option<Self> {
137 match code {
138 1 => Some(Self::Canceled),
139 2 => Some(Self::Unknown),
140 3 => Some(Self::InvalidArgument),
141 4 => Some(Self::DeadlineExceeded),
142 5 => Some(Self::NotFound),
143 6 => Some(Self::AlreadyExists),
144 7 => Some(Self::PermissionDenied),
145 8 => Some(Self::ResourceExhausted),
146 9 => Some(Self::FailedPrecondition),
147 10 => Some(Self::Aborted),
148 11 => Some(Self::OutOfRange),
149 12 => Some(Self::Unimplemented),
150 13 => Some(Self::Internal),
151 14 => Some(Self::Unavailable),
152 15 => Some(Self::DataLoss),
153 16 => Some(Self::Unauthenticated),
154 _ => None,
155 }
156 }
157}
158
159impl std::str::FromStr for ErrorCode {
160 type Err = ();
161
162 fn from_str(s: &str) -> Result<Self, Self::Err> {
166 match s {
167 "canceled" => Ok(Self::Canceled),
168 "unknown" => Ok(Self::Unknown),
169 "invalid_argument" => Ok(Self::InvalidArgument),
170 "deadline_exceeded" => Ok(Self::DeadlineExceeded),
171 "not_found" => Ok(Self::NotFound),
172 "already_exists" => Ok(Self::AlreadyExists),
173 "permission_denied" => Ok(Self::PermissionDenied),
174 "resource_exhausted" => Ok(Self::ResourceExhausted),
175 "failed_precondition" => Ok(Self::FailedPrecondition),
176 "aborted" => Ok(Self::Aborted),
177 "out_of_range" => Ok(Self::OutOfRange),
178 "unimplemented" => Ok(Self::Unimplemented),
179 "internal" => Ok(Self::Internal),
180 "unavailable" => Ok(Self::Unavailable),
181 "data_loss" => Ok(Self::DataLoss),
182 "unauthenticated" => Ok(Self::Unauthenticated),
183 _ => Err(()),
184 }
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize)]
190pub struct ErrorDetail {
191 #[serde(rename = "type")]
194 pub type_url: String,
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub value: Option<String>,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub debug: Option<serde_json::Value>,
201}
202
203impl ErrorDetail {
204 pub fn from_message(type_name: impl Into<String>, message: &impl buffa::Message) -> Self {
217 Self {
218 type_url: type_name.into(),
219 value: Some(detail_b64::encode(&buffa::Message::encode_to_vec(message))),
220 debug: None,
221 }
222 }
223}
224
225pub(crate) mod detail_b64 {
230 use base64::Engine as _;
231 use base64::engine::general_purpose::{STANDARD, STANDARD_NO_PAD};
232
233 pub(crate) fn encode(bytes: &[u8]) -> String {
234 STANDARD_NO_PAD.encode(bytes)
235 }
236
237 pub(crate) fn decode_lenient(s: &str) -> Result<Vec<u8>, base64::DecodeError> {
238 STANDARD_NO_PAD.decode(s).or_else(|_| STANDARD.decode(s))
239 }
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub struct ConnectError {
245 pub code: ErrorCode,
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub message: Option<String>,
250 #[serde(skip_serializing_if = "Vec::is_empty", default)]
252 pub details: Vec<ErrorDetail>,
253 #[serde(skip)]
256 http_status_override: Option<StatusCode>,
257 #[serde(skip)]
263 pub(crate) response_headers: Option<Box<http::HeaderMap>>,
264 #[serde(skip)]
269 pub(crate) trailers: Option<Box<http::HeaderMap>>,
270 #[serde(skip)]
277 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
278}
279
280static EMPTY_HEADERS: std::sync::LazyLock<http::HeaderMap> =
283 std::sync::LazyLock::new(http::HeaderMap::new);
284
285fn box_headers(h: http::HeaderMap) -> Option<Box<http::HeaderMap>> {
286 if h.is_empty() {
287 None
288 } else {
289 Some(Box::new(h))
290 }
291}
292
293impl ConnectError {
294 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
296 Self {
297 code,
298 message: Some(message.into()),
299 details: Vec::new(),
300 http_status_override: None,
301 response_headers: None,
302 trailers: None,
303 source: None,
304 }
305 }
306
307 #[must_use]
309 pub fn with_headers(mut self, headers: http::HeaderMap) -> Self {
310 self.response_headers = box_headers(headers);
311 self
312 }
313
314 #[must_use]
316 pub fn with_trailers(mut self, trailers: http::HeaderMap) -> Self {
317 self.trailers = box_headers(trailers);
318 self
319 }
320
321 pub fn response_headers(&self) -> &http::HeaderMap {
328 self.response_headers.as_deref().unwrap_or(&EMPTY_HEADERS)
329 }
330
331 pub fn trailers(&self) -> &http::HeaderMap {
343 self.trailers.as_deref().unwrap_or(&EMPTY_HEADERS)
344 }
345
346 pub fn response_headers_mut(&mut self) -> &mut http::HeaderMap {
349 self.response_headers.get_or_insert_default()
350 }
351
352 pub fn trailers_mut(&mut self) -> &mut http::HeaderMap {
355 self.trailers.get_or_insert_default()
356 }
357
358 pub fn set_response_headers(&mut self, headers: http::HeaderMap) {
360 self.response_headers = box_headers(headers);
361 }
362
363 pub fn set_trailers(&mut self, trailers: http::HeaderMap) {
365 self.trailers = box_headers(trailers);
366 }
367
368 #[must_use]
373 pub fn with_http_status(mut self, status: StatusCode) -> Self {
374 self.http_status_override = Some(status);
375 self
376 }
377
378 pub fn unsupported_media_type(message: impl Into<String>) -> Self {
382 Self::new(ErrorCode::Unknown, message).with_http_status(StatusCode::UNSUPPORTED_MEDIA_TYPE)
384 }
385
386 pub fn method_not_allowed(message: impl Into<String>) -> Self {
390 Self::new(ErrorCode::Unknown, message).with_http_status(StatusCode::METHOD_NOT_ALLOWED)
392 }
393
394 pub fn canceled(message: impl Into<String>) -> Self {
396 Self::new(ErrorCode::Canceled, message)
397 }
398
399 pub fn unknown(message: impl Into<String>) -> Self {
401 Self::new(ErrorCode::Unknown, message)
402 }
403
404 pub fn invalid_argument(message: impl Into<String>) -> Self {
406 Self::new(ErrorCode::InvalidArgument, message)
407 }
408
409 pub fn deadline_exceeded(message: impl Into<String>) -> Self {
411 Self::new(ErrorCode::DeadlineExceeded, message)
412 }
413
414 pub fn not_found(message: impl Into<String>) -> Self {
416 Self::new(ErrorCode::NotFound, message)
417 }
418
419 pub fn already_exists(message: impl Into<String>) -> Self {
421 Self::new(ErrorCode::AlreadyExists, message)
422 }
423
424 pub fn permission_denied(message: impl Into<String>) -> Self {
426 Self::new(ErrorCode::PermissionDenied, message)
427 }
428
429 pub fn resource_exhausted(message: impl Into<String>) -> Self {
431 Self::new(ErrorCode::ResourceExhausted, message)
432 }
433
434 pub fn failed_precondition(message: impl Into<String>) -> Self {
436 Self::new(ErrorCode::FailedPrecondition, message)
437 }
438
439 pub fn aborted(message: impl Into<String>) -> Self {
441 Self::new(ErrorCode::Aborted, message)
442 }
443
444 pub fn out_of_range(message: impl Into<String>) -> Self {
446 Self::new(ErrorCode::OutOfRange, message)
447 }
448
449 pub fn unimplemented(message: impl Into<String>) -> Self {
451 Self::new(ErrorCode::Unimplemented, message)
452 }
453
454 pub fn internal(message: impl Into<String>) -> Self {
456 Self::new(ErrorCode::Internal, message)
457 }
458
459 pub fn unavailable(message: impl Into<String>) -> Self {
461 Self::new(ErrorCode::Unavailable, message)
462 }
463
464 pub fn data_loss(message: impl Into<String>) -> Self {
466 Self::new(ErrorCode::DataLoss, message)
467 }
468
469 pub fn unauthenticated(message: impl Into<String>) -> Self {
471 Self::new(ErrorCode::Unauthenticated, message)
472 }
473
474 #[must_use]
476 pub fn with_detail(mut self, detail: ErrorDetail) -> Self {
477 self.details.push(detail);
478 self
479 }
480
481 #[must_use]
494 pub fn with_source(
495 mut self,
496 source: impl Into<Box<dyn std::error::Error + Send + Sync>>,
497 ) -> Self {
498 self.source = Some(Arc::from(source.into()));
499 self
500 }
501
502 pub fn http_status(&self) -> StatusCode {
506 self.http_status_override
507 .unwrap_or_else(|| self.code.http_status())
508 }
509
510 pub fn to_json(&self) -> Bytes {
512 Bytes::from(serde_json::to_vec(self).unwrap_or_else(|_| {
513 format!(r#"{{"code":"{}"}}"#, self.code.as_str()).into_bytes()
515 }))
516 }
517}
518
519impl std::fmt::Display for ConnectError {
520 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
521 write!(f, "{}", self.code.as_str())?;
522 if let Some(ref message) = self.message {
523 write!(f, ": {message}")?;
524 }
525 Ok(())
526 }
527}
528
529impl std::error::Error for ConnectError {
530 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
531 self.source
532 .as_ref()
533 .map(|e| &**e as &(dyn std::error::Error + 'static))
534 }
535}
536
537impl From<std::io::Error> for ConnectError {
538 fn from(err: std::io::Error) -> Self {
539 Self::internal(err.to_string()).with_source(err)
540 }
541}
542
543impl From<http::Error> for ConnectError {
546 fn from(err: http::Error) -> Self {
547 Self::internal(err.to_string()).with_source(err)
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554
555 #[test]
556 fn from_http_error_is_internal() {
557 let http_err: http::Error = http::HeaderValue::from_bytes(b"bad\nval")
558 .unwrap_err()
559 .into();
560 let e: ConnectError = http_err.into();
561 assert_eq!(e.code, ErrorCode::Internal);
562 }
563
564 #[test]
565 fn from_io_error_preserves_source() {
566 let io_err = std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "refused");
567 let e: ConnectError = io_err.into();
568 let source = std::error::Error::source(&e).expect("source must be preserved");
569 assert_eq!(source.to_string(), "refused");
570 }
571
572 #[test]
573 fn from_http_error_preserves_source() {
574 let http_err: http::Error = http::HeaderValue::from_bytes(b"bad\nval")
575 .unwrap_err()
576 .into();
577 let e: ConnectError = http_err.into();
578 assert!(std::error::Error::source(&e).is_some());
579 }
580
581 #[test]
582 fn with_source_is_returned_by_error_source() {
583 let cause = std::io::Error::other("boom");
584 let e = ConnectError::unavailable("wrapped").with_source(cause);
585 let source = std::error::Error::source(&e).expect("source must be set");
586 assert_eq!(source.to_string(), "boom");
587 }
588
589 #[test]
590 fn with_source_accepts_already_boxed_error() {
591 let boxed: Box<dyn std::error::Error + Send + Sync> =
592 Box::new(std::io::Error::other("boxed boom"));
593 let e = ConnectError::unavailable("wrapped").with_source(boxed);
594 let source = std::error::Error::source(&e).expect("source must be set");
595 assert_eq!(source.to_string(), "boxed boom");
596 }
597
598 #[test]
599 fn no_source_by_default() {
600 let e = ConnectError::internal("plain");
601 assert!(std::error::Error::source(&e).is_none());
602 }
603
604 #[test]
605 fn with_source_survives_clone() {
606 let cause = std::io::Error::other("boom");
607 let e = ConnectError::unavailable("wrapped")
608 .with_source(cause)
609 .clone();
610 assert!(std::error::Error::source(&e).is_some());
611 }
612
613 #[test]
614 fn test_grpc_code_round_trip() {
615 let codes = [
616 ErrorCode::Canceled,
617 ErrorCode::Unknown,
618 ErrorCode::InvalidArgument,
619 ErrorCode::DeadlineExceeded,
620 ErrorCode::NotFound,
621 ErrorCode::AlreadyExists,
622 ErrorCode::PermissionDenied,
623 ErrorCode::ResourceExhausted,
624 ErrorCode::FailedPrecondition,
625 ErrorCode::Aborted,
626 ErrorCode::OutOfRange,
627 ErrorCode::Unimplemented,
628 ErrorCode::Internal,
629 ErrorCode::Unavailable,
630 ErrorCode::DataLoss,
631 ErrorCode::Unauthenticated,
632 ];
633
634 for code in codes {
635 let grpc = code.grpc_code();
636 let back = ErrorCode::from_grpc_code(grpc);
637 assert_eq!(
638 back,
639 Some(code),
640 "round-trip failed for {code:?} (grpc code {grpc})"
641 );
642 }
643 }
644
645 #[test]
646 fn test_grpc_code_values() {
647 assert_eq!(ErrorCode::Canceled.grpc_code(), 1);
648 assert_eq!(ErrorCode::Unknown.grpc_code(), 2);
649 assert_eq!(ErrorCode::Internal.grpc_code(), 13);
650 assert_eq!(ErrorCode::Unauthenticated.grpc_code(), 16);
651 }
652
653 #[test]
654 fn test_from_grpc_code_ok_returns_none() {
655 assert_eq!(ErrorCode::from_grpc_code(0), None);
656 }
657
658 #[test]
659 fn test_from_grpc_code_unknown_returns_none() {
660 assert_eq!(ErrorCode::from_grpc_code(17), None);
661 assert_eq!(ErrorCode::from_grpc_code(999), None);
662 }
663
664 #[test]
665 fn connect_error_stays_under_result_large_err_threshold() {
666 const THRESHOLD: usize = 96;
669 let size = std::mem::size_of::<ConnectError>();
670 assert!(
671 size <= THRESHOLD,
672 "ConnectError is {size} bytes (threshold {THRESHOLD}); \
673 box large fields to keep Result<_, ConnectError> cheap to move"
674 );
675 }
676
677 #[test]
678 fn header_accessors() {
679 let mut e = ConnectError::internal("x");
680 assert!(e.response_headers().is_empty());
681 assert!(e.trailers().is_empty());
682
683 e.set_response_headers(http::HeaderMap::new());
685 assert!(e.response_headers.is_none());
686 assert!(
687 ConnectError::new(ErrorCode::Internal, "x")
688 .with_headers(http::HeaderMap::new())
689 .response_headers
690 .is_none()
691 );
692
693 e.trailers_mut()
694 .insert("x-t", http::HeaderValue::from_static("v"));
695 assert_eq!(e.trailers().get("x-t").unwrap(), "v");
696
697 let mut h = http::HeaderMap::new();
698 h.insert("x-h", http::HeaderValue::from_static("w"));
699 let e = e.with_headers(h);
700 assert_eq!(e.response_headers().get("x-h").unwrap(), "w");
701 }
702}