Skip to main content

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}