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 = "FEATURE_NOT_ENABLED",
306 message = "Feature '{feature}' is not enabled in this build",
307 retryable = "false",
308 internal = "false",
309 http_status_code = 501
310 )]
311 FeatureNotEnabled {
312 feature: String,
314 },
315
316 #[error(
318 code = "GRPC_CALL_FAILED",
319 message = "gRPC call to service '{service}' method '{method}' failed: {reason}",
320 retryable = "true",
321 internal = "false",
322 http_status_code = 502
323 )]
324 GrpcCallFailed {
325 service: String,
327 method: String,
329 reason: String,
331 },
332
333 #[error(
335 code = "DESERIALIZATION_FAILED",
336 message = "Failed to deserialize {type_name}: {message}",
337 retryable = "false",
338 internal = "false",
339 http_status_code = 400
340 )]
341 DeserializationFailed {
342 message: String,
344 type_name: String,
346 },
347
348 #[error(
350 code = "SERIALIZATION_FAILED",
351 message = "Failed to serialize data: {message}",
352 retryable = "false",
353 internal = "false",
354 http_status_code = 500
355 )]
356 SerializationFailed {
357 message: String,
359 },
360
361 #[error(
363 code = "UNEXPECTED_RESPONSE_FORMAT",
364 message = "Unexpected response format from '{provider}' for binding '{binding_name}': missing field '{field}'. Response: {response_json}",
365 retryable = "false",
366 internal = "false",
367 http_status_code = 502
368 )]
369 UnexpectedResponseFormat {
370 provider: String,
372 binding_name: String,
374 field: String,
376 response_json: String,
378 },
379
380 #[error(
382 code = "CLOUD_PLATFORM_ERROR",
383 message = "Cloud platform error: {message}",
384 retryable = "true",
385 internal = "false",
386 http_status_code = 502
387 )]
388 CloudPlatformError {
389 message: String,
391 resource_id: Option<String>,
393 },
394
395 #[error(
403 code = "POSTGRES_SECRET_RESOLUTION_FAILED",
404 message = "Failed to resolve the password for Postgres binding '{binding_name}' from secret '{secret}': {reason}",
405 retryable = "inherit",
406 internal = "inherit",
407 http_status_code = 502
408 )]
409 PostgresSecretResolutionFailed {
410 binding_name: String,
412 secret: String,
414 reason: String,
416 },
417
418 #[error(
423 code = "POSTGRES_SECRET_VALUE_INVALID",
424 message = "Secret '{secret}' for Postgres binding '{binding_name}' does not contain a valid password: {reason}",
425 retryable = "false",
426 internal = "false",
427 http_status_code = 502
428 )]
429 PostgresSecretValueInvalid {
430 binding_name: String,
432 secret: String,
434 reason: String,
436 },
437
438 #[error(
440 code = "RESOURCE_NOT_FOUND",
441 message = "Resource '{resource_id}' not found",
442 retryable = "false",
443 internal = "false",
444 http_status_code = 404
445 )]
446 ResourceNotFound {
447 resource_id: String,
449 },
450
451 #[error(
453 code = "REMOTE_RESOURCE_NOT_FOUND",
454 message = "{operation_context}: {resource_type} '{resource_name}' not found",
455 retryable = "false",
456 internal = "false",
457 http_status_code = 404
458 )]
459 RemoteResourceNotFound {
460 operation_context: String,
462 resource_type: String,
464 resource_name: String,
466 },
467
468 #[error(
470 code = "REMOTE_RESOURCE_CONFLICT",
471 message = "{operation_context}: Conflict with {resource_type} '{resource_name}' - {conflict_reason}",
472 retryable = "true",
473 internal = "false",
474 http_status_code = 409
475 )]
476 RemoteResourceConflict {
477 operation_context: String,
479 resource_type: String,
481 resource_name: String,
483 conflict_reason: String,
485 },
486
487 #[error(
489 code = "REMOTE_ACCESS_DENIED",
490 message = "{operation_context}: Access denied to {resource_type} '{resource_name}'",
491 retryable = "true",
492 internal = "false",
493 http_status_code = 403
494 )]
495 RemoteAccessDenied {
496 operation_context: String,
498 resource_type: String,
500 resource_name: String,
502 },
503
504 #[error(
506 code = "RATE_LIMIT_EXCEEDED",
507 message = "{operation_context}: Rate limit exceeded - {details}",
508 retryable = "true",
509 internal = "false",
510 http_status_code = 429
511 )]
512 RateLimitExceeded {
513 operation_context: String,
515 details: String,
517 },
518
519 #[error(
521 code = "TIMEOUT",
522 message = "{operation_context}: Operation timed out - {details}",
523 retryable = "true",
524 internal = "false",
525 http_status_code = 408
526 )]
527 Timeout {
528 operation_context: String,
530 details: String,
532 },
533
534 #[error(
536 code = "REMOTE_SERVICE_UNAVAILABLE",
537 message = "{operation_context}: Service unavailable - {details}",
538 retryable = "true",
539 internal = "false",
540 http_status_code = 503
541 )]
542 RemoteServiceUnavailable {
543 operation_context: String,
545 details: String,
547 },
548
549 #[error(
551 code = "QUOTA_EXCEEDED",
552 message = "{operation_context}: Quota exceeded - {details}",
553 retryable = "true",
554 internal = "false",
555 http_status_code = 429
556 )]
557 QuotaExceeded {
558 operation_context: String,
560 details: String,
562 },
563
564 #[error(
566 code = "INVALID_INPUT",
567 message = "{operation_context}: Invalid input - {details}",
568 retryable = "false",
569 internal = "false",
570 http_status_code = 400
571 )]
572 InvalidInput {
573 operation_context: String,
575 details: String,
577 field_name: Option<String>,
579 },
580
581 #[error(
583 code = "AUTHENTICATION_ERROR",
584 message = "{operation_context}: Authentication failed - {details}",
585 retryable = "true",
586 internal = "false",
587 http_status_code = 401
588 )]
589 AuthenticationError {
590 operation_context: String,
592 details: String,
594 },
595
596 #[error(
598 code = "BINDINGS_ERROR",
599 message = "Bindings error: {message}",
600 retryable = "true",
601 internal = "true",
602 http_status_code = 500
603 )]
604 Other {
605 message: String,
607 },
608
609 #[error(
611 code = "PRESIGNED_REQUEST_EXPIRED",
612 message = "Presigned request for path '{path}' expired at {expired_at}",
613 retryable = "false",
614 internal = "false",
615 http_status_code = 403
616 )]
617 PresignedRequestExpired {
618 path: String,
620 expired_at: chrono::DateTime<chrono::Utc>,
622 },
623
624 #[error(
626 code = "HTTP_REQUEST_FAILED",
627 message = "HTTP {method} request to '{url}' failed",
628 retryable = "true",
629 internal = "false",
630 http_status_code = 502
631 )]
632 HttpRequestFailed {
633 url: String,
635 method: String,
637 },
638
639 #[error(
641 code = "LOCAL_FILESYSTEM_ERROR",
642 message = "Local filesystem operation '{operation}' failed for path '{path}'",
643 retryable = "true",
644 internal = "false",
645 http_status_code = 500
646 )]
647 LocalFilesystemError {
648 path: String,
650 operation: String,
652 },
653
654 #[error(
656 code = "client_config_LOAD_FAILED",
657 message = "Failed to load platform configuration for provider '{provider}'",
658 retryable = "false",
659 internal = "false",
660 http_status_code = 400
661 )]
662 ClientConfigLoadFailed {
663 provider: String,
665 },
666
667 #[error(
669 code = "BINDING_SETUP_FAILED",
670 message = "Binding setup failed for type '{binding_type}': {reason}",
671 retryable = "false",
672 internal = "false",
673 http_status_code = 500
674 )]
675 BindingSetupFailed {
676 binding_type: String,
678 reason: String,
680 },
681
682 #[error(
684 code = "KV_OPERATION_FAILED",
685 message = "KV operation '{operation}' failed for key '{key}': {reason}",
686 retryable = "true",
687 internal = "false",
688 http_status_code = 502
689 )]
690 KvOperationFailed {
691 operation: String,
693 key: String,
695 reason: String,
697 },
698
699 #[error(
701 code = "QUEUE_OPERATION_FAILED",
702 message = "Queue operation '{operation}' failed: {reason}",
703 retryable = "true",
704 internal = "false",
705 http_status_code = 502
706 )]
707 QueueOperationFailed {
708 operation: String,
710 reason: String,
712 },
713
714 #[error(
716 code = "REMOTE_ACCESS_FAILED",
717 message = "Remote access failed during operation: {operation}",
718 retryable = "true",
719 internal = "false",
720 http_status_code = 502
721 )]
722 RemoteAccessFailed {
723 operation: String,
725 },
726
727 #[error(
729 code = "CLIENT_CONFIG_INVALID",
730 message = "Client configuration invalid for platform '{platform}': {message}",
731 retryable = "false",
732 internal = "false",
733 http_status_code = 400
734 )]
735 ClientConfigInvalid {
736 platform: alien_core::Platform,
738 message: String,
740 },
741}
742
743impl ErrorData {
744 pub fn config_invalid(binding_name: &str, reason: impl Into<String>) -> Self {
748 ErrorData::BindingConfigInvalid {
749 env_var: binding_env_var(binding_name),
750 binding_name: binding_name.to_string(),
751 reason: reason.into(),
752 }
753 }
754
755 pub fn not_configured(binding_name: &str) -> Self {
758 ErrorData::BindingNotConfigured {
759 binding_name: binding_name.to_string(),
760 env_var: binding_env_var(binding_name),
761 }
762 }
763}
764
765pub type Result<T, E = ErrorData> = alien_error::Result<T, E>;
767
768pub type Error = AlienError<ErrorData>;
770
771pub fn map_cloud_client_error(
798 cloud_error: alien_client_core::Error,
799 operation_context: String,
800 resource_id: Option<String>,
801) -> Error {
802 use alien_client_core::ErrorData as CloudErrorData;
803
804 let error_data = match cloud_error.error.as_ref() {
806 Some(CloudErrorData::RemoteResourceNotFound {
807 resource_type,
808 resource_name,
809 }) => ErrorData::RemoteResourceNotFound {
810 operation_context,
811 resource_type: resource_type.clone(),
812 resource_name: resource_name.clone(),
813 },
814 Some(CloudErrorData::RemoteResourceConflict {
815 resource_type,
816 resource_name,
817 message,
818 }) => ErrorData::RemoteResourceConflict {
819 operation_context,
820 resource_type: resource_type.clone(),
821 resource_name: resource_name.clone(),
822 conflict_reason: message.clone(),
823 },
824 Some(CloudErrorData::RemoteAccessDenied {
825 resource_type,
826 resource_name,
827 }) => ErrorData::RemoteAccessDenied {
828 operation_context,
829 resource_type: resource_type.clone(),
830 resource_name: resource_name.clone(),
831 },
832 Some(CloudErrorData::RateLimitExceeded { message }) => ErrorData::RateLimitExceeded {
833 operation_context,
834 details: message.clone(),
835 },
836 Some(CloudErrorData::Timeout { message }) => ErrorData::Timeout {
837 operation_context,
838 details: message.clone(),
839 },
840 Some(CloudErrorData::RemoteServiceUnavailable { message }) => {
841 ErrorData::RemoteServiceUnavailable {
842 operation_context,
843 details: message.clone(),
844 }
845 }
846 Some(CloudErrorData::QuotaExceeded { message }) => ErrorData::QuotaExceeded {
847 operation_context,
848 details: message.clone(),
849 },
850 Some(CloudErrorData::InvalidInput {
851 message,
852 field_name,
853 }) => ErrorData::InvalidInput {
854 operation_context,
855 details: message.clone(),
856 field_name: field_name.clone(),
857 },
858 Some(CloudErrorData::AuthenticationError { message }) => ErrorData::AuthenticationError {
859 operation_context,
860 details: message.clone(),
861 },
862 _ => ErrorData::CloudPlatformError {
864 message: operation_context,
865 resource_id,
866 },
867 };
868
869 cloud_error.context(error_data)
871}