Skip to main content

alien_bindings/
error.rs

1use alien_error::{AlienError, AlienErrorData, ContextError};
2use serde::{Deserialize, Serialize};
3
4/// Derives the exact `ALIEN_<NAME>_BINDING` environment variable name that would have
5/// configured a binding with the given name — the same derivation `alien-core` uses to
6/// generate binding env vars, so this is guaranteed to match `parse_bindings_from_env`'s
7/// reverse parsing (see `provider.rs`).
8pub fn binding_env_var(binding_name: &str) -> String {
9    alien_core::bindings::binding_env_var_name(binding_name)
10}
11
12/// Errors related to alien-bindings operations.
13#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum ErrorData {
16    /// No binding configuration was found for the requested binding name (the
17    /// `ALIEN_<NAME>_BINDING` environment variable was not set).
18    #[error(
19        code = "BINDING_NOT_CONFIGURED",
20        message = "No binding configured for '{binding_name}': environment variable '{env_var}' is not set",
21        retryable = "false",
22        internal = "false",
23        http_status_code = 400
24    )]
25    BindingNotConfigured {
26        /// Name of the binding that was requested
27        binding_name: String,
28        /// The exact environment variable name that would configure this binding
29        env_var: String,
30    },
31
32    /// Binding provider configuration is invalid or missing.
33    #[error(
34        code = "BINDING_CONFIG_INVALID",
35        message = "Binding configuration invalid for binding '{binding_name}' (env var '{env_var}'): {reason}",
36        retryable = "false",
37        internal = "false",
38        http_status_code = 400
39    )]
40    BindingConfigInvalid {
41        /// Name of the binding
42        binding_name: String,
43        /// The exact environment variable name that configures this binding
44        env_var: String,
45        /// Specific reason why the configuration is invalid
46        reason: String,
47    },
48
49    /// Binding configuration named a provider that this build does not support.
50    #[error(
51        code = "UNSUPPORTED_BINDING_PROVIDER",
52        message = "Binding '{binding_name}' (env var '{env_var}') uses unsupported provider '{provider}'",
53        retryable = "false",
54        internal = "false",
55        http_status_code = 501
56    )]
57    UnsupportedBindingProvider {
58        /// Name of the binding
59        binding_name: String,
60        /// The exact environment variable name that configures this binding
61        env_var: String,
62        /// The provider string that is not supported
63        provider: String,
64    },
65
66    /// Storage operation failed due to provider issues.
67    #[error(
68        code = "STORAGE_OPERATION_FAILED",
69        message = "Storage operation failed for binding '{binding_name}': {operation}",
70        retryable = "true",
71        internal = "false",
72        http_status_code = 502
73    )]
74    StorageOperationFailed {
75        /// Name of the storage binding
76        binding_name: String,
77        /// Description of the operation that failed
78        operation: String,
79    },
80
81    /// Build operation failed due to provider issues.
82    #[error(
83        code = "BUILD_OPERATION_FAILED",
84        message = "Build operation failed for binding '{binding_name}': {operation}",
85        retryable = "true",
86        internal = "false",
87        http_status_code = 502
88    )]
89    BuildOperationFailed {
90        /// Name of the build binding
91        binding_name: String,
92        /// Description of the operation that failed
93        operation: String,
94    },
95
96    /// Required environment variable is missing or invalid.
97    #[error(
98        code = "ENVIRONMENT_VARIABLE_MISSING",
99        message = "Required environment variable '{variable_name}' is missing",
100        retryable = "false",
101        internal = "false",
102        http_status_code = 500
103    )]
104    EnvironmentVariableMissing {
105        /// Name of the missing environment variable
106        variable_name: String,
107    },
108
109    /// Environment variable has an invalid value.
110    #[error(
111        code = "INVALID_ENVIRONMENT_VARIABLE",
112        message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
113        retryable = "false",
114        internal = "false",
115        http_status_code = 500
116    )]
117    InvalidEnvironmentVariable {
118        /// Name of the environment variable
119        variable_name: String,
120        /// The invalid value
121        value: String,
122        /// Reason why the value is invalid
123        reason: String,
124    },
125
126    /// Configuration URL is malformed or invalid.
127    #[error(
128        code = "INVALID_CONFIGURATION_URL",
129        message = "Invalid configuration URL '{url}': {reason}",
130        retryable = "false",
131        internal = "false",
132        http_status_code = 400
133    )]
134    InvalidConfigurationUrl {
135        /// The invalid URL
136        url: String,
137        /// Specific reason why the URL is invalid
138        reason: String,
139    },
140
141    /// Event processing failed due to malformed or unsupported data.
142    #[error(
143        code = "EVENT_PROCESSING_FAILED",
144        message = "Event processing failed for type '{event_type}': {reason}",
145        retryable = "true",
146        internal = "false",
147        http_status_code = 400
148    )]
149    EventProcessingFailed {
150        /// Type of event that failed to process
151        event_type: String,
152        /// Specific reason for the processing failure
153        reason: String,
154    },
155
156    /// gRPC connection failed or became unavailable.
157    #[error(
158        code = "GRPC_CONNECTION_FAILED",
159        message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
160        retryable = "true",
161        internal = "false",
162        http_status_code = 502
163    )]
164    GrpcConnectionFailed {
165        /// The gRPC endpoint that failed to connect
166        endpoint: String,
167        /// Reason for the connection failure
168        reason: String,
169    },
170
171    /// gRPC service unavailable or returned error.
172    #[error(
173        code = "GRPC_SERVICE_UNAVAILABLE",
174        message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
175        retryable = "true",
176        internal = "false",
177        http_status_code = 503
178    )]
179    GrpcServiceUnavailable {
180        /// Name of the gRPC service
181        service: String,
182        /// The gRPC endpoint
183        endpoint: String,
184        /// Reason for service unavailability
185        reason: String,
186    },
187
188    /// gRPC request failed with an error status.
189    #[error(
190        code = "GRPC_REQUEST_FAILED",
191        message = "gRPC request to service '{service}' method '{method}' failed: {details}",
192        retryable = "true",
193        internal = "false",
194        http_status_code = 502
195    )]
196    GrpcRequestFailed {
197        /// Name of the gRPC service
198        service: String,
199        /// Name of the gRPC method
200        method: String,
201        /// Error details from the gRPC status
202        details: String,
203    },
204
205    /// Server failed to bind to the specified address.
206    #[error(
207        code = "SERVER_BIND_FAILED",
208        message = "Failed to bind server to address '{address}': {reason}",
209        retryable = "true",
210        internal = "true",
211        http_status_code = 500
212    )]
213    ServerBindFailed {
214        /// The address that failed to bind
215        address: String,
216        /// Reason for the bind failure
217        reason: String,
218    },
219
220    /// Authentication failed for the configured provider.
221    #[error(
222        code = "AUTHENTICATION_FAILED",
223        message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
224        retryable = "true",
225        internal = "false",
226        http_status_code = 401
227    )]
228    AuthenticationFailed {
229        /// Name of the provider (aws, gcp, azure, etc.)
230        provider: String,
231        /// Name of the binding
232        binding_name: String,
233        /// Reason for authentication failure
234        reason: String,
235    },
236
237    /// Operation not supported by the configured provider.
238    #[error(
239        code = "OPERATION_NOT_SUPPORTED",
240        message = "Operation '{operation}' not supported: {reason}",
241        retryable = "false",
242        internal = "false",
243        http_status_code = 501
244    )]
245    OperationNotSupported {
246        /// Name of the unsupported operation
247        operation: String,
248        /// Reason why the operation is not supported
249        reason: String,
250    },
251
252    /// Feature is not enabled in the compiled binary.
253    #[error(
254        code = "FEATURE_NOT_ENABLED",
255        message = "Feature '{feature}' is not enabled in this build",
256        retryable = "false",
257        internal = "false",
258        http_status_code = 501
259    )]
260    FeatureNotEnabled {
261        /// Name of the feature that is not enabled
262        feature: String,
263    },
264
265    /// gRPC call failed.
266    #[error(
267        code = "GRPC_CALL_FAILED",
268        message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
269        retryable = "true",
270        internal = "false",
271        http_status_code = 502
272    )]
273    GrpcCallFailed {
274        /// Name of the gRPC service
275        service: String,
276        /// Name of the gRPC method
277        method: String,
278        /// Reason for the call failure
279        reason: String,
280    },
281
282    /// Deserialization of data failed.
283    #[error(
284        code = "DESERIALIZATION_FAILED",
285        message = "Failed to deserialize {type_name}: {message}",
286        retryable = "false",
287        internal = "false",
288        http_status_code = 400
289    )]
290    DeserializationFailed {
291        /// Human-readable error message
292        message: String,
293        /// Name of the type being deserialized
294        type_name: String,
295    },
296
297    /// Serialization of data failed.
298    #[error(
299        code = "SERIALIZATION_FAILED",
300        message = "Failed to serialize data: {message}",
301        retryable = "false",
302        internal = "false",
303        http_status_code = 500
304    )]
305    SerializationFailed {
306        /// Human-readable error message
307        message: String,
308    },
309
310    /// Response format from provider API is unexpected or missing required fields.
311    #[error(
312        code = "UNEXPECTED_RESPONSE_FORMAT",
313        message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
314        retryable = "false",
315        internal = "false",
316        http_status_code = 502
317    )]
318    UnexpectedResponseFormat {
319        /// Name of the provider (aws, gcp, azure, etc.)
320        provider: String,
321        /// Name of the binding
322        binding_name: String,
323        /// Name of the missing or malformed field
324        field: String,
325        /// The full response JSON for debugging
326        response_json: String,
327    },
328
329    /// Cloud platform API error.
330    #[error(
331        code = "CLOUD_PLATFORM_ERROR",
332        message = "Cloud platform error: {message}",
333        retryable = "true",
334        internal = "false",
335        http_status_code = 502
336    )]
337    CloudPlatformError {
338        /// Human-readable description of the error
339        message: String,
340        /// Optional resource ID that was involved in the error
341        resource_id: Option<String>,
342    },
343
344    /// Resource not found in the cloud platform.
345    #[error(
346        code = "RESOURCE_NOT_FOUND",
347        message = "Resource '{resource_id}' not found",
348        retryable = "false",
349        internal = "false",
350        http_status_code = 404
351    )]
352    ResourceNotFound {
353        /// ID of the resource that was not found
354        resource_id: String,
355    },
356
357    /// The requested remote resource does not exist.
358    #[error(
359        code = "REMOTE_RESOURCE_NOT_FOUND",
360        message = "{operation_context}: {resource_type} '{resource_name}' not found",
361        retryable = "false",
362        internal = "false",
363        http_status_code = 404
364    )]
365    RemoteResourceNotFound {
366        /// Context of the operation that failed (e.g., "Failed to get ECR repository details")
367        operation_context: String,
368        /// Type of the resource that was not found
369        resource_type: String,
370        /// Name of the resource that was not found
371        resource_name: String,
372    },
373
374    /// Operation conflicts with current remote resource state.
375    #[error(
376        code = "REMOTE_RESOURCE_CONFLICT",
377        message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
378        retryable = "true",
379        internal = "false",
380        http_status_code = 409
381    )]
382    RemoteResourceConflict {
383        /// Context of the operation that failed
384        operation_context: String,
385        /// Type of the resource that has a conflict
386        resource_type: String,
387        /// Name of the resource that has a conflict
388        resource_name: String,
389        /// Specific reason for the conflict
390        conflict_reason: String,
391    },
392
393    /// Access denied due to insufficient permissions.
394    #[error(
395        code = "REMOTE_ACCESS_DENIED",
396        message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
397        retryable = "true",
398        internal = "false",
399        http_status_code = 403
400    )]
401    RemoteAccessDenied {
402        /// Context of the operation that failed
403        operation_context: String,
404        /// Type of the resource access was denied to
405        resource_type: String,
406        /// Name of the resource access was denied to
407        resource_name: String,
408    },
409
410    /// Request rate limit exceeded.
411    #[error(
412        code = "RATE_LIMIT_EXCEEDED",
413        message = "{operation_context}: Rate limit exceeded - {details}",
414        retryable = "true",
415        internal = "false",
416        http_status_code = 429
417    )]
418    RateLimitExceeded {
419        /// Context of the operation that failed
420        operation_context: String,
421        /// Additional details about the rate limit
422        details: String,
423    },
424
425    /// Operation exceeded the allowed timeout.
426    #[error(
427        code = "TIMEOUT",
428        message = "{operation_context}: Operation timed out - {details}",
429        retryable = "true",
430        internal = "false",
431        http_status_code = 408
432    )]
433    Timeout {
434        /// Context of the operation that failed
435        operation_context: String,
436        /// Additional details about the timeout
437        details: String,
438    },
439
440    /// Remote service is temporarily unavailable.
441    #[error(
442        code = "REMOTE_SERVICE_UNAVAILABLE",
443        message = "{operation_context}: Service unavailable - {details}",
444        retryable = "true",
445        internal = "false",
446        http_status_code = 503
447    )]
448    RemoteServiceUnavailable {
449        /// Context of the operation that failed
450        operation_context: String,
451        /// Additional details about the service unavailability
452        details: String,
453    },
454
455    /// Quota or resource limits have been exceeded.
456    #[error(
457        code = "QUOTA_EXCEEDED",
458        message = "{operation_context}: Quota exceeded - {details}",
459        retryable = "true",
460        internal = "false",
461        http_status_code = 429
462    )]
463    QuotaExceeded {
464        /// Context of the operation that failed
465        operation_context: String,
466        /// Additional details about the quota violation
467        details: String,
468    },
469
470    /// Invalid or malformed input parameters provided to the operation.
471    #[error(
472        code = "INVALID_INPUT",
473        message = "{operation_context}: Invalid input - {details}",
474        retryable = "false",
475        internal = "false",
476        http_status_code = 400
477    )]
478    InvalidInput {
479        /// Context of the operation that failed
480        operation_context: String,
481        /// Details about what input was invalid
482        details: String,
483        /// Optional field name that was invalid
484        field_name: Option<String>,
485    },
486
487    /// Authentication with cloud provider failed.
488    #[error(
489        code = "AUTHENTICATION_ERROR",
490        message = "{operation_context}: Authentication failed - {details}",
491        retryable = "true",
492        internal = "false",
493        http_status_code = 401
494    )]
495    AuthenticationError {
496        /// Context of the operation that failed
497        operation_context: String,
498        /// Details about the authentication failure
499        details: String,
500    },
501
502    /// Generic bindings error for uncommon cases.
503    #[error(
504        code = "BINDINGS_ERROR",
505        message = "Bindings error: {message}",
506        retryable = "true",
507        internal = "true",
508        http_status_code = 500
509    )]
510    Other {
511        /// Human-readable description of the error
512        message: String,
513    },
514
515    /// Presigned request has expired and can no longer be used.
516    #[error(
517        code = "PRESIGNED_REQUEST_EXPIRED",
518        message = "Presigned request for path '{path}' expired at {expired_at}",
519        retryable = "false",
520        internal = "false",
521        http_status_code = 403
522    )]
523    PresignedRequestExpired {
524        /// Path that the presigned request was for
525        path: String,
526        /// When the request expired
527        expired_at: chrono::DateTime<chrono::Utc>,
528    },
529
530    /// HTTP request to external service failed.
531    #[error(
532        code = "HTTP_REQUEST_FAILED",
533        message = "HTTP {method} request to '{url}' failed",
534        retryable = "true",
535        internal = "false",
536        http_status_code = 502
537    )]
538    HttpRequestFailed {
539        /// URL that was requested
540        url: String,
541        /// HTTP method that was used
542        method: String,
543    },
544
545    /// Local filesystem operation failed.
546    #[error(
547        code = "LOCAL_FILESYSTEM_ERROR",
548        message = "Local filesystem operation '{operation}' failed for path '{path}'",
549        retryable = "true",
550        internal = "false",
551        http_status_code = 500
552    )]
553    LocalFilesystemError {
554        /// Path that the operation was performed on
555        path: String,
556        /// Operation that failed
557        operation: String,
558    },
559
560    /// Failed to load platform configuration for the provider.
561    #[error(
562        code = "client_config_LOAD_FAILED",
563        message = "Failed to load platform configuration for provider '{provider}'",
564        retryable = "false",
565        internal = "false",
566        http_status_code = 400
567    )]
568    ClientConfigLoadFailed {
569        /// Name of the provider (aws, gcp, azure, etc.)
570        provider: String,
571    },
572
573    /// Binding setup failed during initialization.
574    #[error(
575        code = "BINDING_SETUP_FAILED",
576        message = "Binding setup failed for type '{binding_type}': {reason}",
577        retryable = "false",
578        internal = "false",
579        http_status_code = 500
580    )]
581    BindingSetupFailed {
582        /// Type of binding being set up
583        binding_type: String,
584        /// Reason for the setup failure
585        reason: String,
586    },
587
588    /// KV operation failed.
589    #[error(
590        code = "KV_OPERATION_FAILED",
591        message = "KV operation '{operation}' failed for key '{key}': {reason}",
592        retryable = "true",
593        internal = "false",
594        http_status_code = 502
595    )]
596    KvOperationFailed {
597        /// The KV operation that failed
598        operation: String,
599        /// The key involved in the operation
600        key: String,
601        /// Reason for the operation failure
602        reason: String,
603    },
604
605    /// Queue operation failed.
606    #[error(
607        code = "QUEUE_OPERATION_FAILED",
608        message = "Queue operation '{operation}' failed: {reason}",
609        retryable = "true",
610        internal = "false",
611        http_status_code = 502
612    )]
613    QueueOperationFailed {
614        /// The queue operation that failed
615        operation: String,
616        /// Reason for the operation failure
617        reason: String,
618    },
619
620    /// Remote access to deployment resources failed.
621    #[error(
622        code = "REMOTE_ACCESS_FAILED",
623        message = "Remote access failed during operation: {operation}",
624        retryable = "true",
625        internal = "false",
626        http_status_code = 502
627    )]
628    RemoteAccessFailed {
629        /// Description of the operation that failed
630        operation: String,
631    },
632
633    /// Client configuration is invalid or missing for the platform.
634    #[error(
635        code = "CLIENT_CONFIG_INVALID",
636        message = "Client configuration invalid for platform '{platform}': {message}",
637        retryable = "false",
638        internal = "false",
639        http_status_code = 400
640    )]
641    ClientConfigInvalid {
642        /// The platform that was expected
643        platform: alien_core::Platform,
644        /// Description of the configuration issue
645        message: String,
646    },
647}
648
649impl ErrorData {
650    /// Construct a [`ErrorData::BindingConfigInvalid`] for `binding_name`,
651    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally so call
652    /// sites cannot forget it (the omission that repeatedly broke bindings).
653    pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
654        ErrorData::BindingConfigInvalid {
655            env_var: binding_env_var(binding_name),
656            binding_name: binding_name.to_string(),
657            reason: reason.into(),
658        }
659    }
660
661    /// Construct a [`ErrorData::BindingNotConfigured`] for `binding_name`,
662    /// deriving the exact `ALIEN_<NAME>_BINDING` env var name internally.
663    pub fn not_configured(binding_name: &str) -> Self {
664        ErrorData::BindingNotConfigured {
665            binding_name: binding_name.to_string(),
666            env_var: binding_env_var(binding_name),
667        }
668    }
669}
670
671/// Convenient alias with default error type `ErrorData`.
672pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
673
674/// Convenience alias representing a constructed AlienError with our `ErrorData` payload.
675pub type Error = AlienError<ErrorData>;
676
677/// Maps an `alien_client_core::Error` to an appropriate `alien_bindings::Error`.
678///
679/// Important error types (like resource not found, access denied, etc.) are mapped
680/// to their corresponding variants in alien-bindings while preserving the operation context.
681/// Less important errors are wrapped in `CloudPlatformError`.
682///
683/// # Arguments
684/// * `cloud_error` - The error from cloud client crates
685/// * `operation_context` - Description of the operation that failed (e.g., "Failed to get ECR repository details")
686/// * `resource_id` - Optional resource ID for fallback error context
687///
688/// # Example
689/// ```rust
690/// use alien_bindings::error::map_cloud_client_error;
691///
692/// async fn example() {
693///     // This would be an actual cloud client operation
694///     let result = some_cloud_operation().await
695///         .map_err(|e| map_cloud_client_error(e, "Failed to get ECR repository details".to_string(), Some("my-repo".to_string())));
696/// }
697///
698/// async fn some_cloud_operation() -> Result<(), alien_client_core::Error> {
699///     // Mock implementation
700///     Ok(())
701/// }
702/// ```
703pub fn map_cloud_client_error(
704    cloud_error: alien_client_core::Error,
705    operation_context: String,
706    resource_id: Option<String>,
707) -> Error {
708    use alien_client_core::ErrorData as CloudErrorData;
709
710    // Check the error type first to determine the right context to add
711    let error_data = match cloud_error.error.as_ref() {
712        Some(CloudErrorData::RemoteResourceNotFound {
713            resource_type,
714            resource_name,
715        }) => ErrorData::RemoteResourceNotFound {
716            operation_context,
717            resource_type: resource_type.clone(),
718            resource_name: resource_name.clone(),
719        },
720        Some(CloudErrorData::RemoteResourceConflict {
721            resource_type,
722            resource_name,
723            message,
724        }) => ErrorData::RemoteResourceConflict {
725            operation_context,
726            resource_type: resource_type.clone(),
727            resource_name: resource_name.clone(),
728            conflict_reason: message.clone(),
729        },
730        Some(CloudErrorData::RemoteAccessDenied {
731            resource_type,
732            resource_name,
733        }) => ErrorData::RemoteAccessDenied {
734            operation_context,
735            resource_type: resource_type.clone(),
736            resource_name: resource_name.clone(),
737        },
738        Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
739            operation_context,
740            details: message.clone(),
741        },
742        Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
743            operation_context,
744            details: message.clone(),
745        },
746        Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
747            ErrorData::RemoteServiceUnavailable {
748                operation_context,
749                details: message.clone(),
750            }
751        }
752        Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
753            operation_context,
754            details: message.clone(),
755        },
756        Some(CloudErrorData::InvalidInput {
757            message,
758            field_name,
759        }) => ErrorData::InvalidInput {
760            operation_context,
761            details: message.clone(),
762            field_name: field_name.clone(),
763        },
764        Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
765            operation_context,
766            details: message.clone(),
767        },
768        // For other error types or None, wrap in CloudPlatformError
769        _ => ErrorData::CloudPlatformError {
770            message: operation_context,
771            resource_id,
772        },
773    };
774
775    // Now add the context to the cloud error
776    cloud_error.context(error_data)
777}