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