Skip to main content

chroma_error/
lib.rs

1// Defines 17 standard error codes based on the error codes defined in the
2// gRPC spec. https://grpc.github.io/grpc/core/md_doc_statuscodes.html
3// Custom errors can use these codes in order to allow for generic handling
4use std::error::Error;
5
6#[cfg(feature = "tonic")]
7mod tonic;
8#[cfg(feature = "tonic")]
9pub use tonic::*;
10
11#[cfg(feature = "sqlx")]
12mod sqlx;
13#[cfg(feature = "sqlx")]
14pub use sqlx::*;
15
16#[cfg(feature = "validator")]
17mod validator;
18#[cfg(feature = "validator")]
19pub use validator::*;
20
21/// Returns true when any error in the source chain satisfies `predicate`.
22pub fn source_chain_contains(
23    source: &(dyn Error + 'static),
24    mut predicate: impl FnMut(&(dyn Error + 'static)) -> bool,
25) -> bool {
26    let mut current = Some(source);
27    while let Some(err) = current {
28        if predicate(err) {
29            return true;
30        }
31        current = err.source();
32    }
33    false
34}
35
36#[derive(PartialEq, Debug, Clone, Copy)]
37pub enum ErrorCodes {
38    // OK is returned on success, we use "Success" since Ok is a keyword in Rust.
39    Success = 0,
40    // CANCELLED indicates the operation was cancelled (typically by the caller).
41    Cancelled = 1,
42    // UNKNOWN indicates an unknown error.
43    Unknown = 2,
44    // INVALID_ARGUMENT indicates client specified an invalid argument.
45    InvalidArgument = 3,
46    // DEADLINE_EXCEEDED means operation expired before completion.
47    DeadlineExceeded = 4,
48    // NOT_FOUND means some requested entity (e.g., file or directory) was not found.
49    NotFound = 5,
50    // ALREADY_EXISTS means an entity that we attempted to create (e.g., file or directory) already exists.
51    AlreadyExists = 6,
52    // PERMISSION_DENIED indicates the caller does not have permission to execute the specified operation.
53    PermissionDenied = 7,
54    // RESOURCE_EXHAUSTED indicates some resource has been exhausted, perhaps a per-user quota, or perhaps the entire file system is out of space.
55    ResourceExhausted = 8,
56    // FAILED_PRECONDITION indicates operation was rejected because the system is not in a state required for the operation's execution.
57    FailedPrecondition = 9,
58    // ABORTED indicates the operation was aborted.
59    Aborted = 10,
60    // OUT_OF_RANGE means operation was attempted past the valid range.
61    OutOfRange = 11,
62    // UNIMPLEMENTED indicates operation is not implemented or not supported/enabled.
63    Unimplemented = 12,
64    // INTERNAL errors are internal errors.
65    Internal = 13,
66    // UNAVAILABLE indicates service is currently unavailable.
67    Unavailable = 14,
68    // DATA_LOSS indicates unrecoverable data loss or corruption.
69    DataLoss = 15,
70    // UNAUTHENTICATED indicates the request does not have valid authentication credentials for the operation.
71    Unauthenticated = 16,
72    // VERSION_MISMATCH indicates a version mismatch. This is not from the gRPC spec and is specific to Chroma.
73    VersionMismatch = 17,
74    // UNPROCESSABLE_ENTITY indicates the request is valid but cannot be processed.
75    UnprocessableEntity = 18,
76}
77
78impl ErrorCodes {
79    pub fn name(&self) -> &'static str {
80        match self {
81            ErrorCodes::InvalidArgument => "InvalidArgumentError",
82            ErrorCodes::NotFound => "NotFoundError",
83            ErrorCodes::Internal => "InternalError",
84            ErrorCodes::VersionMismatch => "VersionMismatchError",
85            _ => "ChromaError",
86        }
87    }
88}
89
90#[cfg(feature = "http")]
91impl From<ErrorCodes> for http::StatusCode {
92    fn from(error_code: ErrorCodes) -> Self {
93        match error_code {
94            ErrorCodes::Success => http::StatusCode::OK,
95            ErrorCodes::Cancelled => http::StatusCode::BAD_REQUEST,
96            ErrorCodes::Unknown => http::StatusCode::INTERNAL_SERVER_ERROR,
97            ErrorCodes::InvalidArgument => http::StatusCode::BAD_REQUEST,
98            ErrorCodes::DeadlineExceeded => http::StatusCode::GATEWAY_TIMEOUT,
99            ErrorCodes::NotFound => http::StatusCode::NOT_FOUND,
100            ErrorCodes::AlreadyExists => http::StatusCode::CONFLICT,
101            ErrorCodes::PermissionDenied => http::StatusCode::FORBIDDEN,
102            ErrorCodes::ResourceExhausted => http::StatusCode::TOO_MANY_REQUESTS,
103            ErrorCodes::FailedPrecondition => http::StatusCode::PRECONDITION_FAILED,
104            ErrorCodes::Aborted => http::StatusCode::BAD_REQUEST,
105            ErrorCodes::OutOfRange => http::StatusCode::BAD_REQUEST,
106            ErrorCodes::Unimplemented => http::StatusCode::NOT_IMPLEMENTED,
107            ErrorCodes::Internal => http::StatusCode::INTERNAL_SERVER_ERROR,
108            ErrorCodes::Unavailable => http::StatusCode::SERVICE_UNAVAILABLE,
109            ErrorCodes::DataLoss => http::StatusCode::INTERNAL_SERVER_ERROR,
110            ErrorCodes::Unauthenticated => http::StatusCode::UNAUTHORIZED,
111            ErrorCodes::VersionMismatch => http::StatusCode::INTERNAL_SERVER_ERROR,
112            ErrorCodes::UnprocessableEntity => http::StatusCode::UNPROCESSABLE_ENTITY,
113        }
114    }
115}
116
117#[cfg(feature = "http")]
118impl From<http::StatusCode> for ErrorCodes {
119    fn from(value: http::StatusCode) -> Self {
120        match value {
121            http::StatusCode::OK => ErrorCodes::Success,
122            http::StatusCode::BAD_REQUEST => ErrorCodes::InvalidArgument,
123            http::StatusCode::UNAUTHORIZED => ErrorCodes::Unauthenticated,
124            http::StatusCode::FORBIDDEN => ErrorCodes::PermissionDenied,
125            http::StatusCode::NOT_FOUND => ErrorCodes::NotFound,
126            http::StatusCode::CONFLICT => ErrorCodes::AlreadyExists,
127            http::StatusCode::TOO_MANY_REQUESTS => ErrorCodes::ResourceExhausted,
128            http::StatusCode::INTERNAL_SERVER_ERROR => ErrorCodes::Internal,
129            http::StatusCode::SERVICE_UNAVAILABLE => ErrorCodes::Unavailable,
130            http::StatusCode::NOT_IMPLEMENTED => ErrorCodes::Unimplemented,
131            http::StatusCode::GATEWAY_TIMEOUT => ErrorCodes::DeadlineExceeded,
132            http::StatusCode::PRECONDITION_FAILED => ErrorCodes::FailedPrecondition,
133            http::StatusCode::UNPROCESSABLE_ENTITY => ErrorCodes::UnprocessableEntity,
134            _ => ErrorCodes::Unknown,
135        }
136    }
137}
138
139pub trait ChromaError: Error + Send {
140    fn code(&self) -> ErrorCodes;
141    fn boxed(self) -> Box<dyn ChromaError>
142    where
143        Self: Sized + 'static,
144    {
145        Box::new(self)
146    }
147    fn should_trace_error(&self) -> bool {
148        true
149    }
150}
151
152impl Error for Box<dyn ChromaError> {}
153
154impl ChromaError for Box<dyn ChromaError> {
155    fn code(&self) -> ErrorCodes {
156        self.as_ref().code()
157    }
158}
159
160impl ChromaError for std::io::Error {
161    fn code(&self) -> ErrorCodes {
162        ErrorCodes::Unknown
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use std::{cell::Cell, error::Error, fmt};
169
170    use super::source_chain_contains;
171
172    #[derive(Debug)]
173    struct TestError {
174        message: &'static str,
175        source: Option<Box<dyn Error + Send + Sync + 'static>>,
176    }
177
178    impl TestError {
179        fn new(message: &'static str) -> Self {
180            Self {
181                message,
182                source: None,
183            }
184        }
185
186        fn with_source(message: &'static str, source: impl Error + Send + Sync + 'static) -> Self {
187            Self {
188                message,
189                source: Some(Box::new(source)),
190            }
191        }
192    }
193
194    impl fmt::Display for TestError {
195        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196            write!(f, "{}", self.message)
197        }
198    }
199
200    impl Error for TestError {
201        fn source(&self) -> Option<&(dyn Error + 'static)> {
202            self.source
203                .as_deref()
204                .map(|err| err as &(dyn Error + 'static))
205        }
206    }
207
208    #[test]
209    fn source_chain_contains_matches_root_error() {
210        let err = TestError::new("root");
211
212        assert!(source_chain_contains(&err, |candidate| {
213            candidate.to_string() == "root"
214        }));
215    }
216
217    #[test]
218    fn source_chain_contains_matches_nested_source_error() {
219        let err = TestError::with_source(
220            "root",
221            TestError::with_source("middle", TestError::new("leaf")),
222        );
223
224        assert!(source_chain_contains(&err, |candidate| {
225            candidate.to_string() == "leaf"
226        }));
227    }
228
229    #[test]
230    fn source_chain_contains_returns_false_when_no_error_matches() {
231        let err = TestError::with_source(
232            "root",
233            TestError::with_source("middle", TestError::new("leaf")),
234        );
235
236        assert!(!source_chain_contains(&err, |candidate| {
237            candidate.to_string() == "missing"
238        }));
239    }
240
241    #[test]
242    fn source_chain_contains_stops_after_first_match() {
243        let err = TestError::with_source(
244            "root",
245            TestError::with_source("middle", TestError::new("leaf")),
246        );
247        let visits = Cell::new(0);
248
249        assert!(source_chain_contains(&err, |candidate| {
250            visits.set(visits.get() + 1);
251            candidate.to_string() == "middle"
252        }));
253        assert_eq!(2, visits.get());
254    }
255}