docker_image_pusher/error/
handlers.rs

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