Skip to main content

alien_core/
error.rs

1use alien_error::AlienErrorData;
2use serde::{Deserialize, Serialize};
3
4use crate::{Platform, ResourceType};
5
6/// Core error data exposed by the `alien-core` crate.
7#[derive(Debug, Clone, AlienErrorData, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub enum ErrorData {
10    /// A fallback error when nothing more specific matches.
11    #[error(
12        code = "GENERIC_ERROR",
13        message = "{message}",
14        retryable = "true",
15        internal = "false"
16    )]
17    GenericError {
18        /// Human-readable description of the error.
19        message: String,
20    },
21
22    /// Resource type mismatch found during stack operations.
23    #[error(
24        code = "UNEXPECTED_RESOURCE_TYPE",
25        message = "Unexpected resource type for resource '{resource_id}': expected {expected}, but got {actual}",
26        retryable = "false",
27        internal = "false"
28    )]
29    UnexpectedResourceType {
30        resource_id: String,
31        expected: ResourceType,
32        actual: ResourceType,
33    },
34
35    /// Attempt to update a resource that does not support updates.
36    #[error(
37        code = "INVALID_RESOURCE_UPDATE",
38        message = "Resource '{resource_id}' cannot be updated: {reason}",
39        retryable = "false",
40        internal = "false"
41    )]
42    InvalidResourceUpdate { resource_id: String, reason: String },
43
44    /// Worker execution timeout is outside the supported range.
45    #[error(
46        code = "WORKER_TIMEOUT_INVALID",
47        message = "Worker timeoutSeconds must be between 1 and {max_timeout_seconds} seconds, got {timeout_seconds}",
48        retryable = "false",
49        internal = "false",
50        http_status_code = 400
51    )]
52    WorkerTimeoutInvalid {
53        timeout_seconds: u32,
54        max_timeout_seconds: u32,
55    },
56
57    /// The resource exists but has not produced any outputs yet.
58    #[error(
59        code = "RESOURCE_HAS_NO_OUTPUTS",
60        message = "Resource '{resource_id}' has no outputs",
61        retryable = "true",
62        internal = "false"
63    )]
64    ResourceHasNoOutputs { resource_id: String },
65
66    /// Requested resource absent from the stack state.
67    #[error(
68        code = "RESOURCE_NOT_FOUND",
69        message = "Resource '{resource_id}' not found in stack state. Available resources: {available_resources:?}",
70        retryable = "false",
71        internal = "false",
72        http_status_code = 404
73    )]
74    ResourceNotFound {
75        resource_id: String,
76        available_resources: Vec<String>,
77    },
78
79    /// Binding configuration is invalid or missing required fields.
80    #[error(
81        code = "BINDING_CONFIG_INVALID",
82        message = "Invalid binding configuration for '{binding_name}': {reason}",
83        retryable = "false",
84        internal = "false"
85    )]
86    BindingConfigInvalid {
87        binding_name: String,
88        reason: String,
89    },
90
91    /// Environment variable for binding is missing.
92    #[error(
93        code = "BINDING_ENV_VAR_MISSING",
94        message = "Missing environment variable '{env_var}' for binding '{binding_name}'",
95        retryable = "false",
96        internal = "false"
97    )]
98    BindingEnvVarMissing {
99        binding_name: String,
100        env_var: String,
101    },
102
103    /// Failed to parse binding JSON from environment variable.
104    #[error(
105        code = "BINDING_JSON_PARSE_FAILED",
106        message = "Failed to parse binding JSON for '{binding_name}': {reason}",
107        retryable = "false",
108        internal = "false"
109    )]
110    BindingJsonParseFailed {
111        binding_name: String,
112        reason: String,
113    },
114
115    /// Unexpected combination of resource statuses when computing stack status.
116    #[error(
117        code = "UNEXPECTED_RESOURCE_STATUS_COMBINATION",
118        message = "Unexpected resource status combination during {operation}: {resource_statuses:?}",
119        retryable = "false",
120        internal = "true"
121    )]
122    UnexpectedResourceStatusCombination {
123        resource_statuses: Vec<String>,
124        operation: String,
125    },
126
127    /// External binding type does not match the resource type.
128    #[error(
129        code = "EXTERNAL_BINDING_TYPE_MISMATCH",
130        message = "External binding type mismatch for resource '{resource_id}': expected {expected}, got {actual}",
131        retryable = "false",
132        internal = "false"
133    )]
134    ExternalBindingTypeMismatch {
135        resource_id: String,
136        expected: String,
137        actual: String,
138    },
139
140    // Presigned request errors
141    /// Presigned request has expired.
142    #[error(
143        code = "PRESIGNED_REQUEST_EXPIRED",
144        message = "Presigned request for '{path}' expired at {expired_at}",
145        retryable = "false",
146        internal = "false"
147    )]
148    PresignedRequestExpired {
149        path: String,
150        expired_at: chrono::DateTime<chrono::Utc>,
151    },
152
153    /// HTTP request failed.
154    #[error(
155        code = "HTTP_REQUEST_FAILED",
156        message = "HTTP {method} request to '{url}' failed",
157        retryable = "true",
158        internal = "false"
159    )]
160    HttpRequestFailed { url: String, method: String },
161
162    /// Operation not supported.
163    #[error(
164        code = "OPERATION_NOT_SUPPORTED",
165        message = "Operation '{operation}' not supported: {reason}",
166        retryable = "false",
167        internal = "false"
168    )]
169    OperationNotSupported { operation: String, reason: String },
170
171    /// Local filesystem error.
172    #[error(
173        code = "LOCAL_FILESYSTEM_ERROR",
174        message = "Local filesystem error for '{path}' during {operation}",
175        retryable = "false",
176        internal = "false"
177    )]
178    LocalFilesystemError { path: String, operation: String },
179
180    /// Feature not enabled.
181    #[error(
182        code = "FEATURE_NOT_ENABLED",
183        message = "Feature '{feature}' is not enabled",
184        retryable = "false",
185        internal = "false"
186    )]
187    FeatureNotEnabled { feature: String },
188
189    /// A sandbox capability is unavailable on the target platform.
190    #[error(
191        code = "SANDBOX_CAPABILITY_UNSUPPORTED",
192        message = "Sandbox capability '{capability}' is not supported on {platform}",
193        retryable = "false",
194        internal = "false",
195        http_status_code = 400
196    )]
197    SandboxCapabilityUnsupported {
198        /// The capability that was requested
199        capability: String,
200        /// The platform that does not offer it
201        platform: String,
202    },
203
204    /// The target platform has no sandbox backend at all.
205    #[error(
206        code = "SANDBOX_PLATFORM_UNSUPPORTED",
207        message = "Sandbox is not available on {platform}",
208        retryable = "false",
209        internal = "false",
210        http_status_code = 400
211    )]
212    SandboxPlatformUnsupported {
213        /// The platform with no sandbox backend
214        platform: String,
215    },
216
217    /// A sandbox session capability was refused.
218    #[error(
219        code = "SANDBOX_CAPABILITY_REFUSED",
220        message = "Sandbox capability refused: {reason}",
221        retryable = "false",
222        internal = "false",
223        http_status_code = 403
224    )]
225    SandboxCapabilityRefused {
226        /// Why it was refused. Deliberately coarse — it must not reveal which sessions exist.
227        reason: String,
228    },
229
230    /// A sandbox limit is missing or not a valid quantity.
231    #[error(
232        code = "SANDBOX_LIMIT_INVALID",
233        message = "Sandbox '{resource_id}' has an invalid {field} limit '{value}': {reason}",
234        retryable = "false",
235        internal = "false",
236        http_status_code = 400
237    )]
238    SandboxLimitInvalid {
239        /// The sandbox whose limit failed validation
240        resource_id: String,
241        /// Which limit (cpu, memory, disk, maxProcesses)
242        field: String,
243        /// The rejected value
244        value: String,
245        /// Why it was rejected
246        reason: String,
247    },
248
249    // Commands protocol errors
250    /// Invalid Commands envelope.
251    #[error(
252        code = "INVALID_ENVELOPE",
253        message = "Invalid Commands envelope: {message}",
254        retryable = "false",
255        internal = "false"
256    )]
257    InvalidEnvelope {
258        message: String,
259        field: Option<String>,
260    },
261
262    /// A command-enabled resource exists but the deployment has no token to
263    /// authenticate its command transport.
264    #[error(
265        code = "COMMAND_TOKEN_MISSING",
266        message = "Deployment has command-enabled resource '{resource_id}' but no deployment token to authenticate it: {reason}",
267        retryable = "false",
268        internal = "false"
269    )]
270    CommandTokenMissing { resource_id: String, reason: String },
271
272    /// Public URL configuration is invalid.
273    #[error(
274        code = "PUBLIC_URL_INVALID",
275        message = "Invalid public URL for resource '{resource_id}': {reason}",
276        retryable = "false",
277        internal = "false",
278        http_status_code = 400
279    )]
280    PublicUrlInvalid { resource_id: String, reason: String },
281
282    /// JSON serialization failed.
283    #[error(
284        code = "JSON_SERIALIZATION_FAILED",
285        message = "Failed to serialize JSON: {reason}",
286        retryable = "false",
287        internal = "false"
288    )]
289    JsonSerializationFailed { reason: String },
290
291    /// JSON deserialization failed.
292    #[error(
293        code = "JSON_DESERIALIZATION_FAILED",
294        message = "Failed to deserialize JSON: {reason}",
295        retryable = "false",
296        internal = "false"
297    )]
298    JsonDeserializationFailed { reason: String },
299
300    /// Distribution template serialization failed.
301    #[error(
302        code = "TEMPLATE_SERIALIZATION_FAILED",
303        message = "Failed to serialize {format} template: {reason}",
304        retryable = "false",
305        internal = "true"
306    )]
307    TemplateSerializationFailed { format: String, reason: String },
308
309    /// ImportData did not match the registered typed payload.
310    #[error(
311        code = "IMPORT_DATA_INVALID",
312        message = "Invalid ImportData for resource '{resource_id}' ({resource_type}) on {platform}",
313        retryable = "false",
314        internal = "false",
315        http_status_code = 400
316    )]
317    ImportDataInvalid {
318        resource_id: String,
319        resource_type: ResourceType,
320        platform: Platform,
321    },
322
323    /// No registry implementation exists for a resource/platform pair.
324    #[error(
325        code = "IMPORT_REGISTRATION_MISSING",
326        message = "Missing {registration_kind} registration for resource type '{resource_type}' on {platform}",
327        retryable = "false",
328        internal = "true"
329    )]
330    ImportRegistrationMissing {
331        resource_type: ResourceType,
332        platform: Platform,
333        registration_kind: String,
334    },
335
336    /// ImportData schema serialization failed.
337    #[error(
338        code = "IMPORT_SCHEMA_SERIALIZATION_FAILED",
339        message = "Failed to serialize ImportData schema",
340        retryable = "false",
341        internal = "true"
342    )]
343    ImportSchemaSerializationFailed,
344}
345
346pub type Result<T> = alien_error::Result<T, ErrorData>;