1use std::error::Error;
10use std::fmt::{Display, Formatter};
11
12use dex_protocol::dex::{ErrorSubStatus as ProtoErrorSubStatus, ServiceErrorResponse};
13use prost::Message;
14use prost_types::Any;
15use tonic::Status;
16
17use crate::GrpcCode;
18
19pub type SdkResult<T> = Result<T, SdkError>;
21
22#[derive(Clone, Copy, Debug, Eq, PartialEq)]
23pub enum ErrorSubStatus {
25 Uncategorized,
27 FlowAlreadyStarted,
29 FlowNotExists,
31 WorkerApiError,
33 LongPollTimeout,
35 ChannelMessageNotFound,
37}
38
39#[derive(Debug)]
40pub struct ServiceError {
42 code: GrpcCode,
43 sub_status: ErrorSubStatus,
44 detail: String,
45 operation: &'static str,
46 flow_id: Option<String>,
47 source: Status,
48}
49
50impl ServiceError {
51 pub fn code(&self) -> GrpcCode {
53 self.code
54 }
55
56 pub fn sub_status(&self) -> ErrorSubStatus {
58 self.sub_status
59 }
60
61 pub fn detail(&self) -> &str {
63 &self.detail
64 }
65
66 pub fn operation(&self) -> &str {
68 self.operation
69 }
70
71 pub fn flow_id(&self) -> Option<&str> {
73 self.flow_id.as_deref()
74 }
75
76 pub(crate) fn local(operation: &'static str, detail: impl Into<String>) -> Self {
77 let detail = detail.into();
78 Self {
79 code: GrpcCode::Unknown,
80 sub_status: ErrorSubStatus::Uncategorized,
81 detail: detail.clone(),
82 operation,
83 flow_id: None,
84 source: Status::unknown(detail),
85 }
86 }
87}
88
89impl Display for ServiceError {
90 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
91 formatter.write_str(&self.detail)
92 }
93}
94
95impl Error for ServiceError {
96 fn source(&self) -> Option<&(dyn Error + 'static)> {
97 Some(&self.source)
98 }
99}
100
101#[derive(Clone, Debug, Eq, PartialEq)]
102pub struct WorkerError {
104 code: Option<GrpcCode>,
105 error_type: String,
106 detail: String,
107}
108
109impl WorkerError {
110 pub fn code(&self) -> Option<GrpcCode> {
112 self.code
113 }
114
115 pub fn error_type(&self) -> &str {
117 &self.error_type
118 }
119
120 pub fn detail(&self) -> &str {
122 &self.detail
123 }
124}
125
126#[derive(Debug)]
127pub enum SdkError {
132 Service {
134 service: ServiceError,
136 },
137 FlowAlreadyStarted {
139 service: ServiceError,
141 },
142 FlowNotFound {
144 service: ServiceError,
146 },
147 FlowNotActive {
149 service: ServiceError,
151 },
152 WorkerInvocation {
154 service: ServiceError,
156 worker: Box<WorkerError>,
158 },
159 RpcLockConflict {
161 service: ServiceError,
163 },
164 LongPollTimeout {
166 service: ServiceError,
168 },
169 ChannelMessageNotFound {
171 service: ServiceError,
173 },
174 FlowDefinition {
176 message: String,
178 },
179 InvalidArgument {
181 message: String,
183 },
184 ValueMapping {
186 message: String,
188 },
189 InvalidStepResult {
191 flow_type: String,
193 step_type: String,
195 detail: String,
197 },
198}
199
200impl Display for SdkError {
201 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
202 match self {
203 Self::Service { service }
204 | Self::FlowAlreadyStarted { service }
205 | Self::FlowNotFound { service }
206 | Self::FlowNotActive { service }
207 | Self::RpcLockConflict { service }
208 | Self::LongPollTimeout { service }
209 | Self::ChannelMessageNotFound { service } => Display::fmt(service, formatter),
210 Self::WorkerInvocation { service, .. } => Display::fmt(service, formatter),
211 Self::InvalidStepResult { detail, .. } => formatter.write_str(detail),
212 Self::FlowDefinition { message }
213 | Self::InvalidArgument { message }
214 | Self::ValueMapping { message } => formatter.write_str(message),
215 }
216 }
217}
218
219impl Error for SdkError {
220 fn source(&self) -> Option<&(dyn Error + 'static)> {
221 self.service_error()
222 .map(|error| error as &(dyn Error + 'static))
223 }
224}
225
226impl SdkError {
227 pub fn service_error(&self) -> Option<&ServiceError> {
231 match self {
232 Self::Service { service }
233 | Self::FlowAlreadyStarted { service }
234 | Self::FlowNotFound { service }
235 | Self::FlowNotActive { service }
236 | Self::RpcLockConflict { service }
237 | Self::LongPollTimeout { service }
238 | Self::ChannelMessageNotFound { service } => Some(service),
239 Self::WorkerInvocation { service, .. } => Some(service),
240 _ => None,
241 }
242 }
243
244 pub(crate) fn from_status(
245 status: Status,
246 operation: &'static str,
247 flow_id: Option<&str>,
248 requirement: FlowTargetRequirement,
249 ) -> Self {
250 let code = status.code();
251 let decoded = decode_details(status.details());
252 let details = decoded.as_ref().ok().and_then(|details| details.as_ref());
253 let detail = details
254 .filter(|details| !details.detail.is_empty())
255 .map_or_else(
256 || {
257 if status.message().is_empty() {
258 format!("{code:?}")
259 } else {
260 status.message().to_string()
261 }
262 },
263 |details| details.detail.clone(),
264 );
265 let sub_status = details
266 .map(|details| map_sub_status(details.sub_status))
267 .unwrap_or(ErrorSubStatus::Uncategorized);
268 let service = ServiceError {
269 code,
270 sub_status,
271 detail,
272 operation,
273 flow_id: flow_id.map(str::to_string),
274 source: status,
275 };
276 if decoded.is_err() {
277 return Self::Service { service };
278 }
279 match sub_status {
280 ErrorSubStatus::FlowAlreadyStarted => Self::FlowAlreadyStarted { service },
281 ErrorSubStatus::FlowNotExists => match requirement {
282 FlowTargetRequirement::Active => Self::FlowNotActive { service },
283 FlowTargetRequirement::Existing => Self::FlowNotFound { service },
284 FlowTargetRequirement::None => Self::Service { service },
285 },
286 ErrorSubStatus::WorkerApiError if code == GrpcCode::Aborted => {
287 Self::RpcLockConflict { service }
288 }
289 ErrorSubStatus::WorkerApiError => {
290 let details = details.expect("worker details were decoded");
291 Self::WorkerInvocation {
292 service,
293 worker: Box::new(WorkerError {
294 code: (details.original_worker_error_status != 0)
295 .then(|| GrpcCode::from_i32(details.original_worker_error_status)),
296 error_type: details.original_worker_error_type.clone(),
297 detail: details.original_worker_error_detail.clone(),
298 }),
299 }
300 }
301 ErrorSubStatus::LongPollTimeout => Self::LongPollTimeout { service },
302 ErrorSubStatus::ChannelMessageNotFound => Self::ChannelMessageNotFound { service },
303 ErrorSubStatus::Uncategorized => Self::Service { service },
304 }
305 }
306}
307
308#[derive(Clone, Copy)]
309pub(crate) enum FlowTargetRequirement {
310 None,
311 Existing,
312 Active,
313}
314
315#[derive(Clone, PartialEq, Message)]
316struct GoogleRpcStatus {
317 #[prost(int32, tag = "1")]
318 code: i32,
319 #[prost(string, tag = "2")]
320 message: String,
321 #[prost(message, repeated, tag = "3")]
322 details: Vec<Any>,
323}
324
325fn decode_details(bytes: &[u8]) -> Result<Option<ServiceErrorResponse>, prost::DecodeError> {
326 if bytes.is_empty() {
327 return Ok(None);
328 }
329 match GoogleRpcStatus::decode(bytes) {
330 Ok(status) => {
331 for detail in status.details {
332 if detail.type_url.ends_with("/dex.ServiceErrorResponse") {
333 return ServiceErrorResponse::decode(detail.value.as_slice()).map(Some);
334 }
335 }
336 Ok(None)
337 }
338 Err(status_error) => ServiceErrorResponse::decode(bytes)
339 .map(Some)
340 .or(Err(status_error)),
341 }
342}
343
344fn map_sub_status(value: i32) -> ErrorSubStatus {
345 match ProtoErrorSubStatus::try_from(value).ok() {
346 Some(ProtoErrorSubStatus::FlowAlreadyStarted) => ErrorSubStatus::FlowAlreadyStarted,
347 Some(ProtoErrorSubStatus::FlowNotExists) => ErrorSubStatus::FlowNotExists,
348 Some(ProtoErrorSubStatus::WorkerApiError) => ErrorSubStatus::WorkerApiError,
349 Some(ProtoErrorSubStatus::LongPollTimeOut) => ErrorSubStatus::LongPollTimeout,
350 Some(ProtoErrorSubStatus::ChannelMessageNotFound) => ErrorSubStatus::ChannelMessageNotFound,
351 _ => ErrorSubStatus::Uncategorized,
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358
359 #[test]
360 fn missing_flow_uses_endpoint_lifecycle_requirement() {
361 let found = SdkError::from_status(
362 rich_status(GrpcCode::NotFound, ProtoErrorSubStatus::FlowNotExists),
363 "describe_flow",
364 Some("flow-id"),
365 FlowTargetRequirement::Existing,
366 );
367 assert!(matches!(found, SdkError::FlowNotFound { .. }));
368
369 let active = SdkError::from_status(
370 rich_status(GrpcCode::NotFound, ProtoErrorSubStatus::FlowNotExists),
371 "publish",
372 Some("flow-id"),
373 FlowTargetRequirement::Active,
374 );
375 assert!(matches!(active, SdkError::FlowNotActive { .. }));
376 }
377
378 #[test]
379 fn worker_details_and_lock_conflicts_are_distinct() {
380 let invocation = SdkError::from_status(
381 worker_status(GrpcCode::FailedPrecondition),
382 "invoke_rpc",
383 Some("flow-id"),
384 FlowTargetRequirement::Active,
385 );
386 match invocation {
387 SdkError::WorkerInvocation { service, worker } => {
388 assert_eq!(GrpcCode::FailedPrecondition, service.code());
389 assert_eq!(Some(GrpcCode::InvalidArgument), worker.code());
390 assert_eq!("ApplicationError", worker.error_type());
391 assert_eq!("invalid order", worker.detail());
392 }
393 error => panic!("expected WorkerInvocation, got {error:?}"),
394 }
395
396 let conflict = SdkError::from_status(
397 worker_status(GrpcCode::Aborted),
398 "invoke_rpc",
399 Some("flow-id"),
400 FlowTargetRequirement::Active,
401 );
402 assert!(matches!(conflict, SdkError::RpcLockConflict { .. }));
403 }
404
405 #[test]
406 fn missing_and_malformed_details_use_generic_fallback() {
407 let missing = SdkError::from_status(
408 Status::internal("missing"),
409 "search_flows",
410 None,
411 FlowTargetRequirement::None,
412 );
413 assert!(matches!(missing, SdkError::Service { .. }));
414
415 let malformed = SdkError::from_status(
416 Status::with_details(GrpcCode::Internal, "malformed", vec![255].into()),
417 "search_flows",
418 None,
419 FlowTargetRequirement::None,
420 );
421 assert!(matches!(malformed, SdkError::Service { .. }));
422 }
423
424 fn rich_status(code: GrpcCode, sub_status: ProtoErrorSubStatus) -> Status {
425 status_with_response(
426 code,
427 ServiceErrorResponse {
428 detail: "service detail".to_string(),
429 sub_status: sub_status as i32,
430 ..ServiceErrorResponse::default()
431 },
432 )
433 }
434
435 fn worker_status(code: GrpcCode) -> Status {
436 status_with_response(
437 code,
438 ServiceErrorResponse {
439 detail: "worker failure".to_string(),
440 sub_status: ProtoErrorSubStatus::WorkerApiError as i32,
441 original_worker_error_detail: "invalid order".to_string(),
442 original_worker_error_type: "ApplicationError".to_string(),
443 original_worker_error_status: GrpcCode::InvalidArgument as i32,
444 original_worker_error_stack_trace: String::new(),
445 },
446 )
447 }
448
449 fn status_with_response(code: GrpcCode, response: ServiceErrorResponse) -> Status {
450 let status = GoogleRpcStatus {
451 code: code as i32,
452 message: "gRPC detail".to_string(),
453 details: vec![Any {
454 type_url: "type.googleapis.com/dex.ServiceErrorResponse".to_string(),
455 value: response.encode_to_vec(),
456 }],
457 };
458 Status::with_details(code, "gRPC detail", status.encode_to_vec().into())
459 }
460}