Skip to main content

rustferry_remote/
error.rs

1use thiserror::Error;
2
3use crate::{
4    protocol::{JobState, ProtocolVersion},
5    provider::ProviderFeature,
6};
7
8/// Result returned by remote-build protocol and provider operations.
9pub type RemoteBuildResult<T> = Result<T, RemoteBuildError>;
10
11/// Typed failures crossing the remote-build boundary.
12#[derive(Clone, Debug, Eq, Error, PartialEq)]
13pub enum RemoteBuildError {
14    /// The peer uses an incompatible protocol major version.
15    #[error("remote protocol version {received} is incompatible; this client supports {supported}")]
16    IncompatibleProtocolVersion {
17        /// Highest protocol version understood by this implementation.
18        supported: ProtocolVersion,
19        /// Version sent by the peer.
20        received: ProtocolVersion,
21    },
22    /// A stream ended before one complete JSON event was available.
23    #[error("remote event ended before a complete JSON object was received")]
24    TruncatedEvent,
25    /// A remote event was not valid JSON or did not match the protocol envelope.
26    #[error("remote event is malformed: {message}")]
27    MalformedEvent {
28        /// Bounded parser description; never raw worker output.
29        message: String,
30    },
31    /// Event bytes were not UTF-8.
32    #[error("remote event is not valid UTF-8")]
33    InvalidUtf8,
34    /// One event exceeded the protocol decoder limit.
35    #[error("remote event is {bytes} bytes; maximum accepted size is {maximum} bytes")]
36    EventTooLarge {
37        /// Received byte count.
38        bytes: usize,
39        /// Decoder limit.
40        maximum: usize,
41    },
42    /// A stable identifier was empty, too long, or contained control characters.
43    #[error("remote field `{field}` is invalid: {reason}")]
44    InvalidIdentifier {
45        /// Protocol field name.
46        field: &'static str,
47        /// Stable validation reason.
48        reason: &'static str,
49    },
50    /// Event text contained terminal control sequences.
51    #[error("remote field `{field}` contains terminal control data")]
52    UnsafeEventText {
53        /// Protocol field containing unsafe text.
54        field: &'static str,
55    },
56    /// A typed event carried inconsistent or impossible values.
57    #[error("remote event `{event}` is invalid: {reason}")]
58    InvalidEventPayload {
59        /// Stable event name.
60        event: &'static str,
61        /// Stable validation reason.
62        reason: &'static str,
63    },
64    /// The required source manifest was malformed or non-canonical.
65    #[error("source manifest is invalid: {message}")]
66    InvalidSourceManifest {
67        /// Sanitized source-validation summary.
68        message: String,
69    },
70    /// A provider reported an impossible job-state change.
71    #[error("job cannot transition from {from:?} to {to:?}")]
72    InvalidJobTransition {
73        /// State before the transition.
74        from: JobState,
75        /// Requested state.
76        to: JobState,
77    },
78    /// The caller requested cancellation.
79    #[error("remote operation was cancelled")]
80    Cancelled,
81    /// The selected provider cannot satisfy a required feature.
82    #[error("provider `{provider}` does not support {feature}")]
83    UnsupportedCapability {
84        /// Stable provider identifier.
85        provider: String,
86        /// Missing typed feature.
87        feature: ProviderFeature,
88    },
89    /// A provider failed with a stable provider-specific code.
90    #[error("provider `{provider}` failed with `{code}`: {message}")]
91    ProviderFailure {
92        /// Stable provider identifier.
93        provider: String,
94        /// Stable provider-specific error code.
95        code: String,
96        /// Sanitized failure summary.
97        message: String,
98        /// Whether retrying the same operation may succeed.
99        retryable: bool,
100    },
101    /// A requested artifact is absent from the job manifest.
102    #[error("artifact `{artifact_id}` was not found for job `{job_id}`")]
103    ArtifactNotFound {
104        /// Stable job identifier.
105        job_id: String,
106        /// Stable artifact identifier.
107        artifact_id: String,
108    },
109    /// Downloaded bytes do not match the signed manifest digest.
110    #[error("artifact `{artifact_id}` digest mismatch: expected {expected}, received {actual}")]
111    IntegrityMismatch {
112        /// Stable artifact identifier.
113        artifact_id: String,
114        /// Manifest SHA-256 digest.
115        expected: String,
116        /// Downloaded SHA-256 digest.
117        actual: String,
118    },
119    /// A local protocol object could not be serialized.
120    #[error("could not serialize remote protocol data: {message}")]
121    Serialization {
122        /// Bounded serializer description.
123        message: String,
124    },
125}
126
127impl RemoteBuildError {
128    /// Stable machine-readable error code.
129    pub const fn code(&self) -> &'static str {
130        match self {
131            Self::IncompatibleProtocolVersion { .. } => "incompatible_protocol_version",
132            Self::TruncatedEvent => "truncated_event",
133            Self::MalformedEvent { .. } => "malformed_event",
134            Self::InvalidUtf8 => "invalid_utf8",
135            Self::EventTooLarge { .. } => "event_too_large",
136            Self::InvalidIdentifier { .. } => "invalid_identifier",
137            Self::UnsafeEventText { .. } => "unsafe_event_text",
138            Self::InvalidEventPayload { .. } => "invalid_event_payload",
139            Self::InvalidSourceManifest { .. } => "invalid_source_manifest",
140            Self::InvalidJobTransition { .. } => "invalid_job_transition",
141            Self::Cancelled => "cancelled",
142            Self::UnsupportedCapability { .. } => "unsupported_capability",
143            Self::ProviderFailure { .. } => "provider_failure",
144            Self::ArtifactNotFound { .. } => "artifact_not_found",
145            Self::IntegrityMismatch { .. } => "integrity_mismatch",
146            Self::Serialization { .. } => "serialization_failed",
147        }
148    }
149
150    /// Whether retrying the same operation may succeed without user changes.
151    pub const fn retryable(&self) -> bool {
152        match self {
153            Self::ProviderFailure { retryable, .. } => *retryable,
154            Self::TruncatedEvent | Self::Cancelled => true,
155            Self::IncompatibleProtocolVersion { .. }
156            | Self::MalformedEvent { .. }
157            | Self::InvalidUtf8
158            | Self::EventTooLarge { .. }
159            | Self::InvalidIdentifier { .. }
160            | Self::UnsafeEventText { .. }
161            | Self::InvalidEventPayload { .. }
162            | Self::InvalidSourceManifest { .. }
163            | Self::InvalidJobTransition { .. }
164            | Self::UnsupportedCapability { .. }
165            | Self::ArtifactNotFound { .. }
166            | Self::IntegrityMismatch { .. }
167            | Self::Serialization { .. } => false,
168        }
169    }
170}