exoware_sdk/proto/
error_details.rs1use base64::Engine;
2use buffa::Message;
3use buffa_types::google::protobuf::Any;
4use connectrpc::error::ErrorDetail;
5use connectrpc::{ConnectError, ErrorCode};
6
7use crate::google::rpc::{BadRequest, ErrorInfo, RetryInfo};
8use crate::query::Detail;
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct DecodedConnectError {
12 pub code: ErrorCode,
13 pub message: Option<String>,
14 pub bad_request: Option<BadRequest>,
15 pub error_info: Option<ErrorInfo>,
16 pub retry_info: Option<RetryInfo>,
17 pub query_detail: Option<Detail>,
18 pub other_details: Vec<Any>,
19}
20
21pub fn with_bad_request_detail(err: ConnectError, detail: BadRequest) -> ConnectError {
22 err.with_detail(pack_detail(&detail, BadRequest::TYPE_URL))
23}
24
25pub fn with_error_info_detail(err: ConnectError, detail: ErrorInfo) -> ConnectError {
26 err.with_detail(pack_detail(&detail, ErrorInfo::TYPE_URL))
27}
28
29pub fn with_retry_info_detail(err: ConnectError, detail: RetryInfo) -> ConnectError {
30 err.with_detail(pack_detail(&detail, RetryInfo::TYPE_URL))
31}
32
33pub fn with_query_detail(err: ConnectError, detail: Detail) -> ConnectError {
35 err.with_detail(pack_detail(&detail, Detail::TYPE_URL))
36}
37
38pub fn decode_connect_error(err: &ConnectError) -> Result<DecodedConnectError, buffa::DecodeError> {
39 let mut decoded = DecodedConnectError {
40 code: err.code,
41 message: err.message.clone(),
42 bad_request: None,
43 error_info: None,
44 retry_info: None,
45 query_detail: None,
46 other_details: Vec::new(),
47 };
48
49 for detail in &err.details {
50 let any = decode_detail(detail)?;
51 if let Some(msg) = any.unpack_if::<BadRequest>(BadRequest::TYPE_URL)? {
52 decoded.bad_request = Some(msg);
53 continue;
54 }
55 if let Some(msg) = any.unpack_if::<ErrorInfo>(ErrorInfo::TYPE_URL)? {
56 decoded.error_info = Some(msg);
57 continue;
58 }
59 if let Some(msg) = any.unpack_if::<RetryInfo>(RetryInfo::TYPE_URL)? {
60 decoded.retry_info = Some(msg);
61 continue;
62 }
63 if let Some(msg) = any.unpack_if::<Detail>(Detail::TYPE_URL)? {
64 decoded.query_detail = Some(msg);
65 continue;
66 }
67 decoded.other_details.push(any);
68 }
69
70 Ok(decoded)
71}
72
73fn pack_detail<M: Message>(message: &M, type_url: &str) -> ErrorDetail {
74 let any = Any::pack(message, type_url.to_string());
75 ErrorDetail {
76 type_url: any.type_url,
77 value: Some(base64::engine::general_purpose::STANDARD_NO_PAD.encode(any.value)),
78 debug: None,
79 }
80}
81
82fn decode_detail(detail: &ErrorDetail) -> Result<Any, buffa::DecodeError> {
83 let value = detail
86 .value
87 .as_deref()
88 .map(|encoded| {
89 base64::engine::general_purpose::STANDARD_NO_PAD
90 .decode(encoded)
91 .or_else(|_| base64::engine::general_purpose::STANDARD.decode(encoded))
92 })
93 .transpose()
94 .map_err(|_| buffa::DecodeError::InvalidUtf8)?
95 .unwrap_or_default();
96 Ok(Any {
97 type_url: detail.type_url.clone(),
98 value,
99 ..Default::default()
100 })
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106 use crate::google::rpc::{bad_request::FieldViolation, ErrorInfo};
107 use crate::query::Detail;
108
109 #[test]
110 fn round_trips_read_stats_detail() {
111 let err = with_query_detail(
112 ConnectError::invalid_argument("bad"),
113 Detail {
114 sequence_number: 42,
115 ..Default::default()
116 },
117 );
118 let decoded = decode_connect_error(&err).expect("decode details");
119 assert_eq!(decoded.query_detail.unwrap().sequence_number, 42);
120 }
121
122 #[test]
123 fn round_trips_typed_details() {
124 let err = with_error_info_detail(
125 with_bad_request_detail(
126 ConnectError::invalid_argument("invalid request"),
127 BadRequest {
128 field_violations: vec![FieldViolation {
129 field: "key".to_string(),
130 description: "too long".to_string(),
131 ..Default::default()
132 }],
133 ..Default::default()
134 },
135 ),
136 ErrorInfo {
137 reason: "INVALID_KEY".to_string(),
138 domain: "store.ingest".to_string(),
139 metadata: [("max_key_len".to_string(), "254".to_string())]
140 .into_iter()
141 .collect(),
142 ..Default::default()
143 },
144 );
145
146 let decoded = decode_connect_error(&err).expect("decode details");
147 assert_eq!(decoded.code, ErrorCode::InvalidArgument);
148 assert_eq!(
149 decoded
150 .bad_request
151 .unwrap()
152 .field_violations
153 .first()
154 .expect("field violation")
155 .field,
156 "key"
157 );
158 assert_eq!(
159 decoded
160 .error_info
161 .unwrap()
162 .metadata
163 .get("max_key_len")
164 .map(String::as_str),
165 Some("254")
166 );
167 }
168}