1use std::error::Error;
4use std::fmt::{self, Debug, Display, Formatter};
5
6use crate::error::{ErrorEnvelope, ServiceError, canonical_http_status, new_error_instance_id};
7
8pub type BoxError = Box<dyn Error + Send + Sync + 'static>;
10
11pub struct ServiceFailure<E> {
13 envelope: ErrorEnvelope,
14 declared: Option<E>,
15 observed_status: Option<u16>,
16}
17
18impl<E> ServiceFailure<E> {
19 pub fn undeclared(envelope: ErrorEnvelope) -> Self {
21 Self {
22 envelope,
23 declared: None,
24 observed_status: None,
25 }
26 }
27
28 pub fn envelope(&self) -> &ErrorEnvelope {
30 &self.envelope
31 }
32
33 pub fn declared(&self) -> Option<&E> {
35 self.declared.as_ref()
36 }
37
38 pub fn into_declared(mut self) -> Result<E, Self> {
40 match self.declared.take() {
41 Some(error) => Ok(error),
42 None => Err(self),
43 }
44 }
45
46 pub fn canonical_status(&self) -> u16 {
48 canonical_http_status(&self.envelope.error_code)
49 }
50
51 pub fn observed_status(&self) -> Option<u16> {
53 self.observed_status
54 }
55
56 pub fn status(&self) -> u16 {
58 self.observed_status
59 .unwrap_or_else(|| self.canonical_status())
60 }
61
62 fn into_local_undeclared<T>(self) -> ServiceFailure<T> {
63 ServiceFailure {
64 envelope: self.envelope,
65 declared: None,
66 observed_status: None,
67 }
68 }
69}
70
71impl<E: ServiceError> ServiceFailure<E> {
72 fn locally_declared(error: E) -> Self {
73 let envelope = error.to_envelope(new_error_instance_id());
74 Self {
75 envelope,
76 declared: Some(error),
77 observed_status: None,
78 }
79 }
80
81 pub fn remote(status: u16, envelope: ErrorEnvelope) -> Self {
83 let declared = (canonical_http_status(&envelope.error_code) == status)
84 .then(|| E::from_envelope(&envelope))
85 .flatten();
86 Self {
87 envelope,
88 declared,
89 observed_status: Some(status),
90 }
91 }
92}
93
94pub struct UnknownRemote {
96 status: u16,
97 content_type: Option<String>,
98 body: Vec<u8>,
99 truncated: bool,
100 source: Option<BoxError>,
101}
102
103impl UnknownRemote {
104 pub fn status(&self) -> u16 {
106 self.status
107 }
108
109 pub fn content_type(&self) -> Option<&str> {
111 self.content_type.as_deref()
112 }
113
114 pub fn body(&self) -> &[u8] {
116 &self.body
117 }
118
119 pub fn truncated(&self) -> bool {
121 self.truncated
122 }
123}
124
125pub struct TransportFailure {
127 source: BoxError,
128 observed_status: Option<u16>,
129}
130
131impl TransportFailure {
132 pub fn observed_status(&self) -> Option<u16> {
134 self.observed_status
135 }
136}
137
138pub struct ProtocolFailure {
140 message: String,
141 observed_status: Option<u16>,
142}
143
144impl ProtocolFailure {
145 pub fn message(&self) -> &str {
147 &self.message
148 }
149
150 pub fn observed_status(&self) -> Option<u16> {
152 self.observed_status
153 }
154}
155
156pub enum CallError<E> {
158 Service(ServiceFailure<E>),
160 UnknownRemote(UnknownRemote),
162 Transport(TransportFailure),
164 Protocol(ProtocolFailure),
166 Internal {
168 source: BoxError,
170 error_instance_id: String,
172 },
173}
174
175impl<E> CallError<E> {
176 pub fn transport(error: impl Into<BoxError>) -> Self {
178 Self::Transport(TransportFailure {
179 source: error.into(),
180 observed_status: None,
181 })
182 }
183
184 pub fn transport_response(status: u16, error: impl Into<BoxError>) -> Self {
186 Self::Transport(TransportFailure {
187 source: error.into(),
188 observed_status: Some(status),
189 })
190 }
191
192 pub fn protocol(message: impl Into<String>) -> Self {
194 Self::Protocol(ProtocolFailure {
195 message: message.into(),
196 observed_status: None,
197 })
198 }
199
200 pub fn protocol_response(status: u16, message: impl Into<String>) -> Self {
202 Self::Protocol(ProtocolFailure {
203 message: message.into(),
204 observed_status: Some(status),
205 })
206 }
207
208 pub fn unknown_remote(
210 status: u16,
211 content_type: Option<String>,
212 body: Vec<u8>,
213 truncated: bool,
214 ) -> Self {
215 Self::UnknownRemote(UnknownRemote {
216 status,
217 content_type,
218 body,
219 truncated,
220 source: None,
221 })
222 }
223
224 pub fn unreadable_remote(
226 status: u16,
227 content_type: Option<String>,
228 body: Vec<u8>,
229 error: impl Into<BoxError>,
230 ) -> Self {
231 Self::UnknownRemote(UnknownRemote {
232 status,
233 content_type,
234 body,
235 truncated: false,
236 source: Some(error.into()),
237 })
238 }
239
240 pub fn service(envelope: ErrorEnvelope) -> Self {
242 Self::Service(ServiceFailure::undeclared(envelope))
243 }
244
245 pub fn internal(error: impl Into<BoxError>) -> Self {
247 Self::Internal {
248 source: error.into(),
249 error_instance_id: new_error_instance_id(),
250 }
251 }
252
253 pub fn unauthenticated() -> Self {
255 Self::service(ErrorEnvelope::new(
256 "UNAUTHENTICATED",
257 "Default:Unauthenticated",
258 new_error_instance_id(),
259 ))
260 }
261
262 pub fn permission_denied(operation: impl Into<String>, resource: impl Into<String>) -> Self {
264 let mut envelope = ErrorEnvelope::new(
265 "PERMISSION_DENIED",
266 "Default:PermissionDenied",
267 new_error_instance_id(),
268 );
269 envelope
270 .parameters
271 .insert("operation".into(), operation.into());
272 envelope
273 .parameters
274 .insert("resource".into(), resource.into());
275 Self::service(envelope)
276 }
277
278 pub fn error_instance_id(&self) -> Option<&str> {
280 match self {
281 Self::Service(failure) => Some(&failure.envelope.error_instance_id),
282 Self::Internal {
283 error_instance_id, ..
284 } => Some(error_instance_id),
285 Self::UnknownRemote(_) | Self::Transport(_) | Self::Protocol(_) => None,
286 }
287 }
288
289 pub fn from_dependency<F>(error: CallError<F>) -> Self
291 where
292 F: Send + Sync + 'static,
293 {
294 match error {
295 CallError::Service(failure) if matches!(failure.canonical_status(), 401 | 403) => {
296 Self::Service(failure.into_local_undeclared())
297 }
298 error => {
299 let error_instance_id = error
300 .error_instance_id()
301 .map(ToOwned::to_owned)
302 .unwrap_or_else(new_error_instance_id);
303 Self::Internal {
304 source: Box::new(error),
305 error_instance_id,
306 }
307 }
308 }
309 }
310}
311
312impl<E: ServiceError> From<E> for CallError<E> {
313 fn from(error: E) -> Self {
314 Self::Service(ServiceFailure::locally_declared(error))
315 }
316}
317
318impl<E> Display for CallError<E> {
319 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
320 formatter.write_str(match self {
321 Self::Service(_) => "service call rejected",
322 Self::UnknownRemote(_) => "remote service returned an unknown error",
323 Self::Transport(_) => "service transport failed",
324 Self::Protocol(_) => "invalid service response",
325 Self::Internal { .. } => "service implementation failed",
326 })
327 }
328}
329
330impl<E> Debug for CallError<E> {
331 fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
332 formatter.write_str(match self {
333 Self::Service(_) => "CallError::Service([REDACTED])",
334 Self::UnknownRemote(_) => "CallError::UnknownRemote([REDACTED])",
335 Self::Transport(_) => "CallError::Transport([REDACTED])",
336 Self::Protocol(_) => "CallError::Protocol([REDACTED])",
337 Self::Internal { .. } => "CallError::Internal([REDACTED])",
338 })
339 }
340}
341
342impl<E> Error for CallError<E>
343where
344 E: Send + Sync + 'static,
345{
346 fn source(&self) -> Option<&(dyn Error + 'static)> {
347 match self {
348 Self::Service(failure) => Some(&failure.envelope),
349 Self::UnknownRemote(error) => error.source.as_deref().map(|error| error as _),
350 Self::Transport(error) => Some(error.source.as_ref()),
351 Self::Protocol(_) => None,
352 Self::Internal { source, .. } => Some(source.as_ref()),
353 }
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use std::error::Error;
360 use std::io;
361
362 use uuid::Uuid;
363
364 use super::{CallError, ServiceFailure};
365 use crate::error::{ErrorEnvelope, ServiceError};
366
367 #[derive(Debug)]
368 struct TestError;
369
370 impl ServiceError for TestError {
371 fn to_envelope(&self, instance: String) -> ErrorEnvelope {
372 ErrorEnvelope::new("NOT_FOUND", "Test:NotFound", instance)
373 }
374
375 fn from_envelope(envelope: &ErrorEnvelope) -> Option<Self> {
376 (envelope.error_code == "NOT_FOUND" && envelope.error_name == "Test:NotFound")
377 .then_some(Self)
378 }
379 }
380
381 #[test]
382 fn hides_diagnostics_but_preserves_error_sources() {
383 let errors = [
384 CallError::<TestError>::transport(io::Error::other("secret")),
385 CallError::<TestError>::protocol("secret"),
386 CallError::<TestError>::internal(io::Error::other("secret")),
387 ];
388 for error in errors {
389 assert!(!error.to_string().contains("secret"));
390 assert!(!format!("{error:?}").contains("secret"));
391 if !matches!(error, CallError::Protocol(_)) {
392 assert_eq!(error.source().unwrap().to_string(), "secret");
393 }
394 }
395 }
396
397 #[test]
398 fn dependency_propagation_keeps_only_canonical_auth_failures() {
399 let denied = CallError::<TestError>::permission_denied("read", "resource");
400 let instance = denied.error_instance_id().unwrap().to_owned();
401 let propagated = CallError::<TestError>::from_dependency(denied);
402 let CallError::Service(failure) = propagated else {
403 panic!("permission denial was not propagated")
404 };
405 assert_eq!(failure.canonical_status(), 403);
406 assert_eq!(failure.status(), 403);
407 assert_eq!(failure.observed_status(), None);
408 assert_eq!(failure.envelope().error_instance_id, instance);
409 assert!(failure.declared().is_none());
410
411 let envelope = ErrorEnvelope::new(
412 "PERMISSION_DENIED",
413 "Default:PermissionDenied",
414 Uuid::now_v7().to_string(),
415 );
416 let remote = CallError::<TestError>::Service(ServiceFailure::remote(500, envelope));
417 let CallError::Service(failure) = CallError::<TestError>::from_dependency(remote) else {
418 panic!("canonical permission denial was not propagated")
419 };
420 assert_eq!(failure.status(), 403);
421 assert_eq!(failure.observed_status(), None);
422
423 let remote = CallError::<TestError>::unknown_remote(401, None, Vec::new(), false);
424 assert!(matches!(
425 CallError::<TestError>::from_dependency(remote),
426 CallError::Internal { .. }
427 ));
428 }
429}