Skip to main content

anytype_rpc/
error.rs

1//! Errors returned by anytype-rpc gRPC operations.
2
3use std::fmt;
4
5use snafu::prelude::*;
6
7/// Local stream boundary that interrupted a control mutation.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum GrpcControlBoundaryKind {
10    /// The decoded event queue reached its configured capacity.
11    QueueSaturated,
12    /// The session event stream closed before a terminal mutation result.
13    StreamClosed,
14    /// The session event transport failed before a terminal mutation result.
15    TransportLost,
16}
17
18impl fmt::Display for GrpcControlBoundaryKind {
19    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
20        formatter.write_str(match self {
21            Self::QueueSaturated => "queue_saturated",
22            Self::StreamClosed => "stream_closed",
23            Self::TransportLost => "transport_lost",
24        })
25    }
26}
27
28/// Unified error type for anytype-rpc gRPC operations.
29#[derive(Snafu)]
30#[snafu(visibility(pub))]
31pub enum AnytypeGrpcError {
32    /// Authentication error.
33    #[snafu(display("gRPC authentication failed (details redacted)"))]
34    Auth { source: AuthError },
35
36    /// Configuration error.
37    #[snafu(display("gRPC configuration failed (details redacted)"))]
38    Config { source: ConfigError },
39
40    /// View operation error.
41    #[snafu(display("gRPC view operation failed (details redacted)"))]
42    View { source: ViewError },
43
44    /// Space backup operation error.
45    #[snafu(display("gRPC backup operation failed (details redacted)"))]
46    Backup { source: BackupError },
47
48    /// gRPC transport connection error.
49    #[snafu(display("gRPC transport failed (details redacted)"))]
50    Transport {
51        #[snafu(source(false))]
52        source: tonic::transport::Error,
53    },
54
55    /// Logical gRPC deadline configuration error.
56    #[snafu(
57        context(suffix(GrpcTimeoutConfigSnafu)),
58        display("gRPC deadline configuration error: {source}")
59    )]
60    TimeoutConfig {
61        source: crate::deadline::GrpcTimeoutConfigError,
62    },
63
64    /// Stable, payload-free logical gRPC deadline expiration.
65    #[snafu(context(suffix(GrpcDeadlineSnafu)), display("{source}"))]
66    Deadline {
67        source: crate::deadline::GrpcDeadlineError,
68    },
69
70    /// A local session-stream boundary interrupted a control mutation.
71    #[snafu(display("gRPC control boundary kind={kind} outcome={outcome}"))]
72    ControlBoundary {
73        /// Closed boundary classification without peer-provided text.
74        kind: GrpcControlBoundaryKind,
75        /// Whether the control future could have dispatched before interruption.
76        outcome: crate::deadline::GrpcTimeoutOutcome,
77    },
78}
79
80/// Errors from authentication operations.
81#[derive(Snafu)]
82#[snafu(visibility(pub))]
83pub enum AuthError {
84    /// gRPC status error from a request.
85    #[snafu(display("gRPC auth request failed with code {}", source.code()))]
86    Status {
87        #[snafu(source(false))]
88        source: tonic::Status,
89    },
90
91    /// Anytype API returned an error response.
92    #[snafu(display("Anytype auth API error ({code}; description redacted)"))]
93    Api { code: i32, description: String },
94
95    /// Create session returned an empty token.
96    #[snafu(display("Create session returned empty token"))]
97    EmptyToken,
98
99    /// Invalid metadata value for auth token.
100    #[snafu(display("invalid authentication metadata (details redacted)"))]
101    InvalidMetadata {
102        #[snafu(source(false))]
103        source: tonic::metadata::errors::InvalidMetadataValue,
104    },
105
106    /// Logical deadline policy could not be resolved.
107    #[snafu(
108        context(suffix(AuthTimeoutConfigSnafu)),
109        display("gRPC deadline configuration error: {source}")
110    )]
111    TimeoutConfig {
112        source: crate::deadline::GrpcTimeoutConfigError,
113    },
114
115    /// Credential setup reached its logical deadline.
116    #[snafu(context(suffix(AuthDeadlineSnafu)), display("{source}"))]
117    Deadline {
118        source: crate::deadline::GrpcDeadlineError,
119    },
120}
121
122/// Errors from configuration operations.
123#[derive(Snafu)]
124#[snafu(visibility(pub))]
125pub enum ConfigError {
126    /// Config file I/O error.
127    #[snafu(display("configuration I/O failed (details redacted)"))]
128    Io {
129        #[snafu(source(false))]
130        source: std::io::Error,
131    },
132
133    /// Config file parse error.
134    #[snafu(display("configuration parsing failed (details redacted)"))]
135    Parse {
136        #[snafu(source(false))]
137        source: serde_json::Error,
138    },
139
140    /// Home-directory environment variables are unavailable.
141    #[snafu(display("home directory environment variable not set"))]
142    MissingHome,
143}
144
145/// Errors from view operations.
146#[derive(Snafu)]
147#[snafu(visibility(pub))]
148pub enum ViewError {
149    /// Authentication token attachment failed before request dispatch.
150    #[snafu(
151        context(suffix(ViewSnafu)),
152        display("view authentication failed (details redacted)")
153    )]
154    Auth { source: AuthError },
155
156    /// gRPC status error from a request.
157    #[snafu(display("view gRPC request failed with code {}", source.code()))]
158    Rpc {
159        #[snafu(source(false))]
160        source: tonic::Status,
161    },
162
163    /// Anytype API returned an error response.
164    #[snafu(display("Anytype view API error ({code}; description redacted)"))]
165    ApiResponse { code: i32, description: String },
166
167    /// Object view missing in response.
168    #[snafu(display("Object view missing in response"))]
169    MissingObjectView,
170
171    /// Dataview block not found for view id.
172    #[snafu(display("dataview block not found (view id redacted)"))]
173    MissingDataviewBlock { view_id: String },
174
175    /// View id not found.
176    #[snafu(display("view not found (id redacted)"))]
177    MissingView { view_id: String },
178
179    /// View type not supported.
180    #[snafu(display("view is not supported (id redacted; type {actual})"))]
181    NotSupportedView { view_id: String, actual: i32 },
182}
183
184/// Errors from space backup operations.
185#[derive(Snafu)]
186#[snafu(visibility(pub))]
187pub enum BackupError {
188    /// gRPC status error from a request.
189    #[snafu(display("backup gRPC request failed with code {}", source.code()))]
190    BackupRpc {
191        #[snafu(source(false))]
192        source: tonic::Status,
193    },
194
195    /// Anytype API returned an error response.
196    #[snafu(display("Anytype backup API error ({code}; description redacted)"))]
197    BackupApiResponse { code: i32, description: String },
198
199    /// Authentication token metadata was invalid.
200    #[snafu(display("backup authentication failed (details redacted)"))]
201    BackupAuth { source: AuthError },
202
203    /// Backup options were invalid.
204    #[snafu(display("invalid backup options (details redacted)"))]
205    InvalidOptions { message: String },
206
207    /// Failed to resolve the friendly name for a space.
208    #[snafu(display("failed to resolve backup space name (details redacted)"))]
209    SpaceNameLookup { space_id: String, message: String },
210
211    /// Server response did not include an export path.
212    #[snafu(display("Backup response missing export path"))]
213    MissingExportPath,
214
215    /// Failed to create or access a local path.
216    #[snafu(display("backup I/O failed (details redacted)"))]
217    BackupIo {
218        path: std::path::PathBuf,
219        #[snafu(source(false))]
220        source: std::io::Error,
221    },
222
223    /// Failed to move generated backup to its final target path.
224    #[snafu(display("moving backup output failed (details redacted)"))]
225    BackupMove {
226        from: std::path::PathBuf,
227        to: std::path::PathBuf,
228        #[snafu(source(false))]
229        source: std::io::Error,
230    },
231
232    /// A backup gRPC operation reached its logical deadline.
233    #[snafu(context(suffix(BackupDeadlineSnafu)), display("{source}"))]
234    Deadline {
235        source: crate::deadline::GrpcDeadlineError,
236    },
237}
238
239macro_rules! impl_redacted_debug {
240    ($($error:ty),+ $(,)?) => {
241        $(
242            impl fmt::Debug for $error {
243                fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
244                    fmt::Display::fmt(self, formatter)
245                }
246            }
247        )+
248    };
249}
250
251impl_redacted_debug!(
252    AnytypeGrpcError,
253    AuthError,
254    ConfigError,
255    ViewError,
256    BackupError,
257);
258
259// From impls for AuthError
260impl From<tonic::Status> for AuthError {
261    fn from(source: tonic::Status) -> Self {
262        AuthError::Status { source }
263    }
264}
265
266impl From<tonic::metadata::errors::InvalidMetadataValue> for AuthError {
267    fn from(source: tonic::metadata::errors::InvalidMetadataValue) -> Self {
268        AuthError::InvalidMetadata { source }
269    }
270}
271
272impl From<crate::deadline::GrpcTimeoutConfigError> for AuthError {
273    fn from(source: crate::deadline::GrpcTimeoutConfigError) -> Self {
274        AuthError::TimeoutConfig { source }
275    }
276}
277
278// From impls for ConfigError
279impl From<std::io::Error> for ConfigError {
280    fn from(source: std::io::Error) -> Self {
281        ConfigError::Io { source }
282    }
283}
284
285impl From<serde_json::Error> for ConfigError {
286    fn from(source: serde_json::Error) -> Self {
287        ConfigError::Parse { source }
288    }
289}
290
291// From impls for ViewError
292impl From<AuthError> for ViewError {
293    fn from(source: AuthError) -> Self {
294        ViewError::Auth { source }
295    }
296}
297
298impl From<tonic::Status> for ViewError {
299    fn from(source: tonic::Status) -> Self {
300        ViewError::Rpc { source }
301    }
302}
303
304// From impls for BackupError
305impl From<tonic::Status> for BackupError {
306    fn from(source: tonic::Status) -> Self {
307        BackupError::BackupRpc { source }
308    }
309}
310
311impl From<AuthError> for BackupError {
312    fn from(source: AuthError) -> Self {
313        BackupError::BackupAuth { source }
314    }
315}
316
317// From impls for AnytypeGrpcError
318impl From<AuthError> for AnytypeGrpcError {
319    fn from(source: AuthError) -> Self {
320        match source {
321            AuthError::TimeoutConfig { source } => AnytypeGrpcError::TimeoutConfig { source },
322            AuthError::Deadline { source } => AnytypeGrpcError::Deadline { source },
323            source => AnytypeGrpcError::Auth { source },
324        }
325    }
326}
327
328impl From<ConfigError> for AnytypeGrpcError {
329    fn from(source: ConfigError) -> Self {
330        AnytypeGrpcError::Config { source }
331    }
332}
333
334impl From<ViewError> for AnytypeGrpcError {
335    fn from(source: ViewError) -> Self {
336        AnytypeGrpcError::View { source }
337    }
338}
339
340impl From<BackupError> for AnytypeGrpcError {
341    fn from(source: BackupError) -> Self {
342        match source {
343            BackupError::Deadline { source } => AnytypeGrpcError::Deadline { source },
344            source => AnytypeGrpcError::Backup { source },
345        }
346    }
347}
348
349impl From<tonic::transport::Error> for AnytypeGrpcError {
350    fn from(source: tonic::transport::Error) -> Self {
351        AnytypeGrpcError::Transport { source }
352    }
353}
354
355impl From<crate::deadline::GrpcTimeoutConfigError> for AnytypeGrpcError {
356    fn from(source: crate::deadline::GrpcTimeoutConfigError) -> Self {
357        AnytypeGrpcError::TimeoutConfig { source }
358    }
359}
360
361impl From<crate::deadline::GrpcDeadlineError> for AnytypeGrpcError {
362    fn from(source: crate::deadline::GrpcDeadlineError) -> Self {
363        AnytypeGrpcError::Deadline { source }
364    }
365}
366
367#[cfg(test)]
368mod tests {
369    use super::*;
370
371    const SECRET: &str = "HOSTILE_RPC_PEER_SECRET";
372
373    fn assert_redacted<E>(error: &E)
374    where
375        E: std::error::Error,
376    {
377        assert!(!error.to_string().contains(SECRET));
378        assert!(!format!("{error:?}").contains(SECRET));
379        let mut source = error.source();
380        while let Some(current) = source {
381            assert!(!current.to_string().contains(SECRET));
382            assert!(!format!("{current:?}").contains(SECRET));
383            source = current.source();
384        }
385    }
386
387    #[test]
388    fn public_rpc_error_families_redact_peer_controlled_details_and_sources() {
389        let auth_status = AuthError::Status {
390            source: tonic::Status::internal(SECRET),
391        };
392        assert_redacted(&auth_status);
393        assert_redacted(&AuthError::Api {
394            code: 500,
395            description: SECRET.to_owned(),
396        });
397
398        let config = ConfigError::Io {
399            source: std::io::Error::other(SECRET),
400        };
401        assert_redacted(&config);
402
403        let view_status = ViewError::Rpc {
404            source: tonic::Status::invalid_argument(SECRET),
405        };
406        assert_redacted(&view_status);
407        assert_redacted(&ViewError::ApiResponse {
408            code: 400,
409            description: SECRET.to_owned(),
410        });
411        assert_redacted(&ViewError::MissingView {
412            view_id: SECRET.to_owned(),
413        });
414
415        let backup_status = BackupError::BackupRpc {
416            source: tonic::Status::unavailable(SECRET),
417        };
418        assert_redacted(&backup_status);
419        assert_redacted(&BackupError::BackupApiResponse {
420            code: 503,
421            description: SECRET.to_owned(),
422        });
423        assert_redacted(&BackupError::SpaceNameLookup {
424            space_id: SECRET.to_owned(),
425            message: SECRET.to_owned(),
426        });
427
428        assert_redacted(&AnytypeGrpcError::Auth {
429            source: AuthError::Api {
430                code: 500,
431                description: SECRET.to_owned(),
432            },
433        });
434        let transport = tonic::transport::Endpoint::from_shared(format!("not {SECRET}"))
435            .expect_err("hostile endpoint is invalid");
436        assert_redacted(&AnytypeGrpcError::Transport { source: transport });
437        assert_redacted(&AnytypeGrpcError::ControlBoundary {
438            kind: GrpcControlBoundaryKind::QueueSaturated,
439            outcome: crate::deadline::GrpcTimeoutOutcome::MutationIndeterminate,
440        });
441    }
442}