docker_image_pusher/error/
handlers.rs

1//! Standardized error handling patterns to eliminate duplication
2
3use crate::error::{PusherError, Result};
4use reqwest::StatusCode;
5use std::time::Duration;
6
7/// Standard error handler for HTTP responses
8pub struct HttpErrorHandler;
9
10impl HttpErrorHandler {
11    /// Handle upload-related HTTP errors with standardized messages
12    pub fn handle_upload_error(status: StatusCode, error_text: &str, context: &str) -> PusherError {
13        let error_msg = match status.as_u16() {
14            400 => {
15                if error_text.contains("exist blob require digest") {
16                    format!(
17                        "Digest validation failed - Registry reports blob exists but digest mismatch: {}",
18                        error_text
19                    )
20                } else if error_text.contains("BAD_REQUEST") {
21                    format!(
22                        "Bad request - Check digest format and data integrity: {}",
23                        error_text
24                    )
25                } else {
26                    format!("Bad request during {}: {}", context, error_text)
27                }
28            }
29            401 => format!("Authentication failed during {}: {}", context, error_text),
30            403 => format!("Permission denied for {}: {}", context, error_text),
31            404 => format!(
32                "Repository not found or {} session expired: {}",
33                context, error_text
34            ),
35            409 => format!(
36                "Conflict - Blob already exists with different digest: {}",
37                error_text
38            ),
39            413 => format!("File too large for {}: {}", context, error_text),
40            422 => format!("Invalid digest or data for {}: {}", context, error_text),
41            500 => format!("Registry server error during {}: {}", context, error_text),
42            502 | 503 => format!("Registry unavailable during {}: {}", context, error_text),
43            507 => format!("Registry out of storage during {}: {}", context, error_text),
44            508 => format!("Streaming {} timeout: {}", context, error_text),
45            _ => format!("{} failed (status {}): {}", context, status, error_text),
46        };
47
48        PusherError::Upload(error_msg)
49    }
50
51    /// Handle authentication-related HTTP errors
52    pub fn handle_auth_error(status: StatusCode, error_text: &str) -> PusherError {
53        let error_msg = match status.as_u16() {
54            400 => "Invalid token request parameters".to_string(),
55            401 => "Invalid credentials provided".to_string(),
56            403 => "Access denied - insufficient permissions".to_string(),
57            404 => "Authentication endpoint not found".to_string(),
58            _ => format!("Authentication failed (status {}): {}", status, error_text),
59        };
60
61        PusherError::Authentication(error_msg)
62    }
63
64    /// Handle registry-related HTTP errors
65    pub fn handle_registry_error(
66        status: StatusCode,
67        error_text: &str,
68        operation: &str,
69    ) -> PusherError {
70        let error_msg = match status.as_u16() {
71            401 => format!(
72                "Unauthorized to perform {} operation: {}",
73                operation, error_text
74            ),
75            403 => format!(
76                "Forbidden: insufficient permissions for {}: {}",
77                operation, error_text
78            ),
79            404 => format!("Resource not found for {}: {}", operation, error_text),
80            429 => format!("Rate limited during {}: {}", operation, error_text),
81            500 => format!("Registry server error during {}: {}", operation, error_text),
82            502 | 503 => format!("Registry unavailable for {}: {}", operation, error_text),
83            _ => format!("{} failed (status {}): {}", operation, status, error_text),
84        };
85
86        PusherError::Registry(error_msg)
87    }
88
89    /// Handle streaming upload specific errors
90    pub fn handle_streaming_error(status: StatusCode, error_text: &str) -> PusherError {
91        let error_msg = match status.as_u16() {
92            400 => {
93                if error_text.contains("DIGEST_INVALID") {
94                    "Digest validation failed - Registry reports uploaded content doesn't match expected digest".to_string()
95                } else {
96                    format!("Bad request during streaming upload: {}", error_text)
97                }
98            }
99            413 => "File too large for registry".to_string(),
100            507 => "Insufficient storage space on registry".to_string(),
101            401 => "Authentication failed during upload".to_string(),
102            403 => "Permission denied for upload".to_string(),
103            408 | 504 => "Streaming upload timeout".to_string(),
104            500 => {
105                if error_text.contains("s3aws") || error_text.contains("DriverName") {
106                    format!(
107                        "Registry storage backend error (S3): {}. This is typically a temporary issue with the registry's storage system",
108                        error_text
109                    )
110                } else {
111                    format!("Registry internal server error: {}", error_text)
112                }
113            }
114            502 | 503 => format!("Registry temporarily unavailable: {}", error_text),
115            _ => format!(
116                "Streaming upload failed (status {}): {}",
117                status, error_text
118            ),
119        };
120
121        PusherError::Upload(error_msg)
122    }
123
124    /// Check if error indicates a storage backend issue that might be temporary
125    pub fn is_storage_backend_error(error_text: &str) -> bool {
126        error_text.contains("s3aws")
127            || error_text.contains("DriverName")
128            || error_text.contains("storage backend")
129            || error_text.contains("500 Internal Server Error")
130    }
131
132    /// Get suggested retry delay for storage backend errors (exponential backoff)
133    pub fn get_storage_error_retry_delay(attempt: u32, base_delay_secs: u64) -> Duration {
134        let backoff_multiplier = 2_u64.pow(attempt.min(4));
135        Duration::from_secs(base_delay_secs * backoff_multiplier)
136    }
137}
138
139/// Network error categorization and handling
140pub struct NetworkErrorHandler;
141
142impl NetworkErrorHandler {
143    /// Categorize and format network errors with helpful context
144    pub fn handle_network_error(error: &reqwest::Error, context: &str) -> PusherError {
145        if error.is_timeout() {
146            PusherError::Timeout(format!("{} timeout: {}", context, error))
147        } else if error.is_connect() {
148            PusherError::Network(format!("Connection error during {}: {}", context, error))
149        } else if error.to_string().contains("dns") {
150            PusherError::Network(format!("DNS resolution error for {}: {}", context, error))
151        } else if error.to_string().contains("certificate") {
152            PusherError::Network(format!(
153                "TLS certificate error during {}: {}",
154                context, error
155            ))
156        } else {
157            PusherError::Network(format!("{} network error: {}", context, error))
158        }
159    }
160}
161
162/// Validation error utilities
163pub struct ValidationErrorHandler;
164
165impl ValidationErrorHandler {
166    /// Standard file validation error messages
167    pub fn validate_file_path(file_path: &str) -> Result<()> {
168        use std::path::Path;
169
170        let path = Path::new(file_path);
171
172        if !path.exists() {
173            return Err(PusherError::Validation(format!(
174                "Input file does not exist: {}",
175                file_path
176            )));
177        }
178
179        if !path.is_file() {
180            return Err(PusherError::Validation(format!(
181                "Input path is not a file: {}",
182                file_path
183            )));
184        }
185
186        // Check file extension
187        let extension = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
188
189        if !matches!(extension.to_lowercase().as_str(), "tar" | "tar.gz" | "tgz") {
190            return Err(PusherError::Validation(format!(
191                "Input file must be a tar archive (.tar, .tar.gz, or .tgz): {}",
192                file_path
193            )));
194        }
195
196        Ok(())
197    }
198
199    /// Standard URL validation
200    pub fn validate_repository_url(url: &str) -> Result<()> {
201        if url.is_empty() {
202            return Err(PusherError::Validation(
203                "Repository URL cannot be empty".to_string(),
204            ));
205        }
206
207        if !url.contains("://") {
208            return Err(PusherError::Validation(
209                "Repository URL must include protocol (http:// or https://)".to_string(),
210            ));
211        }
212
213        Ok(())
214    }
215
216    /// Standard credential validation
217    pub fn validate_credentials(
218        username: &Option<String>,
219        password: &Option<String>,
220    ) -> Result<()> {
221        match (username, password) {
222            (Some(_), None) => Err(PusherError::Validation(
223                "Password is required when username is provided".to_string(),
224            )),
225            (None, Some(_)) => Err(PusherError::Validation(
226                "Username is required when password is provided".to_string(),
227            )),
228            _ => Ok(()), // Both provided or both None is fine
229        }
230    }
231
232    /// Standard numeric range validation
233    pub fn validate_timeout(timeout: u64) -> Result<()> {
234        if timeout == 0 {
235            return Err(PusherError::Validation(
236                "Timeout must be greater than 0".to_string(),
237            ));
238        }
239
240        if timeout > 86400 {
241            // 24 hours
242            return Err(PusherError::Validation(
243                "Timeout cannot exceed 24 hours (86400 seconds)".to_string(),
244            ));
245        }
246
247        Ok(())
248    }
249}
250
251/// Macro for standardizing error context
252#[macro_export]
253macro_rules! with_context {
254    ($result:expr, $context:expr) => {
255        $result.map_err(|e| match e {
256            PusherError::Config(msg) => PusherError::Config(format!("{}: {}", $context, msg)),
257            PusherError::Authentication(msg) => {
258                PusherError::Authentication(format!("{}: {}", $context, msg))
259            }
260            PusherError::Network(msg) => PusherError::Network(format!("{}: {}", $context, msg)),
261            PusherError::Upload(msg) => PusherError::Upload(format!("{}: {}", $context, msg)),
262            PusherError::Io(msg) => PusherError::Io(format!("{}: {}", $context, msg)),
263            PusherError::Parse(msg) => PusherError::Parse(format!("{}: {}", $context, msg)),
264            PusherError::Registry(msg) => PusherError::Registry(format!("{}: {}", $context, msg)),
265            PusherError::ImageParsing(msg) => {
266                PusherError::ImageParsing(format!("{}: {}", $context, msg))
267            }
268            PusherError::Validation(msg) => {
269                PusherError::Validation(format!("{}: {}", $context, msg))
270            }
271            PusherError::Timeout(msg) => PusherError::Timeout(format!("{}: {}", $context, msg)),
272        })
273    };
274}