openapp_sdk_core/
error.rs1pub use openapp_sdk_common::ApiErrorResponse;
4use openapp_sdk_common::TokenFormatError;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
9#[non_exhaustive]
10pub enum SdkError {
11 #[error("api error (status {status}): {}", .body.message)]
13 Api {
14 status: u16,
16 body: ApiErrorResponse,
18 },
19
20 #[error("http {status}: {message}")]
22 Http { status: u16, message: String },
23
24 #[error("auth error: {0}")]
26 Auth(String),
27
28 #[error("invalid api key: {0}")]
30 Token(#[from] TokenFormatError),
31
32 #[error("transport error: {0}")]
34 Transport(String),
35
36 #[error("failed to decode response: {0}")]
38 Deserialize(String),
39
40 #[error("invalid configuration: {0}")]
42 Config(String),
43
44 #[error("failed to serialize request: {0}")]
46 Serialize(String),
47
48 #[error("no {resource_type} named {name:?}")]
50 ResourceNotFound {
51 resource_type: &'static str,
53 name: String,
55 },
56
57 #[error("{resource_type} name {name:?} is ambiguous ({match_count} matches)")]
59 AmbiguousResource {
60 resource_type: &'static str,
62 name: String,
64 matches: Vec<serde_json::Value>,
66 match_count: usize,
68 },
69
70 #[error(transparent)]
72 Other(#[from] anyhow::Error),
73}
74
75impl SdkError {
76 #[must_use]
78 pub fn is_retryable(&self) -> bool {
79 match self {
80 Self::Transport(_) => true,
81 Self::Http { status, .. } | Self::Api { status, .. } => {
82 matches!(*status, 408 | 425 | 429 | 500 | 502 | 503 | 504)
83 }
84 _ => false,
85 }
86 }
87
88 #[must_use]
90 pub fn status(&self) -> Option<u16> {
91 match self {
92 Self::Api { status, .. } | Self::Http { status, .. } => Some(*status),
93 _ => None,
94 }
95 }
96}
97
98impl From<reqwest::Error> for SdkError {
99 fn from(value: reqwest::Error) -> Self {
100 if value.is_timeout() {
101 Self::Transport(format!("timeout: {value}"))
102 } else if value.is_connect() {
103 Self::Transport(format!("connect error: {value}"))
104 } else if value.is_decode() {
105 Self::Deserialize(value.to_string())
106 } else {
107 Self::Transport(value.to_string())
108 }
109 }
110}
111
112impl From<reqwest_middleware::Error> for SdkError {
113 fn from(value: reqwest_middleware::Error) -> Self {
114 match value {
115 reqwest_middleware::Error::Reqwest(err) => err.into(),
116 reqwest_middleware::Error::Middleware(err) => Self::Transport(err.to_string()),
117 }
118 }
119}
120
121impl From<serde_json::Error> for SdkError {
122 fn from(value: serde_json::Error) -> Self {
123 Self::Deserialize(value.to_string())
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn retryable_classification() {
133 assert!(SdkError::Transport("x".into()).is_retryable());
134 assert!(
135 SdkError::Http {
136 status: 503,
137 message: "x".into()
138 }
139 .is_retryable()
140 );
141 assert!(
142 !SdkError::Http {
143 status: 400,
144 message: "x".into()
145 }
146 .is_retryable()
147 );
148 assert!(!SdkError::Auth("nope".into()).is_retryable());
149 }
150
151 #[test]
152 fn status_extraction() {
153 assert_eq!(
154 SdkError::Http {
155 status: 404,
156 message: String::new()
157 }
158 .status(),
159 Some(404)
160 );
161 assert_eq!(SdkError::Auth("x".into()).status(), None);
162 }
163}