Skip to main content

valence_core/
error.rs

1//! Error types for Valence routing and backends.
2
3use thiserror::Error;
4
5use crate::redact::redact_credentials_in_text;
6
7#[derive(Debug, Error)]
8pub enum Error {
9    #[error("Database error: {message}")]
10    Database {
11        message: String,
12        #[source]
13        source: Option<Box<dyn std::error::Error + Send + Sync>>,
14    },
15
16    #[error("Not found: {0}")]
17    NotFound(String),
18
19    #[error("Validation error: {0}")]
20    Validation(String),
21
22    #[error("Privacy policy violation: {0}")]
23    Privacy(String),
24
25    #[error("Internal error: {0}")]
26    Internal(String),
27
28    #[error("Serialization error: {message}")]
29    Serialization {
30        message: String,
31        #[source]
32        source: Option<serde_json::Error>,
33    },
34
35    #[error("Pending deletion: {0}")]
36    PendingDeletion(String),
37
38    #[error("Identity error: {0}")]
39    Identity(String),
40}
41
42impl From<serde_json::Error> for Error {
43    fn from(e: serde_json::Error) -> Self {
44        Self::serialization(e)
45    }
46}
47
48impl Error {
49    /// Build a [`Error::Database`] with URL userinfo redacted from the message.
50    #[must_use]
51    pub fn database(msg: impl AsRef<str>) -> Self {
52        Self::Database {
53            message: redact_credentials_in_text(msg.as_ref()),
54            source: None,
55        }
56    }
57
58    /// Build a [`Error::Database`] with a typed source cause and redacted message.
59    #[must_use]
60    pub fn database_with_source(
61        msg: impl AsRef<str>,
62        source: impl std::error::Error + Send + Sync + 'static,
63    ) -> Self {
64        Self::Database {
65            message: redact_credentials_in_text(msg.as_ref()),
66            source: Some(Box::new(source)),
67        }
68    }
69
70    /// Build a [`Error::Serialization`] that preserves the `serde_json` cause.
71    #[must_use]
72    pub fn serialization(e: serde_json::Error) -> Self {
73        Self::Serialization {
74            message: e.to_string(),
75            source: Some(e),
76        }
77    }
78
79    /// Build a [`Error::Serialization`] from a message when no serde cause is available.
80    #[must_use]
81    pub fn serialization_msg(msg: impl Into<String>) -> Self {
82        Self::Serialization {
83            message: msg.into(),
84            source: None,
85        }
86    }
87
88    /// True when the database engine reported MVCC / transaction contention that may succeed on retry.
89    pub fn is_retryable_transaction_contention(&self) -> bool {
90        match self {
91            Error::Database { message, .. } => {
92                let s = message.to_lowercase();
93                s.contains("read or write conflict")
94                    || s.contains("can be retried")
95                    || (s.contains("failed transaction") && s.contains("conflict"))
96            }
97            _ => false,
98        }
99    }
100}
101
102impl From<&str> for Error {
103    fn from(s: &str) -> Self {
104        Error::Validation(s.to_string())
105    }
106}
107
108pub type Result<T> = std::result::Result<T, Error>;
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn database_redacts_url_userinfo() {
116        let err = Error::database("connect failed: postgres://user:secret@host/db");
117        let s = err.to_string();
118        assert!(s.contains("postgres://***@host/db"));
119        assert!(!s.contains("secret"));
120    }
121
122    #[test]
123    fn serialization_preserves_source() {
124        let raw = serde_json::from_str::<u32>("not-json").expect_err("bad json");
125        let err = Error::from(raw);
126        let Error::Serialization {
127            source: Some(_), ..
128        } = err
129        else {
130            panic!("expected Serialization with source: {err:?}");
131        };
132    }
133}