Skip to main content

atproto_tap/
errors.rs

1//! Error types for TAP operations.
2//!
3//! This module defines the error types returned by TAP stream and client operations.
4
5use thiserror::Error;
6
7/// Errors that can occur during TAP operations.
8#[derive(Debug, Error)]
9pub enum TapError {
10    /// WebSocket connection failed.
11    #[error("error-atproto-tap-connection-1 WebSocket connection failed: {0}")]
12    ConnectionFailed(String),
13
14    /// Connection was closed unexpectedly.
15    #[error("error-atproto-tap-connection-2 Connection closed unexpectedly")]
16    ConnectionClosed,
17
18    /// Maximum reconnection attempts exceeded.
19    #[error(
20        "error-atproto-tap-connection-3 Maximum reconnection attempts exceeded after {0} attempts"
21    )]
22    MaxReconnectAttemptsExceeded(u32),
23
24    /// Authentication failed.
25    #[error("error-atproto-tap-auth-1 Authentication failed: {0}")]
26    AuthenticationFailed(String),
27
28    /// Failed to parse a message from the server.
29    #[error("error-atproto-tap-parse-1 Failed to parse message: {0}")]
30    ParseError(String),
31
32    /// Failed to send an acknowledgment.
33    #[error("error-atproto-tap-ack-1 Failed to send acknowledgment: {0}")]
34    AckFailed(String),
35
36    /// HTTP request failed.
37    #[error("error-atproto-tap-http-1 HTTP request failed: {0}")]
38    HttpError(String),
39
40    /// HTTP response indicated an error.
41    #[error("error-atproto-tap-http-2 HTTP error response: {status} - {message}")]
42    HttpResponseError {
43        /// HTTP status code.
44        status: u16,
45        /// Error message from response.
46        message: String,
47    },
48
49    /// Invalid URL.
50    #[error("error-atproto-tap-url-1 Invalid URL: {0}")]
51    InvalidUrl(String),
52
53    /// I/O error.
54    #[error("error-atproto-tap-io-1 I/O error: {0}")]
55    IoError(#[from] std::io::Error),
56
57    /// JSON serialization/deserialization error.
58    #[error("error-atproto-tap-json-1 JSON error: {0}")]
59    JsonError(#[from] serde_json::Error),
60
61    /// Stream has been closed and cannot be used.
62    #[error("error-atproto-tap-stream-1 Stream is closed")]
63    StreamClosed,
64
65    /// Operation timed out.
66    #[error("error-atproto-tap-timeout-1 Operation timed out")]
67    Timeout,
68}
69
70impl TapError {
71    /// Returns true if this error indicates a connection issue that may be recoverable.
72    pub fn is_connection_error(&self) -> bool {
73        matches!(
74            self,
75            TapError::ConnectionFailed(_)
76                | TapError::ConnectionClosed
77                | TapError::IoError(_)
78                | TapError::Timeout
79        )
80    }
81
82    /// Returns true if this error is a parse error that doesn't affect connection state.
83    pub fn is_parse_error(&self) -> bool {
84        matches!(self, TapError::ParseError(_) | TapError::JsonError(_))
85    }
86
87    /// Returns true if this error is fatal and the stream should not attempt recovery.
88    pub fn is_fatal(&self) -> bool {
89        matches!(
90            self,
91            TapError::MaxReconnectAttemptsExceeded(_)
92                | TapError::AuthenticationFailed(_)
93                | TapError::StreamClosed
94        )
95    }
96}
97
98impl From<reqwest::Error> for TapError {
99    fn from(err: reqwest::Error) -> Self {
100        if err.is_timeout() {
101            TapError::Timeout
102        } else if err.is_connect() {
103            TapError::ConnectionFailed(err.to_string())
104        } else {
105            TapError::HttpError(err.to_string())
106        }
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_error_classification() {
116        assert!(TapError::ConnectionFailed("test".into()).is_connection_error());
117        assert!(TapError::ConnectionClosed.is_connection_error());
118        assert!(TapError::Timeout.is_connection_error());
119
120        assert!(TapError::ParseError("test".into()).is_parse_error());
121        assert!(
122            TapError::JsonError(serde_json::from_str::<()>("invalid").unwrap_err())
123                .is_parse_error()
124        );
125
126        assert!(TapError::MaxReconnectAttemptsExceeded(5).is_fatal());
127        assert!(TapError::AuthenticationFailed("test".into()).is_fatal());
128        assert!(TapError::StreamClosed.is_fatal());
129
130        // Non-fatal errors
131        assert!(!TapError::ConnectionFailed("test".into()).is_fatal());
132        assert!(!TapError::ParseError("test".into()).is_fatal());
133    }
134
135    #[test]
136    fn test_error_display() {
137        let err = TapError::ConnectionFailed("refused".to_string());
138        assert!(err.to_string().contains("error-atproto-tap-connection-1"));
139        assert!(err.to_string().contains("refused"));
140
141        let err = TapError::HttpResponseError {
142            status: 404,
143            message: "Not Found".to_string(),
144        };
145        assert!(err.to_string().contains("404"));
146        assert!(err.to_string().contains("Not Found"));
147    }
148}