Skip to main content

youtube_uploader/
error.rs

1/// Errors that can occur during video upload.
2///
3/// All variants are non-exhaustive to allow future additions without breaking changes.
4#[derive(thiserror::Error, Debug)]
5#[non_exhaustive]
6pub enum UploadError {
7    #[error("HTTP request failed: {0}")]
8    Http(#[from] reqwest::Error),
9
10    #[error("I/O error: {0}")]
11    Io(#[from] std::io::Error),
12
13    #[error("Authentication failed: {0}")]
14    Auth(String),
15
16    #[error("Token refresh failed: {0}")]
17    TokenRefresh(String),
18
19    #[error("Platform API error ({status}): {message}")]
20    PlatformApi { status: u16, message: String },
21
22    #[error("Upload interrupted after {uploaded} of {total} bytes")]
23    Interrupted { uploaded: u64, total: u64 },
24
25    #[error("File too large: {size} bytes (max {max})")]
26    FileTooLarge { size: u64, max: u64 },
27
28    #[error("Unsupported file format: {0}")]
29    UnsupportedFormat(String),
30
31    #[error("Configuration error: {0}")]
32    Config(String),
33
34    #[error("Encryption error: {0}")]
35    Encryption(String),
36
37    #[error("Retry exhausted with no attempts made")]
38    NoAttempts,
39}
40
41impl UploadError {
42    /// Returns `true` if this error is likely transient and worth retrying.
43    pub fn is_retryable(&self) -> bool {
44        match self {
45            UploadError::Http(_) => true,
46            UploadError::Interrupted { .. } => true,
47            UploadError::PlatformApi { status, .. } => matches!(status, 500..=504 | 429),
48            _ => false,
49        }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn test_interrupted_error_is_retryable() {
59        let err = UploadError::Interrupted {
60            uploaded: 1000,
61            total: 5000,
62        };
63        assert!(err.is_retryable());
64    }
65
66    #[test]
67    fn test_platform_api_5xx_is_retryable() {
68        let err = UploadError::PlatformApi {
69            status: 500,
70            message: "Server error".into(),
71        };
72        assert!(err.is_retryable());
73
74        let err = UploadError::PlatformApi {
75            status: 502,
76            message: "Bad gateway".into(),
77        };
78        assert!(err.is_retryable());
79
80        let err = UploadError::PlatformApi {
81            status: 503,
82            message: "Service unavailable".into(),
83        };
84        assert!(err.is_retryable());
85
86        let err = UploadError::PlatformApi {
87            status: 429,
88            message: "Rate limited".into(),
89        };
90        assert!(err.is_retryable());
91    }
92
93    #[test]
94    fn test_platform_api_4xx_not_retryable() {
95        let err = UploadError::PlatformApi {
96            status: 400,
97            message: "Bad request".into(),
98        };
99        assert!(!err.is_retryable());
100
101        let err = UploadError::PlatformApi {
102            status: 401,
103            message: "Unauthorized".into(),
104        };
105        assert!(!err.is_retryable());
106
107        let err = UploadError::PlatformApi {
108            status: 404,
109            message: "Not found".into(),
110        };
111        assert!(!err.is_retryable());
112    }
113
114    #[test]
115    fn test_auth_error_not_retryable() {
116        let err = UploadError::Auth("Invalid credentials".into());
117        assert!(!err.is_retryable());
118    }
119
120    #[test]
121    fn test_io_error_not_retryable() {
122        let err = UploadError::Io(std::io::Error::new(
123            std::io::ErrorKind::NotFound,
124            "File not found",
125        ));
126        assert!(!err.is_retryable());
127    }
128
129    #[test]
130    fn test_encryption_error_not_retryable() {
131        let err = UploadError::Encryption("failed to decrypt".into());
132        assert!(!err.is_retryable());
133    }
134
135    #[test]
136    fn test_no_attempts_error_not_retryable() {
137        let err = UploadError::NoAttempts;
138        assert!(!err.is_retryable());
139    }
140}