1use alien_error::{AlienError, AlienErrorData, ContextError};
2use serde::{Deserialize, Serialize};
3
4pub fn binding_env_var(binding_name: &str) -> String {
9 alien_core::bindings::binding_env_var_name(binding_name)
10}
11
12#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
14#[serde(rename_all = "camelCase")]
15pub enum ErrorData {
16 #[error(
18 code = "KEY_INPUT_INVALID",
19 message = "Key input is invalid: {reason}",
20 retryable = "false",
21 internal = "false",
22 http_status_code = 400
23 )]
24 KeyInputInvalid { reason: String },
25
26 #[error(
28 code = "KEY_CIPHERTEXT_INVALID",
29 message = "Key ciphertext is invalid: {reason}",
30 retryable = "false",
31 internal = "false",
32 http_status_code = 400
33 )]
34 KeyCiphertextInvalid { reason: String },
35
36 #[error(
39 code = "BINDING_NOT_CONFIGURED",
40 message = "No binding configured for '{binding_name}': environment variable '{env_var}' is not set",
41 retryable = "false",
42 internal = "false",
43 http_status_code = 400
44 )]
45 BindingNotConfigured {
46 binding_name: String,
48 env_var: String,
50 },
51
52 #[error(
54 code = "BINDING_CONFIG_INVALID",
55 message = "Binding configuration invalid for binding '{binding_name}' (env var '{env_var}'): {reason}",
56 retryable = "false",
57 internal = "false",
58 http_status_code = 400
59 )]
60 BindingConfigInvalid {
61 binding_name: String,
63 env_var: String,
65 reason: String,
67 },
68
69 #[error(
71 code = "UNSUPPORTED_BINDING_PROVIDER",
72 message = "Binding '{binding_name}' (env var '{env_var}') uses unsupported provider '{provider}'",
73 retryable = "false",
74 internal = "false",
75 http_status_code = 501
76 )]
77 UnsupportedBindingProvider {
78 binding_name: String,
80 env_var: String,
82 provider: String,
84 },
85
86 #[error(
88 code = "STORAGE_OPERATION_FAILED",
89 message = "Storage operation failed for binding '{binding_name}': {operation}",
90 retryable = "true",
91 internal = "false",
92 http_status_code = 502
93 )]
94 StorageOperationFailed {
95 binding_name: String,
97 operation: String,
99 },
100
101 #[error(
103 code = "STORAGE_ACCESS_DENIED",
104 message = "Storage access denied for binding '{binding_name}' while attempting to {operation}",
105 retryable = "false",
106 internal = "false",
107 http_status_code = 403,
108 hint = "Check that the customer's Storage connection is active and its Access identity still has the required permissions."
109 )]
110 StorageAccessDenied {
111 binding_name: String,
113 operation: String,
115 },
116
117 #[error(
119 code = "STORAGE_OBJECT_NOT_FOUND",
120 message = "Storage object not found for binding '{binding_name}' while attempting to {operation}",
121 retryable = "false",
122 internal = "false",
123 http_status_code = 404
124 )]
125 StorageObjectNotFound {
126 binding_name: String,
128 operation: String,
130 },
131
132 #[error(
134 code = "BUILD_OPERATION_FAILED",
135 message = "Build operation failed for binding '{binding_name}': {operation}",
136 retryable = "true",
137 internal = "false",
138 http_status_code = 502
139 )]
140 BuildOperationFailed {
141 binding_name: String,
143 operation: String,
145 },
146
147 #[error(
149 code = "ENVIRONMENT_VARIABLE_MISSING",
150 message = "Required environment variable '{variable_name}' is missing",
151 retryable = "false",
152 internal = "false",
153 http_status_code = 500
154 )]
155 EnvironmentVariableMissing {
156 variable_name: String,
158 },
159
160 #[error(
162 code = "INVALID_ENVIRONMENT_VARIABLE",
163 message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
164 retryable = "false",
165 internal = "false",
166 http_status_code = 500
167 )]
168 InvalidEnvironmentVariable {
169 variable_name: String,
171 value: String,
173 reason: String,
175 },
176
177 #[error(
179 code = "INVALID_CONFIGURATION_URL",
180 message = "Invalid configuration URL '{url}': {reason}",
181 retryable = "false",
182 internal = "false",
183 http_status_code = 400
184 )]
185 InvalidConfigurationUrl {
186 url: String,
188 reason: String,
190 },
191
192 #[error(
194 code = "EVENT_PROCESSING_FAILED",
195 message = "Event processing failed for type '{event_type}': {reason}",
196 retryable = "true",
197 internal = "false",
198 http_status_code = 400
199 )]
200 EventProcessingFailed {
201 event_type: String,
203 reason: String,
205 },
206
207 #[error(
209 code = "GRPC_CONNECTION_FAILED",
210 message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
211 retryable = "true",
212 internal = "false",
213 http_status_code = 502
214 )]
215 GrpcConnectionFailed {
216 endpoint: String,
218 reason: String,
220 },
221
222 #[error(
224 code = "GRPC_SERVICE_UNAVAILABLE",
225 message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
226 retryable = "true",
227 internal = "false",
228 http_status_code = 503
229 )]
230 GrpcServiceUnavailable {
231 service: String,
233 endpoint: String,
235 reason: String,
237 },
238
239 #[error(
241 code = "GRPC_REQUEST_FAILED",
242 message = "gRPC request to service '{service}' method '{method}' failed: {details}",
243 retryable = "true",
244 internal = "false",
245 http_status_code = 502
246 )]
247 GrpcRequestFailed {
248 service: String,
250 method: String,
252 details: String,
254 },
255
256 #[error(
258 code = "SERVER_BIND_FAILED",
259 message = "Failed to bind server to address '{address}': {reason}",
260 retryable = "true",
261 internal = "true",
262 http_status_code = 500
263 )]
264 ServerBindFailed {
265 address: String,
267 reason: String,
269 },
270
271 #[error(
273 code = "AUTHENTICATION_FAILED",
274 message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
275 retryable = "true",
276 internal = "false",
277 http_status_code = 401
278 )]
279 AuthenticationFailed {
280 provider: String,
282 binding_name: String,
284 reason: String,
286 },
287
288 #[error(
290 code = "OPERATION_NOT_SUPPORTED",
291 message = "Operation '{operation}' not supported: {reason}",
292 retryable = "false",
293 internal = "false",
294 http_status_code = 501
295 )]
296 OperationNotSupported {
297 operation: String,
299 reason: String,
301 },
302
303 #[error(
305 code = "SANDBOX_COMMAND_FAILED",
306 message = "Sandbox command failed ({failure}): {reason}",
307 retryable = "false",
308 internal = "false",
309 http_status_code = 400
310 )]
311 SandboxCommandFailed {
312 failure: String,
314 reason: String,
316 },
317
318 #[error(
325 code = "SANDBOX_UNREACHABLE",
326 message = "Sandbox operation '{operation}' could not reach the agent: {reason}",
327 retryable = "true",
328 internal = "inherit",
329 http_status_code = 503
330 )]
331 SandboxUnreachable {
332 operation: String,
334 reason: String,
336 },
337
338 #[error(
340 code = "FEATURE_NOT_ENABLED",
341 message = "Feature '{feature}' is not enabled in this build",
342 retryable = "false",
343 internal = "false",
344 http_status_code = 501
345 )]
346 FeatureNotEnabled {
347 feature: String,
349 },
350
351 #[error(
353 code = "GRPC_CALL_FAILED",
354 message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
355 retryable = "true",
356 internal = "false",
357 http_status_code = 502
358 )]
359 GrpcCallFailed {
360 service: String,
362 method: String,
364 reason: String,
366 },
367
368 #[error(
370 code = "DESERIALIZATION_FAILED",
371 message = "Failed to deserialize {type_name}: {message}",
372 retryable = "false",
373 internal = "false",
374 http_status_code = 400
375 )]
376 DeserializationFailed {
377 message: String,
379 type_name: String,
381 },
382
383 #[error(
385 code = "SERIALIZATION_FAILED",
386 message = "Failed to serialize data: {message}",
387 retryable = "false",
388 internal = "false",
389 http_status_code = 500
390 )]
391 SerializationFailed {
392 message: String,
394 },
395
396 #[error(
398 code = "UNEXPECTED_RESPONSE_FORMAT",
399 message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
400 retryable = "false",
401 internal = "false",
402 http_status_code = 502
403 )]
404 UnexpectedResponseFormat {
405 provider: String,
407 binding_name: String,
409 field: String,
411 response_json: String,
413 },
414
415 #[error(
417 code = "CLOUD_PLATFORM_ERROR",
418 message = "Cloud platform error: {message}",
419 retryable = "true",
420 internal = "false",
421 http_status_code = 502
422 )]
423 CloudPlatformError {
424 message: String,
426 resource_id: Option<String>,
428 },
429
430 #[error(
438 code = "POSTGRES_SECRET_RESOLUTION_FAILED",
439 message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
440 retryable = "inherit",
441 internal = "inherit",
442 http_status_code = 502
443 )]
444 PostgresSecretResolutionFailed {
445 binding_name: String,
447 secret: String,
449 reason: String,
451 },
452
453 #[error(
458 code = "POSTGRES_SECRET_VALUE_INVALID",
459 message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
460 retryable = "false",
461 internal = "false",
462 http_status_code = 502
463 )]
464 PostgresSecretValueInvalid {
465 binding_name: String,
467 secret: String,
469 reason: String,
471 },
472
473 #[error(
475 code = "RESOURCE_NOT_FOUND",
476 message = "Resource '{resource_id}' not found",
477 retryable = "false",
478 internal = "false",
479 http_status_code = 404
480 )]
481 ResourceNotFound {
482 resource_id: String,
484 },
485
486 #[error(
488 code = "REMOTE_RESOURCE_NOT_FOUND",
489 message = "{operation_context}: {resource_type} '{resource_name}' not found",
490 retryable = "false",
491 internal = "false",
492 http_status_code = 404
493 )]
494 RemoteResourceNotFound {
495 operation_context: String,
497 resource_type: String,
499 resource_name: String,
501 },
502
503 #[error(
505 code = "REMOTE_RESOURCE_CONFLICT",
506 message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
507 retryable = "true",
508 internal = "false",
509 http_status_code = 409
510 )]
511 RemoteResourceConflict {
512 operation_context: String,
514 resource_type: String,
516 resource_name: String,
518 conflict_reason: String,
520 },
521
522 #[error(
524 code = "REMOTE_ACCESS_DENIED",
525 message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
526 retryable = "true",
527 internal = "false",
528 http_status_code = 403
529 )]
530 RemoteAccessDenied {
531 operation_context: String,
533 resource_type: String,
535 resource_name: String,
537 },
538
539 #[error(
541 code = "RATE_LIMIT_EXCEEDED",
542 message = "{operation_context}: Rate limit exceeded - {details}",
543 retryable = "true",
544 internal = "false",
545 http_status_code = 429
546 )]
547 RateLimitExceeded {
548 operation_context: String,
550 details: String,
552 },
553
554 #[error(
556 code = "TIMEOUT",
557 message = "{operation_context}: Operation timed out - {details}",
558 retryable = "true",
559 internal = "false",
560 http_status_code = 408
561 )]
562 Timeout {
563 operation_context: String,
565 details: String,
567 },
568
569 #[error(
571 code = "REMOTE_SERVICE_UNAVAILABLE",
572 message = "{operation_context}: Service unavailable - {details}",
573 retryable = "true",
574 internal = "false",
575 http_status_code = 503
576 )]
577 RemoteServiceUnavailable {
578 operation_context: String,
580 details: String,
582 },
583
584 #[error(
586 code = "QUOTA_EXCEEDED",
587 message = "{operation_context}: Quota exceeded - {details}",
588 retryable = "true",
589 internal = "false",
590 http_status_code = 429
591 )]
592 QuotaExceeded {
593 operation_context: String,
595 details: String,
597 },
598
599 #[error(
601 code = "INVALID_INPUT",
602 message = "{operation_context}: Invalid input - {details}",
603 retryable = "false",
604 internal = "false",
605 http_status_code = 400
606 )]
607 InvalidInput {
608 operation_context: String,
610 details: String,
612 field_name: Option<String>,
614 },
615
616 #[error(
618 code = "AUTHENTICATION_ERROR",
619 message = "{operation_context}: Authentication failed - {details}",
620 retryable = "true",
621 internal = "false",
622 http_status_code = 401
623 )]
624 AuthenticationError {
625 operation_context: String,
627 details: String,
629 },
630
631 #[error(
633 code = "BINDINGS_ERROR",
634 message = "Bindings error: {message}",
635 retryable = "true",
636 internal = "true",
637 http_status_code = 500
638 )]
639 Other {
640 message: String,
642 },
643
644 #[error(
646 code = "PRESIGNED_REQUEST_EXPIRED",
647 message = "Presigned request for path '{path}' expired at {expired_at}",
648 retryable = "false",
649 internal = "false",
650 http_status_code = 403
651 )]
652 PresignedRequestExpired {
653 path: String,
655 expired_at: chrono::DateTime<chrono::Utc>,
657 },
658
659 #[error(
661 code = "HTTP_REQUEST_FAILED",
662 message = "HTTP {method} request to '{url}' failed",
663 retryable = "true",
664 internal = "false",
665 http_status_code = 502
666 )]
667 HttpRequestFailed {
668 url: String,
670 method: String,
672 },
673
674 #[error(
676 code = "LOCAL_FILESYSTEM_ERROR",
677 message = "Local filesystem operation '{operation}' failed for path '{path}'",
678 retryable = "true",
679 internal = "false",
680 http_status_code = 500
681 )]
682 LocalFilesystemError {
683 path: String,
685 operation: String,
687 },
688
689 #[error(
691 code = "client_config_LOAD_FAILED",
692 message = "Failed to load platform configuration for provider '{provider}'",
693 retryable = "false",
694 internal = "false",
695 http_status_code = 400
696 )]
697 ClientConfigLoadFailed {
698 provider: String,
700 },
701
702 #[error(
704 code = "BINDING_SETUP_FAILED",
705 message = "Binding setup failed for type '{binding_type}': {reason}",
706 retryable = "false",
707 internal = "false",
708 http_status_code = 500
709 )]
710 BindingSetupFailed {
711 binding_type: String,
713 reason: String,
715 },
716
717 #[error(
719 code = "KV_OPERATION_FAILED",
720 message = "KV operation '{operation}' failed for key '{key}': {reason}",
721 retryable = "true",
722 internal = "false",
723 http_status_code = 502
724 )]
725 KvOperationFailed {
726 operation: String,
728 key: String,
730 reason: String,
732 },
733
734 #[error(
736 code = "QUEUE_OPERATION_FAILED",
737 message = "Queue operation '{operation}' failed: {reason}",
738 retryable = "true",
739 internal = "false",
740 http_status_code = 502
741 )]
742 QueueOperationFailed {
743 operation: String,
745 reason: String,
747 },
748
749 #[error(
751 code = "REMOTE_ACCESS_FAILED",
752 message = "Remote access failed during operation: {operation}",
753 retryable = "true",
754 internal = "false",
755 http_status_code = 502
756 )]
757 RemoteAccessFailed {
758 operation: String,
760 },
761
762 #[error(
764 code = "CLIENT_CONFIG_INVALID",
765 message = "Client configuration invalid for platform '{platform}': {message}",
766 retryable = "false",
767 internal = "false",
768 http_status_code = 400
769 )]
770 ClientConfigInvalid {
771 platform: alien_core::Platform,
773 message: String,
775 },
776}
777
778impl ErrorData {
779 pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
783 ErrorData::BindingConfigInvalid {
784 env_var: binding_env_var(binding_name),
785 binding_name: binding_name.to_string(),
786 reason: reason.into(),
787 }
788 }
789
790 pub fn not_configured(binding_name: &str) -> Self {
793 ErrorData::BindingNotConfigured {
794 binding_name: binding_name.to_string(),
795 env_var: binding_env_var(binding_name),
796 }
797 }
798}
799
800pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
802
803pub type Error = AlienError<ErrorData>;
805
806pub fn map_cloud_client_error(
833 cloud_error: alien_client_core::Error,
834 operation_context: String,
835 resource_id: Option<String>,
836) -> Error {
837 use alien_client_core::ErrorData as CloudErrorData;
838
839 let error_data = match cloud_error.error.as_ref() {
841 Some(CloudErrorData::RemoteResourceNotFound {
842 resource_type,
843 resource_name,
844 }) => ErrorData::RemoteResourceNotFound {
845 operation_context,
846 resource_type: resource_type.clone(),
847 resource_name: resource_name.clone(),
848 },
849 Some(CloudErrorData::RemoteResourceConflict {
850 resource_type,
851 resource_name,
852 message,
853 }) => ErrorData::RemoteResourceConflict {
854 operation_context,
855 resource_type: resource_type.clone(),
856 resource_name: resource_name.clone(),
857 conflict_reason: message.clone(),
858 },
859 Some(CloudErrorData::RemoteAccessDenied {
860 resource_type,
861 resource_name,
862 }) => ErrorData::RemoteAccessDenied {
863 operation_context,
864 resource_type: resource_type.clone(),
865 resource_name: resource_name.clone(),
866 },
867 Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
868 operation_context,
869 details: message.clone(),
870 },
871 Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
872 operation_context,
873 details: message.clone(),
874 },
875 Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
876 ErrorData::RemoteServiceUnavailable {
877 operation_context,
878 details: message.clone(),
879 }
880 }
881 Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
882 operation_context,
883 details: message.clone(),
884 },
885 Some(CloudErrorData::InvalidInput {
886 message,
887 field_name,
888 }) => ErrorData::InvalidInput {
889 operation_context,
890 details: message.clone(),
891 field_name: field_name.clone(),
892 },
893 Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
894 operation_context,
895 details: message.clone(),
896 },
897 _ => ErrorData::CloudPlatformError {
899 message: operation_context,
900 resource_id,
901 },
902 };
903
904 cloud_error.context(error_data)
906}