Skip to main content

cloud_sdk/transport/
delivery.rs

1//! Conservative transport-delivery classification.
2
3use core::fmt;
4
5use super::StatusCode;
6
7/// Furthest delivery point known for one failed HTTP attempt.
8#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
9pub enum DeliveryPhase {
10    /// Validation or connection establishment failed before request delivery.
11    NotSent,
12    /// Some request bytes may have reached the peer.
13    PossiblySent,
14    /// Any informational or final response head was observed from the peer.
15    ResponseStarted,
16}
17
18/// Error whose furthest possible request-delivery phase is explicit.
19///
20/// Implementations should retain a final response status once observed so
21/// authentication and reconciliation policy can classify later failures.
22pub trait DeliveryClassified {
23    /// Returns the conservative delivery phase for permit transitions.
24    fn delivery_phase(&self) -> DeliveryPhase;
25
26    /// Returns a valid final status observed before the failure, when known.
27    fn observed_status(&self) -> Option<StatusCode> {
28        None
29    }
30}
31
32/// Payload-redacting transport failure with an explicit delivery phase and
33/// optional already-observed final status.
34///
35/// Unknown send state must use [`Self::unknown`], which conservatively maps to
36/// [`DeliveryPhase::PossiblySent`].
37#[derive(Clone, Copy)]
38pub struct TransportFailure<E> {
39    phase: DeliveryPhase,
40    observed_status: Option<StatusCode>,
41    error: E,
42}
43
44impl<E> TransportFailure<E> {
45    /// Records a failure proven to have happened before request delivery.
46    #[must_use]
47    pub const fn not_sent(error: E) -> Self {
48        Self {
49            phase: DeliveryPhase::NotSent,
50            observed_status: None,
51            error,
52        }
53    }
54
55    /// Records a failure after request delivery may have begun.
56    #[must_use]
57    pub const fn possibly_sent(error: E) -> Self {
58        Self {
59            phase: DeliveryPhase::PossiblySent,
60            observed_status: None,
61            error,
62        }
63    }
64
65    /// Records a failure after any response head was observed.
66    #[must_use]
67    pub const fn response_started(error: E) -> Self {
68        Self {
69            phase: DeliveryPhase::ResponseStarted,
70            observed_status: None,
71            error,
72        }
73    }
74
75    /// Records a failure after the supplied final response status was observed.
76    #[must_use]
77    pub const fn response_started_with_status(status: StatusCode, error: E) -> Self {
78        Self {
79            phase: DeliveryPhase::ResponseStarted,
80            observed_status: Some(status),
81            error,
82        }
83    }
84
85    /// Conservatively records an error whose delivery state is unknown.
86    #[must_use]
87    pub const fn unknown(error: E) -> Self {
88        Self::possibly_sent(error)
89    }
90
91    /// Returns the conservative delivery phase.
92    #[must_use]
93    pub const fn phase(&self) -> DeliveryPhase {
94        self.phase
95    }
96
97    /// Returns a final status observed before response processing failed.
98    #[must_use]
99    pub const fn observed_status(&self) -> Option<StatusCode> {
100        self.observed_status
101    }
102
103    /// Preserves this failure while attaching a subsequently known final status.
104    #[must_use]
105    pub const fn with_observed_status(mut self, status: StatusCode) -> Self {
106        self.phase = DeliveryPhase::ResponseStarted;
107        self.observed_status = Some(status);
108        self
109    }
110
111    /// Returns a shared view of the payload-free adapter error.
112    #[must_use]
113    pub const fn error(&self) -> &E {
114        &self.error
115    }
116
117    /// Consumes the failure and returns its adapter error.
118    #[must_use]
119    pub fn into_error(self) -> E {
120        self.error
121    }
122
123    /// Transforms the payload-free adapter error while preserving delivery state.
124    #[must_use]
125    pub fn map<F>(self, transform: impl FnOnce(E) -> F) -> TransportFailure<F> {
126        TransportFailure {
127            phase: self.phase,
128            observed_status: self.observed_status,
129            error: transform(self.error),
130        }
131    }
132}
133
134impl<E> DeliveryClassified for TransportFailure<E> {
135    fn delivery_phase(&self) -> DeliveryPhase {
136        self.phase
137    }
138
139    fn observed_status(&self) -> Option<StatusCode> {
140        self.observed_status
141    }
142}
143
144impl<E: PartialEq> PartialEq for TransportFailure<E> {
145    fn eq(&self, other: &Self) -> bool {
146        self.phase == other.phase
147            && self.observed_status == other.observed_status
148            && self.error == other.error
149    }
150}
151
152impl<E: Eq> Eq for TransportFailure<E> {}
153
154impl<E> fmt::Debug for TransportFailure<E> {
155    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
156        formatter
157            .debug_struct("TransportFailure")
158            .field("phase", &self.phase)
159            .field("observed_status", &self.observed_status)
160            .field("error", &"[redacted]")
161            .finish()
162    }
163}
164
165impl<E> fmt::Display for TransportFailure<E> {
166    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
167        match self.phase {
168            DeliveryPhase::NotSent => formatter.write_str("transport failed before delivery"),
169            DeliveryPhase::PossiblySent => {
170                formatter.write_str("transport failed with uncertain delivery")
171            }
172            DeliveryPhase::ResponseStarted => {
173                formatter.write_str("transport failed after response start")
174            }
175        }
176    }
177}
178
179impl<E> core::error::Error for TransportFailure<E> {}
180
181#[cfg(test)]
182mod tests {
183    use core::fmt::{self, Write};
184
185    use super::{DeliveryClassified, DeliveryPhase, StatusCode, TransportFailure};
186
187    struct FixedText {
188        bytes: [u8; 96],
189        len: usize,
190    }
191
192    impl FixedText {
193        const fn new() -> Self {
194            Self {
195                bytes: [0_u8; 96],
196                len: 0,
197            }
198        }
199
200        fn as_str(&self) -> &str {
201            core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
202        }
203    }
204
205    impl Write for FixedText {
206        fn write_str(&mut self, value: &str) -> fmt::Result {
207            let end = self.len.checked_add(value.len()).ok_or(fmt::Error)?;
208            let output = self.bytes.get_mut(self.len..end).ok_or(fmt::Error)?;
209            output.copy_from_slice(value.as_bytes());
210            self.len = end;
211            Ok(())
212        }
213    }
214
215    #[test]
216    fn unknown_delivery_fails_closed_and_debug_redacts_the_error() {
217        let failure = TransportFailure::unknown("secret payload");
218        assert_eq!(failure.phase(), DeliveryPhase::PossiblySent);
219        let mut debug = FixedText::new();
220        assert!(write!(&mut debug, "{failure:?}").is_ok());
221        assert!(debug.as_str().contains("PossiblySent"));
222        assert!(!debug.as_str().contains("secret payload"));
223    }
224
225    #[test]
226    fn observed_status_survives_mapping_without_exposing_error_payload() {
227        let failure = TransportFailure::response_started_with_status(
228            StatusCode::new(401).unwrap_or(StatusCode::OK),
229            "secret payload",
230        )
231        .map(|_| 7_u8);
232        assert_eq!(failure.phase(), DeliveryPhase::ResponseStarted);
233        assert_eq!(failure.observed_status().map(StatusCode::get), Some(401));
234        assert_eq!(
235            DeliveryClassified::observed_status(&failure),
236            failure.observed_status()
237        );
238        assert_eq!(*failure.error(), 7);
239    }
240}