Skip to main content

tea_protocol/
error.rs

1use serde::{Deserialize, Deserializer, Serialize, Serializer};
2
3use crate::envelope::deserialize_unique_value;
4use thiserror::Error;
5
6use crate::{
7    CURRENT_PROTOCOL_VERSION, CorrelationId, ProtocolMetadata, ProtocolMetadataError,
8    ProtocolVersion,
9};
10
11/// Maximum UTF-8 bytes in an English technical error message.
12pub const MAX_ERROR_MESSAGE_BYTES: usize = 4096;
13
14/// Stable machine-readable protocol error code.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum AgentErrorCode {
18    /// The command discriminator is not supported by this host.
19    UnsupportedCommand,
20    /// A required durable record discriminator is not supported.
21    UnsupportedRecord,
22    /// The protocol version is outside the supported range.
23    UnsupportedProtocolVersion,
24    /// A known command has invalid structure or state.
25    InvalidCommand,
26    /// Input failed canonical validation.
27    InvalidInput,
28    /// An append or transition used a stale expected sequence.
29    SequenceConflict,
30    /// An upstream or host rate limit rejected the request.
31    RateLimited,
32    /// A provider is temporarily unavailable.
33    ProviderUnavailable,
34    /// The operation was cancelled.
35    Cancelled,
36    /// An unexpected internal failure occurred.
37    Internal,
38}
39
40/// Whether and how a caller may retry an operation.
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "snake_case")]
43pub enum RetryClass {
44    /// Retrying the same operation is not safe or useful.
45    Never,
46    /// The same operation may be retried immediately.
47    Immediate,
48    /// The operation may be retried after bounded backoff.
49    AfterBackoff,
50}
51
52/// A safe, localizable protocol error payload.
53#[derive(Debug, Clone, PartialEq)]
54pub struct ProtocolError {
55    code: AgentErrorCode,
56    message: String,
57    retry: RetryClass,
58    correlation_id: Option<CorrelationId>,
59    details: ProtocolMetadata,
60}
61
62impl ProtocolError {
63    /// Creates a validated protocol error without serializing an internal source chain.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error when the technical message is empty, contains a null
68    /// character, or exceeds [`MAX_ERROR_MESSAGE_BYTES`].
69    pub fn new(
70        code: AgentErrorCode,
71        message: impl Into<String>,
72        retry: RetryClass,
73    ) -> Result<Self, ProtocolErrorValidationError> {
74        let message = message.into();
75        validate_message(&message)?;
76        Ok(Self {
77            code,
78            message,
79            retry,
80            correlation_id: None,
81            details: ProtocolMetadata::default(),
82        })
83    }
84
85    pub(crate) fn invalid_command(correlation_id: CorrelationId) -> Self {
86        Self {
87            code: AgentErrorCode::InvalidCommand,
88            message: "command envelope is invalid".to_owned(),
89            retry: RetryClass::Never,
90            correlation_id: Some(correlation_id),
91            details: ProtocolMetadata::default(),
92        }
93    }
94
95    pub(crate) fn invalid_record(correlation_id: CorrelationId) -> Self {
96        Self {
97            code: AgentErrorCode::InvalidInput,
98            message: "durable session record is invalid".to_owned(),
99            retry: RetryClass::Never,
100            correlation_id: Some(correlation_id),
101            details: ProtocolMetadata::default(),
102        }
103    }
104
105    pub(crate) fn unsupported_protocol_version(
106        correlation_id: CorrelationId,
107        received_version: ProtocolVersion,
108    ) -> Self {
109        Self {
110            code: AgentErrorCode::UnsupportedProtocolVersion,
111            message: "protocol version is not supported".to_owned(),
112            retry: RetryClass::Never,
113            correlation_id: Some(correlation_id),
114            details: ProtocolMetadata::protocol_version_details(&received_version.to_string()),
115        }
116    }
117
118    pub(crate) fn unsupported_record(correlation_id: CorrelationId, record_type: &str) -> Self {
119        Self {
120            code: AgentErrorCode::UnsupportedRecord,
121            message: "durable record type is not supported".to_owned(),
122            retry: RetryClass::Never,
123            correlation_id: Some(correlation_id),
124            details: ProtocolMetadata::protocol_compatibility_details(Some(record_type)),
125        }
126    }
127
128    /// Creates the canonical unsupported-command error for protocol 1.x.
129    #[must_use]
130    pub fn unsupported_command(correlation_id: CorrelationId) -> Self {
131        let details = ProtocolMetadata::protocol_compatibility_details(None);
132        Self {
133            code: AgentErrorCode::UnsupportedCommand,
134            message: "command type is not supported".to_owned(),
135            retry: RetryClass::Never,
136            correlation_id: Some(correlation_id),
137            details,
138        }
139    }
140
141    /// Adds a diagnostic correlation identifier.
142    #[must_use]
143    pub fn with_correlation_id(mut self, correlation_id: CorrelationId) -> Self {
144        self.correlation_id = Some(correlation_id);
145        self
146    }
147
148    /// Adds validated, bounded safe details.
149    #[must_use]
150    pub fn with_details(mut self, details: ProtocolMetadata) -> Self {
151        self.details = details;
152        self
153    }
154
155    /// Returns the stable error code.
156    #[must_use]
157    pub const fn code(&self) -> AgentErrorCode {
158        self.code
159    }
160
161    /// Returns the English technical diagnostic message.
162    #[must_use]
163    pub fn message(&self) -> &str {
164        &self.message
165    }
166
167    /// Returns the retry classification.
168    #[must_use]
169    pub const fn retry(&self) -> RetryClass {
170        self.retry
171    }
172
173    /// Returns the optional diagnostic correlation identifier.
174    #[must_use]
175    pub const fn correlation_id(&self) -> Option<&CorrelationId> {
176        self.correlation_id.as_ref()
177    }
178
179    /// Returns bounded safe structured details.
180    #[must_use]
181    pub const fn details(&self) -> &ProtocolMetadata {
182        &self.details
183    }
184
185    fn validate(&self) -> Result<(), ProtocolErrorValidationError> {
186        validate_message(&self.message)
187    }
188}
189
190#[derive(Serialize)]
191#[serde(rename_all = "camelCase")]
192struct SerializableProtocolError<'a> {
193    code: AgentErrorCode,
194    message: &'a str,
195    retry: RetryClass,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    correlation_id: &'a Option<CorrelationId>,
198    #[serde(skip_serializing_if = "ProtocolMetadata::is_empty")]
199    details: &'a ProtocolMetadata,
200}
201
202impl Serialize for ProtocolError {
203    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
204    where
205        S: Serializer,
206    {
207        self.validate().map_err(serde::ser::Error::custom)?;
208        SerializableProtocolError {
209            code: self.code,
210            message: &self.message,
211            retry: self.retry,
212            correlation_id: &self.correlation_id,
213            details: &self.details,
214        }
215        .serialize(serializer)
216    }
217}
218
219#[derive(Deserialize)]
220#[serde(rename_all = "camelCase")]
221struct RawProtocolError {
222    code: AgentErrorCode,
223    message: String,
224    retry: RetryClass,
225    #[serde(default)]
226    correlation_id: Option<CorrelationId>,
227    #[serde(default)]
228    details: ProtocolMetadata,
229}
230
231impl<'de> Deserialize<'de> for ProtocolError {
232    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
233    where
234        D: Deserializer<'de>,
235    {
236        let raw = RawProtocolError::deserialize(deserializer)?;
237        let mut error = Self::new(raw.code, raw.message, raw.retry)
238            .map_err(serde::de::Error::custom)?
239            .with_details(raw.details);
240        error.correlation_id = raw.correlation_id;
241        Ok(error)
242    }
243}
244
245/// Versioned protocol-error transport envelope.
246#[derive(Debug, Clone, PartialEq, Serialize)]
247#[serde(rename_all = "camelCase")]
248pub struct ProtocolErrorEnvelope {
249    protocol_version: ProtocolVersion,
250    #[serde(rename = "type")]
251    kind: ProtocolErrorEnvelopeType,
252    error: ProtocolError,
253}
254
255impl ProtocolErrorEnvelope {
256    /// Wraps an error in the current protocol envelope.
257    #[must_use]
258    pub const fn new(error: ProtocolError) -> Self {
259        Self {
260            protocol_version: CURRENT_PROTOCOL_VERSION,
261            kind: ProtocolErrorEnvelopeType::ProtocolError,
262            error,
263        }
264    }
265
266    /// Returns the envelope protocol version.
267    #[must_use]
268    pub const fn protocol_version(&self) -> ProtocolVersion {
269        self.protocol_version
270    }
271
272    /// Returns the safe error payload.
273    #[must_use]
274    pub const fn error(&self) -> &ProtocolError {
275        &self.error
276    }
277}
278
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
280#[serde(rename_all = "snake_case")]
281enum ProtocolErrorEnvelopeType {
282    ProtocolError,
283}
284
285#[derive(Deserialize)]
286#[serde(rename_all = "camelCase")]
287struct RawProtocolErrorEnvelope {
288    protocol_version: ProtocolVersion,
289    #[serde(rename = "type")]
290    kind: ProtocolErrorEnvelopeType,
291    error: ProtocolError,
292}
293
294impl<'de> Deserialize<'de> for ProtocolErrorEnvelope {
295    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
296    where
297        D: Deserializer<'de>,
298    {
299        let value = deserialize_unique_value(deserializer)?;
300        let raw = RawProtocolErrorEnvelope::deserialize(value).map_err(serde::de::Error::custom)?;
301        if raw.protocol_version.major() != CURRENT_PROTOCOL_VERSION.major() {
302            return Err(serde::de::Error::custom(
303                "unsupported protocol major version",
304            ));
305        }
306        Ok(Self {
307            protocol_version: raw.protocol_version,
308            kind: raw.kind,
309            error: raw.error,
310        })
311    }
312}
313
314/// Error returned when validating a safe protocol error.
315#[derive(Debug, Error)]
316pub enum ProtocolErrorValidationError {
317    /// The technical message is empty, oversized, or contains a null character.
318    #[error("technical error message is invalid")]
319    InvalidMessage,
320    /// Safe details failed metadata validation.
321    #[error("safe error details are invalid: {0}")]
322    InvalidDetails(#[from] ProtocolMetadataError),
323}
324
325fn validate_message(message: &str) -> Result<(), ProtocolErrorValidationError> {
326    if message.is_empty()
327        || message.len() > MAX_ERROR_MESSAGE_BYTES
328        || message.contains('\0')
329        || message.chars().any(|character| character == '\r')
330    {
331        Err(ProtocolErrorValidationError::InvalidMessage)
332    } else {
333        Ok(())
334    }
335}