1use std::{backtrace::Backtrace, fmt, sync::Arc, time::Duration};
4use zeroize::Zeroizing;
5
6mod problem;
7pub(crate) use problem::body_retry_after;
8pub use problem::{ErrorMetadata, ErrorType, FailureKind, ProblemDetails, ProblemFieldError};
9
10pub type Result<T> = std::result::Result<T, Error>;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15#[non_exhaustive]
16pub enum ErrorKind {
17 Configuration,
18 Validation,
19 Destination,
20 Authentication,
21 Io,
22 Transport,
23 Http,
24 Decode,
25 Limit,
26 Deadline,
27 Cancelled,
28 Protocol,
29 Pagination,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34#[non_exhaustive]
35pub enum RemoteOutcome {
36 NotSent,
37 Rejected,
38 Unknown,
39}
40
41#[derive(Clone)]
44pub struct Error {
45 kind: ErrorKind,
46 status: Option<u16>,
47 operation: Option<&'static str>,
48 request_id: Option<String>,
49 service_code: Option<String>,
50 retry_after: Option<Duration>,
51 remote_outcome: RemoteOutcome,
52 details: Option<Arc<ErrorDetails>>,
53}
54#[derive(Clone, Default)]
55struct ErrorDetails {
56 custom_type: Option<Option<ErrorType>>,
57 message: Option<Zeroizing<String>>,
58 problem: Option<ProblemDetails>,
59 failure_kind: Option<FailureKind>,
60 backtrace: Option<Arc<Backtrace>>,
61}
62
63impl Error {
64 pub fn new(kind: ErrorKind) -> Self {
66 Self {
67 kind,
68 status: None,
69 operation: None,
70 request_id: None,
71 service_code: None,
72 retry_after: None,
73 remote_outcome: RemoteOutcome::NotSent,
74 details: None,
75 }
76 }
77 pub fn custom(
80 message: impl AsRef<str>,
81 error_type: Option<ErrorType>,
82 metadata: ErrorMetadata,
83 ) -> Result<Self> {
84 let message = message.as_ref();
85 if message.len() > 8192 {
86 return Err(Self::new(ErrorKind::Limit));
87 }
88 let message = Zeroizing::new(message.to_owned());
89 let mut error = Self::new(ErrorKind::Protocol);
90 error.status = metadata.status;
91 error.retry_after = metadata.retry_after;
92 error.details = Some(Arc::new(ErrorDetails {
93 message: Some(message),
94 custom_type: Some(error_type),
95 problem: metadata.problem,
96 failure_kind: metadata.failure_kind,
97 backtrace: None,
98 }));
99 Ok(error)
100 }
101 pub fn name(&self) -> &'static str {
102 "ScmSdkError"
103 }
104 pub fn message(&self) -> Option<&str> {
105 self.details
106 .as_ref()
107 .and_then(|details| details.message.as_ref())
108 .map(|value| value.as_str())
109 }
110 pub fn error_type(&self) -> Option<ErrorType> {
111 if let Some(details) = &self.details
112 && let Some(value) = details.custom_type
113 {
114 return value;
115 }
116 Some(match self.kind {
117 ErrorKind::Configuration => ErrorType::MissingVariable,
118 ErrorKind::Limit
119 if self
120 .status
121 .is_some_and(|value| value == 101 || (200..300).contains(&value)) =>
122 {
123 ErrorType::ResponseValidation
124 }
125 ErrorKind::Limit | ErrorKind::Destination
126 if self.remote_outcome != RemoteOutcome::NotSent || self.status.is_some() =>
127 {
128 ErrorType::ClientSideError
129 }
130 ErrorKind::Protocol
131 if self.remote_outcome != RemoteOutcome::NotSent || self.status.is_some() =>
132 {
133 ErrorType::ClientSideError
134 }
135 ErrorKind::Validation | ErrorKind::Destination | ErrorKind::Limit => {
136 ErrorType::UserRequestPayloadError
137 }
138 ErrorKind::Authentication => ErrorType::OAuthError,
139 ErrorKind::Http if self.status.is_some_and(|value| value >= 500) => {
140 ErrorType::ServerSideError
141 }
142 ErrorKind::Http | ErrorKind::Transport | ErrorKind::Io => ErrorType::ClientSideError,
143 ErrorKind::Decode => ErrorType::ResponseValidation,
144 _ => ErrorType::SdkError,
145 })
146 }
147 pub fn problem(&self) -> Option<&ProblemDetails> {
148 self.details
149 .as_ref()
150 .and_then(|details| details.problem.as_ref())
151 }
152 pub fn failure_kind(&self) -> Option<FailureKind> {
153 self.details
154 .as_ref()
155 .and_then(|details| details.failure_kind)
156 }
157 pub fn metadata(&self) -> ErrorMetadata {
158 ErrorMetadata {
159 problem: self.problem().cloned(),
160 failure_kind: self.failure_kind(),
161 status: self.status,
162 retry_after: self.retry_after,
163 }
164 }
165 pub fn capture_backtrace(mut self) -> Self {
167 self.details_mut().backtrace = Some(Arc::new(Backtrace::force_capture()));
168 self
169 }
170 pub fn backtrace(&self) -> Option<&Backtrace> {
171 self.details
172 .as_ref()
173 .and_then(|details| details.backtrace.as_deref())
174 }
175 fn details_mut(&mut self) -> &mut ErrorDetails {
176 Arc::make_mut(
177 self.details
178 .get_or_insert_with(|| Arc::new(ErrorDetails::default())),
179 )
180 }
181 pub fn kind(&self) -> ErrorKind {
182 self.kind
183 }
184 pub fn status(&self) -> Option<u16> {
185 self.status
186 }
187 pub fn operation(&self) -> Option<&'static str> {
188 self.operation
189 }
190 pub fn request_id(&self) -> Option<&str> {
192 self.request_id.as_deref()
193 }
194 pub fn service_code(&self) -> Option<&str> {
196 self.service_code.as_deref()
197 }
198 pub fn retry_after(&self) -> Option<Duration> {
199 self.retry_after
200 }
201 pub fn remote_outcome(&self) -> RemoteOutcome {
202 self.remote_outcome
203 }
204 pub(crate) fn with_operation(mut self, name: &'static str) -> Self {
205 self.operation = Some(name);
206 self
207 }
208 pub(crate) fn with_outcome(mut self, value: RemoteOutcome) -> Self {
209 self.remote_outcome = value;
210 self
211 }
212 pub(crate) fn with_status(mut self, status: u16) -> Self {
213 self.status = Some(status);
214 self
215 }
216 pub(crate) fn with_failure_kind(mut self, value: FailureKind) -> Self {
217 self.details_mut().failure_kind = Some(value);
218 self
219 }
220 pub(crate) fn with_http_body(mut self, body: &[u8]) -> Self {
221 self.details_mut().problem = ProblemDetails::parse(body).ok().flatten();
222 self.details_mut().failure_kind = Some(FailureKind::Http);
223 if self.retry_after.is_none() {
225 self.retry_after = body_retry_after(body);
226 }
227 self
228 }
229 pub(crate) fn with_response_metadata(
230 mut self,
231 status: u16,
232 request_id: Option<String>,
233 ) -> Self {
234 self = self.with_status(status);
235 if self.request_id.is_none() {
236 self.request_id = request_id;
237 }
238 self
239 }
240 pub(crate) fn with_metadata(
241 mut self,
242 request_id: Option<String>,
243 code: Option<String>,
244 delay: Option<Duration>,
245 ) -> Self {
246 self.request_id = request_id;
247 self.service_code = code;
248 self.retry_after = delay;
249 self
250 }
251}
252
253impl fmt::Debug for Error {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 f.debug_struct("Error")
256 .field("kind", &self.kind)
257 .field("status", &self.status)
258 .field("operation", &self.operation)
259 .field("remote_outcome", &self.remote_outcome)
260 .field("retry_after", &self.retry_after)
261 .field("has_request_id", &self.request_id.is_some())
262 .field("has_service_code", &self.service_code.is_some())
263 .field("failure_kind", &self.failure_kind())
264 .field("has_problem", &self.problem().is_some())
265 .finish()
266 }
267}
268impl fmt::Display for Error {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 write!(f, "SCM SDK {:?}", self.kind)?;
271 if let Some(status) = self.status {
272 write!(f, " (HTTP {status})")?;
273 }
274 Ok(())
275 }
276}
277impl std::error::Error for Error {}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 #[test]
283 fn explicit_application_messages_and_backtraces_are_never_formatted() {
284 assert!(std::mem::size_of::<Error>() <= 128);
285 let metadata = ErrorMetadata::new()
286 .with_status(429)
287 .unwrap()
288 .with_failure_kind(FailureKind::Http)
289 .with_retry_after(Duration::from_millis(2));
290 let error = Error::custom(
291 "synthetic caller secret",
292 Some(ErrorType::ClientSideError),
293 metadata,
294 )
295 .unwrap()
296 .capture_backtrace();
297 assert_eq!(error.message(), Some("synthetic caller secret"));
298 assert_eq!(
299 error.error_type().unwrap().as_str(),
300 "AISEC_CLIENT_SIDE_ERROR"
301 );
302 assert_eq!(error.failure_kind(), Some(FailureKind::Http));
303 assert_eq!(error.metadata().status(), Some(429));
304 assert!(error.backtrace().is_some());
305 assert!(!format!("{error:?} {error}").contains("secret"));
306 assert_eq!(error.name(), "ScmSdkError");
307 assert!(Error::custom("x".repeat(8193), None, ErrorMetadata::default()).is_err());
308 assert!(ErrorMetadata::new().with_status(0).is_err());
309 assert_eq!(
310 Error::new(ErrorKind::Http).with_status(503).error_type(),
311 Some(ErrorType::ServerSideError)
312 );
313 assert!(
314 Error::custom("message", None, ErrorMetadata::default())
315 .unwrap()
316 .error_type()
317 .is_none()
318 );
319 }
320}