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(transparent)]
50 Other(#[from] anyhow::Error),
51}
52
53impl SdkError {
54 #[must_use]
56 pub fn is_retryable(&self) -> bool {
57 match self {
58 Self::Transport(_) => true,
59 Self::Http { status, .. } | Self::Api { status, .. } => {
60 matches!(*status, 408 | 425 | 429 | 500 | 502 | 503 | 504)
61 }
62 _ => false,
63 }
64 }
65
66 #[must_use]
68 pub fn status(&self) -> Option<u16> {
69 match self {
70 Self::Api { status, .. } | Self::Http { status, .. } => Some(*status),
71 _ => None,
72 }
73 }
74}
75
76impl From<reqwest::Error> for SdkError {
77 fn from(value: reqwest::Error) -> Self {
78 if value.is_timeout() {
79 Self::Transport(format!("timeout: {value}"))
80 } else if value.is_connect() {
81 Self::Transport(format!("connect error: {value}"))
82 } else if value.is_decode() {
83 Self::Deserialize(value.to_string())
84 } else {
85 Self::Transport(value.to_string())
86 }
87 }
88}
89
90impl From<reqwest_middleware::Error> for SdkError {
91 fn from(value: reqwest_middleware::Error) -> Self {
92 match value {
93 reqwest_middleware::Error::Reqwest(err) => err.into(),
94 reqwest_middleware::Error::Middleware(err) => Self::Transport(err.to_string()),
95 }
96 }
97}
98
99impl From<serde_json::Error> for SdkError {
100 fn from(value: serde_json::Error) -> Self {
101 Self::Deserialize(value.to_string())
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108
109 #[test]
110 fn retryable_classification() {
111 assert!(SdkError::Transport("x".into()).is_retryable());
112 assert!(
113 SdkError::Http {
114 status: 503,
115 message: "x".into()
116 }
117 .is_retryable()
118 );
119 assert!(
120 !SdkError::Http {
121 status: 400,
122 message: "x".into()
123 }
124 .is_retryable()
125 );
126 assert!(!SdkError::Auth("nope".into()).is_retryable());
127 }
128
129 #[test]
130 fn status_extraction() {
131 assert_eq!(
132 SdkError::Http {
133 status: 404,
134 message: String::new()
135 }
136 .status(),
137 Some(404)
138 );
139 assert_eq!(SdkError::Auth("x".into()).status(), None);
140 }
141}