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 = "STORAGE_OBJECT_ALREADY_EXISTS",
135 message = "Storage object already exists for binding '{binding_name}' while attempting to {operation}",
136 retryable = "false",
137 internal = "false",
138 http_status_code = 409
139 )]
140 StorageObjectAlreadyExists {
141 binding_name: String,
143 operation: String,
145 },
146
147 #[error(
149 code = "BUILD_OPERATION_FAILED",
150 message = "Build operation failed for binding '{binding_name}': {operation}",
151 retryable = "true",
152 internal = "false",
153 http_status_code = 502
154 )]
155 BuildOperationFailed {
156 binding_name: String,
158 operation: String,
160 },
161
162 #[error(
164 code = "ENVIRONMENT_VARIABLE_MISSING",
165 message = "Required environment variable '{variable_name}' is missing",
166 retryable = "false",
167 internal = "false",
168 http_status_code = 500
169 )]
170 EnvironmentVariableMissing {
171 variable_name: String,
173 },
174
175 #[error(
177 code = "INVALID_ENVIRONMENT_VARIABLE",
178 message = "Environment variable '{variable_name}' has invalid value '{value}': {reason}",
179 retryable = "false",
180 internal = "false",
181 http_status_code = 500
182 )]
183 InvalidEnvironmentVariable {
184 variable_name: String,
186 value: String,
188 reason: String,
190 },
191
192 #[error(
194 code = "INVALID_CONFIGURATION_URL",
195 message = "Invalid configuration URL '{url}': {reason}",
196 retryable = "false",
197 internal = "false",
198 http_status_code = 400
199 )]
200 InvalidConfigurationUrl {
201 url: String,
203 reason: String,
205 },
206
207 #[error(
209 code = "EVENT_PROCESSING_FAILED",
210 message = "Event processing failed for type '{event_type}': {reason}",
211 retryable = "true",
212 internal = "false",
213 http_status_code = 400
214 )]
215 EventProcessingFailed {
216 event_type: String,
218 reason: String,
220 },
221
222 #[error(
224 code = "GRPC_CONNECTION_FAILED",
225 message = "gRPC connection failed to endpoint '{endpoint}': {reason}",
226 retryable = "true",
227 internal = "false",
228 http_status_code = 502
229 )]
230 GrpcConnectionFailed {
231 endpoint: String,
233 reason: String,
235 },
236
237 #[error(
239 code = "GRPC_SERVICE_UNAVAILABLE",
240 message = "gRPC service '{service}' unavailable at endpoint '{endpoint}': {reason}",
241 retryable = "true",
242 internal = "false",
243 http_status_code = 503
244 )]
245 GrpcServiceUnavailable {
246 service: String,
248 endpoint: String,
250 reason: String,
252 },
253
254 #[error(
256 code = "GRPC_REQUEST_FAILED",
257 message = "gRPC request to service '{service}' method '{method}' failed: {details}",
258 retryable = "true",
259 internal = "false",
260 http_status_code = 502
261 )]
262 GrpcRequestFailed {
263 service: String,
265 method: String,
267 details: String,
269 },
270
271 #[error(
273 code = "SERVER_BIND_FAILED",
274 message = "Failed to bind server to address '{address}': {reason}",
275 retryable = "true",
276 internal = "true",
277 http_status_code = 500
278 )]
279 ServerBindFailed {
280 address: String,
282 reason: String,
284 },
285
286 #[error(
288 code = "AUTHENTICATION_FAILED",
289 message = "Authentication failed for provider '{provider}' and binding '{binding_name}': {reason}",
290 retryable = "true",
291 internal = "false",
292 http_status_code = 401
293 )]
294 AuthenticationFailed {
295 provider: String,
297 binding_name: String,
299 reason: String,
301 },
302
303 #[error(
305 code = "OPERATION_NOT_SUPPORTED",
306 message = "Operation '{operation}' not supported: {reason}",
307 retryable = "false",
308 internal = "false",
309 http_status_code = 501
310 )]
311 OperationNotSupported {
312 operation: String,
314 reason: String,
316 },
317
318 #[error(
324 code = "SANDBOX_COMMAND_FAILED",
325 message = "Sandbox command failed ({failure}): {reason}",
326 retryable = "false",
327 internal = "inherit",
328 http_status_code = 400
329 )]
330 SandboxCommandFailed {
331 failure: String,
334 reason: String,
336 },
337
338 #[error(
347 code = "SANDBOX_OUTCOME_UNKNOWN",
348 message = "Sandbox operation '{operation}' did not report its outcome and may have taken effect: {reason}",
349 retryable = "false",
350 internal = "inherit",
351 http_status_code = 502
352 )]
353 SandboxOutcomeUnknown {
354 operation: String,
356 reason: String,
358 },
359
360 #[error(
367 code = "SANDBOX_NOT_AS_DECLARED",
368 message = "Sandbox '{sandbox_id}' does not carry its declared {restriction}, so it cannot be used; create a new one. {reason}",
369 retryable = "false",
370 internal = "false",
371 http_status_code = 502
372 )]
373 SandboxNotAsDeclared {
374 sandbox_id: String,
376 restriction: String,
378 reason: String,
380 },
381
382 #[error(
389 code = "SANDBOX_UNREACHABLE",
390 message = "Sandbox operation '{operation}' could not reach the agent: {reason}",
391 retryable = "true",
392 internal = "inherit",
393 http_status_code = 503
394 )]
395 SandboxUnreachable {
396 operation: String,
398 reason: String,
400 },
401
402 #[error(
404 code = "FEATURE_NOT_ENABLED",
405 message = "Feature '{feature}' is not enabled in this build",
406 retryable = "false",
407 internal = "false",
408 http_status_code = 501
409 )]
410 FeatureNotEnabled {
411 feature: String,
413 },
414
415 #[error(
417 code = "GRPC_CALL_FAILED",
418 message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
419 retryable = "true",
420 internal = "false",
421 http_status_code = 502
422 )]
423 GrpcCallFailed {
424 service: String,
426 method: String,
428 reason: String,
430 },
431
432 #[error(
434 code = "DESERIALIZATION_FAILED",
435 message = "Failed to deserialize {type_name}: {message}",
436 retryable = "false",
437 internal = "false",
438 http_status_code = 400
439 )]
440 DeserializationFailed {
441 message: String,
443 type_name: String,
445 },
446
447 #[error(
449 code = "SERIALIZATION_FAILED",
450 message = "Failed to serialize data: {message}",
451 retryable = "false",
452 internal = "false",
453 http_status_code = 500
454 )]
455 SerializationFailed {
456 message: String,
458 },
459
460 #[error(
462 code = "UNEXPECTED_RESPONSE_FORMAT",
463 message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
464 retryable = "false",
465 internal = "false",
466 http_status_code = 502
467 )]
468 UnexpectedResponseFormat {
469 provider: String,
471 binding_name: String,
473 field: String,
475 response_json: String,
477 },
478
479 #[error(
481 code = "CLOUD_PLATFORM_ERROR",
482 message = "Cloud platform error: {message}",
483 retryable = "true",
484 internal = "false",
485 http_status_code = 502
486 )]
487 CloudPlatformError {
488 message: String,
490 resource_id: Option<String>,
492 },
493
494 #[error(
502 code = "POSTGRES_SECRET_RESOLUTION_FAILED",
503 message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
504 retryable = "inherit",
505 internal = "inherit",
506 http_status_code = 502
507 )]
508 PostgresSecretResolutionFailed {
509 binding_name: String,
511 secret: String,
513 reason: String,
515 },
516
517 #[error(
522 code = "POSTGRES_SECRET_VALUE_INVALID",
523 message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
524 retryable = "false",
525 internal = "false",
526 http_status_code = 502
527 )]
528 PostgresSecretValueInvalid {
529 binding_name: String,
531 secret: String,
533 reason: String,
535 },
536
537 #[error(
539 code = "RESOURCE_NOT_FOUND",
540 message = "Resource '{resource_id}' not found",
541 retryable = "false",
542 internal = "false",
543 http_status_code = 404
544 )]
545 ResourceNotFound {
546 resource_id: String,
548 },
549
550 #[error(
552 code = "REMOTE_RESOURCE_NOT_FOUND",
553 message = "{operation_context}: {resource_type} '{resource_name}' not found",
554 retryable = "false",
555 internal = "false",
556 http_status_code = 404
557 )]
558 RemoteResourceNotFound {
559 operation_context: String,
561 resource_type: String,
563 resource_name: String,
565 },
566
567 #[error(
569 code = "REMOTE_RESOURCE_CONFLICT",
570 message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
571 retryable = "true",
572 internal = "false",
573 http_status_code = 409
574 )]
575 RemoteResourceConflict {
576 operation_context: String,
578 resource_type: String,
580 resource_name: String,
582 conflict_reason: String,
584 },
585
586 #[error(
588 code = "REMOTE_ACCESS_DENIED",
589 message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
590 retryable = "true",
591 internal = "false",
592 http_status_code = 403
593 )]
594 RemoteAccessDenied {
595 operation_context: String,
597 resource_type: String,
599 resource_name: String,
601 },
602
603 #[error(
605 code = "RATE_LIMIT_EXCEEDED",
606 message = "{operation_context}: Rate limit exceeded - {details}",
607 retryable = "true",
608 internal = "false",
609 http_status_code = 429
610 )]
611 RateLimitExceeded {
612 operation_context: String,
614 details: String,
616 },
617
618 #[error(
620 code = "TIMEOUT",
621 message = "{operation_context}: Operation timed out - {details}",
622 retryable = "true",
623 internal = "false",
624 http_status_code = 408
625 )]
626 Timeout {
627 operation_context: String,
629 details: String,
631 },
632
633 #[error(
635 code = "REMOTE_SERVICE_UNAVAILABLE",
636 message = "{operation_context}: Service unavailable - {details}",
637 retryable = "true",
638 internal = "false",
639 http_status_code = 503
640 )]
641 RemoteServiceUnavailable {
642 operation_context: String,
644 details: String,
646 },
647
648 #[error(
650 code = "QUOTA_EXCEEDED",
651 message = "{operation_context}: Quota exceeded - {details}",
652 retryable = "true",
653 internal = "false",
654 http_status_code = 429
655 )]
656 QuotaExceeded {
657 operation_context: String,
659 details: String,
661 },
662
663 #[error(
665 code = "INVALID_INPUT",
666 message = "{operation_context}: Invalid input - {details}",
667 retryable = "false",
668 internal = "false",
669 http_status_code = 400
670 )]
671 InvalidInput {
672 operation_context: String,
674 details: String,
676 field_name: Option<String>,
678 },
679
680 #[error(
682 code = "AUTHENTICATION_ERROR",
683 message = "{operation_context}: Authentication failed - {details}",
684 retryable = "true",
685 internal = "false",
686 http_status_code = 401
687 )]
688 AuthenticationError {
689 operation_context: String,
691 details: String,
693 },
694
695 #[error(
697 code = "BINDINGS_ERROR",
698 message = "Bindings error: {message}",
699 retryable = "true",
700 internal = "true",
701 http_status_code = 500
702 )]
703 Other {
704 message: String,
706 },
707
708 #[error(
710 code = "PRESIGNED_REQUEST_EXPIRED",
711 message = "Presigned request for path '{path}' expired at {expired_at}",
712 retryable = "false",
713 internal = "false",
714 http_status_code = 403
715 )]
716 PresignedRequestExpired {
717 path: String,
719 expired_at: chrono::DateTime<chrono::Utc>,
721 },
722
723 #[error(
725 code = "HTTP_REQUEST_FAILED",
726 message = "HTTP {method} request to '{url}' failed",
727 retryable = "true",
728 internal = "false",
729 http_status_code = 502
730 )]
731 HttpRequestFailed {
732 url: String,
734 method: String,
736 },
737
738 #[error(
740 code = "LOCAL_FILESYSTEM_ERROR",
741 message = "Local filesystem operation '{operation}' failed for path '{path}'",
742 retryable = "true",
743 internal = "false",
744 http_status_code = 500
745 )]
746 LocalFilesystemError {
747 path: String,
749 operation: String,
751 },
752
753 #[error(
755 code = "client_config_LOAD_FAILED",
756 message = "Failed to load platform configuration for provider '{provider}'",
757 retryable = "false",
758 internal = "false",
759 http_status_code = 400
760 )]
761 ClientConfigLoadFailed {
762 provider: String,
764 },
765
766 #[error(
768 code = "BINDING_SETUP_FAILED",
769 message = "Binding setup failed for type '{binding_type}': {reason}",
770 retryable = "false",
771 internal = "false",
772 http_status_code = 500
773 )]
774 BindingSetupFailed {
775 binding_type: String,
777 reason: String,
779 },
780
781 #[error(
783 code = "KV_OPERATION_FAILED",
784 message = "KV operation '{operation}' failed for key '{key}': {reason}",
785 retryable = "true",
786 internal = "false",
787 http_status_code = 502
788 )]
789 KvOperationFailed {
790 operation: String,
792 key: String,
794 reason: String,
796 },
797
798 #[error(
800 code = "QUEUE_OPERATION_FAILED",
801 message = "Queue operation '{operation}' failed: {reason}",
802 retryable = "true",
803 internal = "false",
804 http_status_code = 502
805 )]
806 QueueOperationFailed {
807 operation: String,
809 reason: String,
811 },
812
813 #[error(
815 code = "QUEUE_PROVIDER_RESPONSE_INVALID",
816 message = "Queue provider response is invalid: {reason}",
817 retryable = "false",
818 internal = "false",
819 http_status_code = 502
820 )]
821 QueueProviderResponseInvalid {
822 reason: String,
824 },
825
826 #[error(
828 code = "REMOTE_ACCESS_FAILED",
829 message = "Remote access failed during operation: {operation}",
830 retryable = "true",
831 internal = "false",
832 http_status_code = 502
833 )]
834 RemoteAccessFailed {
835 operation: String,
837 },
838
839 #[error(
841 code = "CLIENT_CONFIG_INVALID",
842 message = "Client configuration invalid for platform '{platform}': {message}",
843 retryable = "false",
844 internal = "false",
845 http_status_code = 400
846 )]
847 ClientConfigInvalid {
848 platform: alien_core::Platform,
850 message: String,
852 },
853}
854
855impl ErrorData {
856 pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
860 ErrorData::BindingConfigInvalid {
861 env_var: binding_env_var(binding_name),
862 binding_name: binding_name.to_string(),
863 reason: reason.into(),
864 }
865 }
866
867 pub fn not_configured(binding_name: &str) -> Self {
870 ErrorData::BindingNotConfigured {
871 binding_name: binding_name.to_string(),
872 env_var: binding_env_var(binding_name),
873 }
874 }
875}
876
877pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
879
880pub type Error = AlienError<ErrorData>;
882
883pub fn map_cloud_client_error(
910 cloud_error: alien_client_core::Error,
911 operation_context: String,
912 resource_id: Option<String>,
913) -> Error {
914 use alien_client_core::ErrorData as CloudErrorData;
915
916 let error_data = match cloud_error.error.as_ref() {
918 Some(CloudErrorData::RemoteResourceNotFound {
919 resource_type,
920 resource_name,
921 }) => ErrorData::RemoteResourceNotFound {
922 operation_context,
923 resource_type: resource_type.clone(),
924 resource_name: resource_name.clone(),
925 },
926 Some(CloudErrorData::RemoteResourceConflict {
927 resource_type,
928 resource_name,
929 message,
930 }) => ErrorData::RemoteResourceConflict {
931 operation_context,
932 resource_type: resource_type.clone(),
933 resource_name: resource_name.clone(),
934 conflict_reason: message.clone(),
935 },
936 Some(CloudErrorData::RemoteAccessDenied {
937 resource_type,
938 resource_name,
939 }) => ErrorData::RemoteAccessDenied {
940 operation_context,
941 resource_type: resource_type.clone(),
942 resource_name: resource_name.clone(),
943 },
944 Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
945 operation_context,
946 details: message.clone(),
947 },
948 Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
949 operation_context,
950 details: message.clone(),
951 },
952 Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
953 ErrorData::RemoteServiceUnavailable {
954 operation_context,
955 details: message.clone(),
956 }
957 }
958 Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
959 operation_context,
960 details: message.clone(),
961 },
962 Some(CloudErrorData::InvalidInput {
963 message,
964 field_name,
965 }) => ErrorData::InvalidInput {
966 operation_context,
967 details: message.clone(),
968 field_name: field_name.clone(),
969 },
970 Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
971 operation_context,
972 details: message.clone(),
973 },
974 _ => ErrorData::CloudPlatformError {
976 message: operation_context,
977 resource_id,
978 },
979 };
980
981 cloud_error.context(error_data)
983}