1use std::fmt;
4
5use snafu::prelude::*;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum GrpcControlBoundaryKind {
10 QueueSaturated,
12 StreamClosed,
14 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#[derive(Snafu)]
30#[snafu(visibility(pub))]
31pub enum AnytypeGrpcError {
32 #[snafu(display("gRPC authentication failed (details redacted)"))]
34 Auth { source: AuthError },
35
36 #[snafu(display("gRPC configuration failed (details redacted)"))]
38 Config { source: ConfigError },
39
40 #[snafu(display("gRPC view operation failed (details redacted)"))]
42 View { source: ViewError },
43
44 #[snafu(display("gRPC backup operation failed (details redacted)"))]
46 Backup { source: BackupError },
47
48 #[snafu(display("gRPC transport failed (details redacted)"))]
50 Transport {
51 #[snafu(source(false))]
52 source: tonic::transport::Error,
53 },
54
55 #[snafu(
57 context(suffix(GrpcTimeoutConfigSnafu)),
58 display("gRPC deadline configuration error: {source}")
59 )]
60 TimeoutConfig {
61 source: crate::deadline::GrpcTimeoutConfigError,
62 },
63
64 #[snafu(context(suffix(GrpcDeadlineSnafu)), display("{source}"))]
66 Deadline {
67 source: crate::deadline::GrpcDeadlineError,
68 },
69
70 #[snafu(display("gRPC control boundary kind={kind} outcome={outcome}"))]
72 ControlBoundary {
73 kind: GrpcControlBoundaryKind,
75 outcome: crate::deadline::GrpcTimeoutOutcome,
77 },
78}
79
80#[derive(Snafu)]
82#[snafu(visibility(pub))]
83pub enum AuthError {
84 #[snafu(display("gRPC auth request failed with code {}", source.code()))]
86 Status {
87 #[snafu(source(false))]
88 source: tonic::Status,
89 },
90
91 #[snafu(display("Anytype auth API error ({code}; description redacted)"))]
93 Api { code: i32, description: String },
94
95 #[snafu(display("Create session returned empty token"))]
97 EmptyToken,
98
99 #[snafu(display("invalid authentication metadata (details redacted)"))]
101 InvalidMetadata {
102 #[snafu(source(false))]
103 source: tonic::metadata::errors::InvalidMetadataValue,
104 },
105
106 #[snafu(
108 context(suffix(AuthTimeoutConfigSnafu)),
109 display("gRPC deadline configuration error: {source}")
110 )]
111 TimeoutConfig {
112 source: crate::deadline::GrpcTimeoutConfigError,
113 },
114
115 #[snafu(context(suffix(AuthDeadlineSnafu)), display("{source}"))]
117 Deadline {
118 source: crate::deadline::GrpcDeadlineError,
119 },
120}
121
122#[derive(Snafu)]
124#[snafu(visibility(pub))]
125pub enum ConfigError {
126 #[snafu(display("configuration I/O failed (details redacted)"))]
128 Io {
129 #[snafu(source(false))]
130 source: std::io::Error,
131 },
132
133 #[snafu(display("configuration parsing failed (details redacted)"))]
135 Parse {
136 #[snafu(source(false))]
137 source: serde_json::Error,
138 },
139
140 #[snafu(display("home directory environment variable not set"))]
142 MissingHome,
143}
144
145#[derive(Snafu)]
147#[snafu(visibility(pub))]
148pub enum ViewError {
149 #[snafu(
151 context(suffix(ViewSnafu)),
152 display("view authentication failed (details redacted)")
153 )]
154 Auth { source: AuthError },
155
156 #[snafu(display("view gRPC request failed with code {}", source.code()))]
158 Rpc {
159 #[snafu(source(false))]
160 source: tonic::Status,
161 },
162
163 #[snafu(display("Anytype view API error ({code}; description redacted)"))]
165 ApiResponse { code: i32, description: String },
166
167 #[snafu(display("Object view missing in response"))]
169 MissingObjectView,
170
171 #[snafu(display("dataview block not found (view id redacted)"))]
173 MissingDataviewBlock { view_id: String },
174
175 #[snafu(display("view not found (id redacted)"))]
177 MissingView { view_id: String },
178
179 #[snafu(display("view is not supported (id redacted; type {actual})"))]
181 NotSupportedView { view_id: String, actual: i32 },
182}
183
184#[derive(Snafu)]
186#[snafu(visibility(pub))]
187pub enum BackupError {
188 #[snafu(display("backup gRPC request failed with code {}", source.code()))]
190 BackupRpc {
191 #[snafu(source(false))]
192 source: tonic::Status,
193 },
194
195 #[snafu(display("Anytype backup API error ({code}; description redacted)"))]
197 BackupApiResponse { code: i32, description: String },
198
199 #[snafu(display("backup authentication failed (details redacted)"))]
201 BackupAuth { source: AuthError },
202
203 #[snafu(display("invalid backup options (details redacted)"))]
205 InvalidOptions { message: String },
206
207 #[snafu(display("failed to resolve backup space name (details redacted)"))]
209 SpaceNameLookup { space_id: String, message: String },
210
211 #[snafu(display("Backup response missing export path"))]
213 MissingExportPath,
214
215 #[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 #[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 #[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
259impl 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
278impl 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
291impl 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
304impl 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
317impl 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}