braid_http/error.rs
1//! Error types for Braid HTTP operations.
2//!
3//! This module defines all error types that can occur when using the Braid-HTTP protocol.
4//! The [`Result`] type alias provides a convenient shorthand for operations that may fail.
5//!
6//! # Error Categories
7//!
8//! | Category | Variants | Retryable |
9//! |----------|----------|-----------|
10//! | Protocol | `HeaderParse`, `BodyParse`, `InvalidVersion` | No |
11//! | Network | `Io`, `Timeout` | Yes |
12//! | Subscription | `SubscriptionClosed`, `InvalidSubscriptionStatus` | Depends |
13//! | Conflict | `MergeConflict`, `HistoryDropped` | No |
14//! | Configuration | `Config` | No |
15//!
16//! # Error Recovery
17//!
18//! Use the [`BraidError::is_retryable()`] method to determine if an operation should be retried.
19//! Most network errors are retryable; protocol errors typically are not.
20//!
21//! # Specification
22//!
23//! See Section 4.5 of [draft-toomim-httpbis-braid-http-04] for error handling requirements.
24//!
25//! [draft-toomim-httpbis-braid-http-04]: https://datatracker.ietf.org/doc/html/draft-toomim-httpbis-braid-http
26
27use std::io;
28use thiserror::Error;
29
30/// Result type for Braid HTTP operations.
31///
32/// Provides a convenient shorthand for `Result<T, BraidError>`.
33pub type Result<T> = std::result::Result<T, BraidError>;
34
35/// Errors that can occur during Braid HTTP operations.
36///
37/// Each variant represents a different failure mode when working with Braid protocol.
38/// Use pattern matching to handle specific errors appropriately.
39#[derive(Error, Debug)]
40#[non_exhaustive]
41pub enum BraidError {
42 /// HTTP request failed with a given error message.
43 #[error("HTTP error: {0}")]
44 Http(String),
45
46 /// Network I/O error (connection failed, read/write error, etc.).
47 ///
48 /// These errors are typically retryable.
49 #[error("I/O error: {0}")]
50 Io(#[from] io::Error),
51
52 /// Failed to parse Braid protocol headers.
53 #[error("Header parse error: {0}")]
54 HeaderParse(String),
55
56 /// Failed to parse protocol body.
57 #[error("Body parse error: {0}")]
58 BodyParse(String),
59
60 /// Invalid version format (not a valid version identifier).
61 #[error("Invalid version: {0}")]
62 InvalidVersion(String),
63
64 /// Subscription-specific error.
65 #[error("Subscription error: {0}")]
66 Subscription(String),
67
68 /// JSON serialization or deserialization error.
69 #[error("JSON error: {0}")]
70 Json(#[from] serde_json::Error),
71
72 /// Subscription closed unexpectedly.
73 #[error("Subscription closed")]
74 SubscriptionClosed,
75
76 /// Invalid subscription status (expected 209)
77 #[error("Expected status 209 for subscription, got {0}")]
78 InvalidSubscriptionStatus(u16),
79
80 /// Protocol violation error
81 #[error("Protocol error: {0}")]
82 Protocol(String),
83
84 /// Operation timed out.
85 #[error("Operation timed out")]
86 Timeout,
87
88 /// Request was aborted.
89 #[error("Request aborted")]
90 Aborted,
91
92 /// Filesystem error (notify, mapping, etc.)
93 #[error("BraidFS Error: {0}")]
94 Fs(String),
95
96 /// Invalid UTF-8 sequence in response.
97 #[error("Invalid UTF-8: {0}")]
98 InvalidUtf8(#[from] std::string::FromUtf8Error),
99
100 /// Configuration error in Braid setup.
101 #[error("Configuration error: {0}")]
102 Config(String),
103
104 /// Internal error in the library.
105 #[error("Internal error: {0}")]
106 Internal(String),
107
108 /// Server has dropped version history (HTTP 410 Gone - Section 4.5).
109 #[error("Server has dropped history - cannot resume synchronization")]
110 HistoryDropped,
111
112 /// Conflicting versions detected in merge (HTTP 293 - Section 2.2).
113 #[error("Conflicting versions in merge: {0}")]
114 MergeConflict(String),
115
116 /// Generic anyhow error (temporary for conversion).
117 #[error("Anyhow error: {0}")]
118 Anyhow(String),
119}
120
121impl BraidError {
122 /// Check if this error is retryable.
123 ///
124 /// Returns `true` for transient errors that may succeed on retry:
125 /// - Network timeouts
126 /// - I/O errors
127 /// - HTTP 408, 425, 429, 502, 503, 504
128 ///
129 /// Returns `false` for permanent errors.
130 #[inline]
131 #[must_use]
132 pub fn is_retryable(&self) -> bool {
133 match self {
134 BraidError::Http(msg) => {
135 msg.contains("408")
136 || msg.contains("425")
137 || msg.contains("429")
138 || msg.contains("502")
139 || msg.contains("503")
140 || msg.contains("504")
141 }
142 BraidError::Timeout | BraidError::Io(_) => true,
143 BraidError::HistoryDropped => false,
144 _ => false,
145 }
146 }
147
148 /// Check if this is an access denied error.
149 ///
150 /// Returns `true` for HTTP 401 (Unauthorized) or 403 (Forbidden).
151 #[inline]
152 #[must_use]
153 pub fn is_access_denied(&self) -> bool {
154 match self {
155 BraidError::Http(msg) => msg.contains("401") || msg.contains("403"),
156 _ => false,
157 }
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_timeout_is_retryable() {
167 assert!(BraidError::Timeout.is_retryable());
168 }
169
170 #[test]
171 fn test_history_dropped_not_retryable() {
172 assert!(!BraidError::HistoryDropped.is_retryable());
173 }
174
175 #[test]
176 fn test_http_503_is_retryable() {
177 let err = BraidError::Http("503 Service Unavailable".into());
178 assert!(err.is_retryable());
179 }
180
181 #[test]
182 fn test_access_denied_401() {
183 let err = BraidError::Http("401 Unauthorized".into());
184 assert!(err.is_access_denied());
185 }
186}